Ver código fonte

Nat Gateway Network Costs Support (#3534)

Signed-off-by: Matt Bolt <mbolt35@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Matt Bolt 3 meses atrás
pai
commit
4bdf813a30
38 arquivos alterados com 1214 adições e 117 exclusões
  1. 2 0
      configs/alibaba.json
  2. 4 2
      configs/aws.json
  3. 3 1
      configs/azure.json
  4. 3 1
      configs/default.json
  5. 3 1
      configs/gcp.json
  6. 4 2
      configs/oracle.json
  7. 4 2
      configs/otc.json
  8. 21 0
      core/pkg/opencost/allocation.go
  9. 2 0
      core/pkg/opencost/allocation_test.go
  10. 1 1
      core/pkg/opencost/bingen.go
  11. 29 9
      core/pkg/opencost/opencost_codecs.go
  12. 4 0
      core/pkg/source/datasource.go
  13. 17 0
      core/pkg/source/decoders.go
  14. 94 0
      modules/collector-source/pkg/collector/collector.go
  15. 16 0
      modules/collector-source/pkg/collector/metricsquerier.go
  16. 4 0
      modules/collector-source/pkg/metric/collector.go
  17. 18 16
      modules/collector-source/pkg/metric/metrics.go
  18. 74 0
      modules/prometheus-source/pkg/prom/metricsquerier.go
  19. 10 0
      pkg/cloud/alibaba/provider.go
  20. 10 0
      pkg/cloud/aws/provider.go
  21. 10 0
      pkg/cloud/azure/provider.go
  22. 14 4
      pkg/cloud/digitalocean/provider.go
  23. 10 0
      pkg/cloud/gcp/provider.go
  24. 2 0
      pkg/cloud/gcp/provider_test.go
  25. 3 1
      pkg/cloud/models/models.go
  26. 4 0
      pkg/cloud/models/models_test.go
  27. 2 0
      pkg/cloud/models/network.go
  28. 2 0
      pkg/cloud/oracle/ratecard.go
  29. 10 0
      pkg/cloud/otc/provider.go
  30. 10 0
      pkg/cloud/provider/customprovider.go
  31. 28 12
      pkg/cloud/provider/providerconfig.go
  32. 2 0
      pkg/cloud/scaleway/provider.go
  33. 12 0
      pkg/costmodel/allocation.go
  34. 8 0
      pkg/costmodel/allocation_helpers.go
  35. 9 5
      pkg/costmodel/costmodel.go
  36. 75 51
      pkg/costmodel/metrics.go
  37. 72 9
      pkg/costmodel/networkcosts.go
  38. 618 0
      pkg/costmodel/networkcosts_test.go

+ 2 - 0
configs/alibaba.json

@@ -12,5 +12,7 @@
     "zoneNetworkEgress": "0.02",
     "regionNetworkEgress": "0.08",
     "internetNetworkEgress": "0.123",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045",
     "defaultLBPrice": "0.007"
 }

+ 4 - 2
configs/aws.json

@@ -10,11 +10,13 @@
     "zoneNetworkEgress": "0.01",
     "regionNetworkEgress": "0.01",
     "internetNetworkEgress": "0.143",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045",
     "spotLabel": "kops.k8s.io/instancegroup",
     "spotLabelValue": "spotinstance-nodes",
     "awsServiceKeyName": "",
     "awsServiceKeySecret": "",
-    "awsSpotDataRegion":"",
+    "awsSpotDataRegion": "",
     "awsSpotDataBucket": "",
     "awsSpotDataPrefix": "",
     "athenaBucketName": "",
@@ -22,4 +24,4 @@
     "athenaDatabase": "",
     "athenaTable": "",
     "projectID": ""
-}
+}

+ 3 - 1
configs/azure.json

@@ -10,10 +10,12 @@
     "zoneNetworkEgress": "0.01",
     "regionNetworkEgress": "0.01",
     "internetNetworkEgress": "0.0725",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045",
     "spotLabel": "kubernetes.azure.com/scalesetpriority",
     "spotLabelValue": "spot",
     "azureSubscriptionID": "",
     "azureClientID": "",
     "azureClientSecret": "",
     "azureTenantID": ""
-}
+}

+ 3 - 1
configs/default.json

@@ -9,5 +9,7 @@
     "storage": "0.00005479452",
     "zoneNetworkEgress": "0.01",
     "regionNetworkEgress": "0.01",
-    "internetNetworkEgress": "0.12"
+    "internetNetworkEgress": "0.12",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045"
 }

+ 3 - 1
configs/gcp.json

@@ -10,5 +10,7 @@
     "zoneNetworkEgress": "0.01",
     "regionNetworkEgress": "0.01",
     "internetNetworkEgress": "0.12",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045",
     "billingDataDataset": ""
-}
+}

+ 4 - 2
configs/oracle.json

@@ -1,9 +1,11 @@
 {
-  "provider":"Oracle",
+  "provider": "Oracle",
   "CPU": "0.015",
   "RAM": "0.002",
   "GPU": "2.00",
   "storage": "0.00005479452",
   "defaultLBPrice": "0.0113",
-  "internetNetworkEgress": "0.0085"
+  "internetNetworkEgress": "0.0085",
+  "natGatewayEgress": "0.045",
+  "natGatewayIngress": "0.045"
 }

+ 4 - 2
configs/otc.json

@@ -6,5 +6,7 @@
     "storage": "0.0",
     "zoneNetworkEgress": "0.0",
     "regionNetworkEgress": "0.0",
-    "internetNetworkEgress": "0.0"
-}
+    "internetNetworkEgress": "0.0",
+    "natGatewayEgress": "0.0",
+    "natGatewayIngress": "0.0"
+}

+ 21 - 0
core/pkg/opencost/allocation.go

@@ -106,6 +106,9 @@ type Allocation struct {
 	GPUAllocation               *GPUAllocation `json:"GPUAllocation"`       //@bingen:field[version=23]
 	CPUCoreLimitAverage         float64        `json:"cpuCoreLimitAverage"` //@bingen:field[version=24]
 	RAMBytesLimitAverage        float64        `json:"ramByteLimitAverage"` //@bingen:field[version=24]
+	// NAT Gateway Costs
+	NetworkNatGatewayEgressCost  float64 `json:"networkNatGatewayEgressCost"`  //@bingen:field[version=25]
+	NetworkNatGatewayIngressCost float64 `json:"networkNatGatewayIngressCost"` //@bingen:field[version=25]
 }
 
 type GPUAllocation struct {
@@ -757,6 +760,8 @@ func (a *Allocation) Clone() *Allocation {
 		NetworkCrossZoneCost:           a.NetworkCrossZoneCost,
 		NetworkCrossRegionCost:         a.NetworkCrossRegionCost,
 		NetworkInternetCost:            a.NetworkInternetCost,
+		NetworkNatGatewayEgressCost:    a.NetworkNatGatewayEgressCost,
+		NetworkNatGatewayIngressCost:   a.NetworkNatGatewayIngressCost,
 		NetworkCostAdjustment:          a.NetworkCostAdjustment,
 		LoadBalancerCost:               a.LoadBalancerCost,
 		LoadBalancerCostAdjustment:     a.LoadBalancerCostAdjustment,
@@ -847,6 +852,12 @@ func (a *Allocation) Equal(that *Allocation) bool {
 	if !util.IsApproximately(a.NetworkInternetCost, that.NetworkInternetCost) {
 		return false
 	}
+	if !util.IsApproximately(a.NetworkNatGatewayEgressCost, that.NetworkNatGatewayEgressCost) {
+		return false
+	}
+	if !util.IsApproximately(a.NetworkNatGatewayIngressCost, that.NetworkNatGatewayIngressCost) {
+		return false
+	}
 	if !util.IsApproximately(a.NetworkCostAdjustment, that.NetworkCostAdjustment) {
 		return false
 	}
@@ -1409,6 +1420,8 @@ func (a *Allocation) add(that *Allocation) {
 	a.NetworkCrossZoneCost += that.NetworkCrossZoneCost
 	a.NetworkCrossRegionCost += that.NetworkCrossRegionCost
 	a.NetworkInternetCost += that.NetworkInternetCost
+	a.NetworkNatGatewayEgressCost += that.NetworkNatGatewayEgressCost
+	a.NetworkNatGatewayIngressCost += that.NetworkNatGatewayIngressCost
 	a.LoadBalancerCost += that.LoadBalancerCost
 	a.SharedCost += that.SharedCost
 	a.ExternalCost += that.ExternalCost
@@ -2766,6 +2779,14 @@ func (a *Allocation) SanitizeNaN() {
 		log.DedupedWarningf(5, "Allocation: Unexpected NaN found for NetworkInternetCost name:%s, window:%s, properties:%s", a.Name, a.Window.String(), a.Properties.String())
 		a.NetworkInternetCost = 0
 	}
+	if math.IsNaN(a.NetworkNatGatewayEgressCost) {
+		log.DedupedWarningf(5, "Allocation: Unexpected NaN found for NetworkNatGatewayEgressCost name:%s, window:%s, properties:%s", a.Name, a.Window.String(), a.Properties.String())
+		a.NetworkNatGatewayEgressCost = 0
+	}
+	if math.IsNaN(a.NetworkNatGatewayIngressCost) {
+		log.DedupedWarningf(5, "Allocation: Unexpected NaN found for NetworkNatGatewayIngressCost name:%s, window:%s, properties:%s", a.Name, a.Window.String(), a.Properties.String())
+		a.NetworkNatGatewayIngressCost = 0
+	}
 	if math.IsNaN(a.NetworkCostAdjustment) {
 		log.DedupedWarningf(5, "Allocation: Unexpected NaN found for NetworkCostAdjustment name:%s, window:%s, properties:%s", a.Name, a.Window.String(), a.Properties.String())
 		a.NetworkCostAdjustment = 0

+ 2 - 0
core/pkg/opencost/allocation_test.go

@@ -3734,6 +3734,8 @@ func getMockAllocation(f float64) Allocation {
 		NetworkCrossRegionCost:         f,
 		NetworkInternetCost:            f,
 		NetworkCostAdjustment:          f,
+		NetworkNatGatewayEgressCost:    f,
+		NetworkNatGatewayIngressCost:   f,
 		LoadBalancerCost:               f,
 		LoadBalancerCostAdjustment:     f,
 		PVs:                            PVAllocations{{Cluster: "testPV", Name: "PVName"}: getMockPVAllocation(math.NaN())},

+ 1 - 1
core/pkg/opencost/bingen.go

@@ -44,7 +44,7 @@ package opencost
 // @bingen:end
 
 // Allocation Version Set: Includes Allocation pipeline specific resources
-// @bingen:set[name=Allocation,version=24]
+// @bingen:set[name=Allocation,version=25]
 // @bingen:generate[migrate]:Allocation
 // @bingen:generate[stringtable]:AllocationSet
 // @bingen:generate:AllocationSetRange

+ 29 - 9
core/pkg/opencost/opencost_codecs.go

@@ -33,6 +33,12 @@ const (
 )
 
 const (
+	// CloudCostCodecVersion is used for any resources listed in the CloudCost version set
+	CloudCostCodecVersion uint8 = 3
+
+	// NetworkInsightCodecVersion is used for any resources listed in the NetworkInsight version set
+	NetworkInsightCodecVersion uint8 = 1
+
 	// DefaultCodecVersion is used for any resources listed in the Default version set
 	DefaultCodecVersion uint8 = 18
 
@@ -40,13 +46,7 @@ const (
 	AssetsCodecVersion uint8 = 21
 
 	// AllocationCodecVersion is used for any resources listed in the Allocation version set
-	AllocationCodecVersion uint8 = 24
-
-	// CloudCostCodecVersion is used for any resources listed in the CloudCost version set
-	CloudCostCodecVersion uint8 = 3
-
-	// NetworkInsightCodecVersion is used for any resources listed in the NetworkInsight version set
-	NetworkInsightCodecVersion uint8 = 1
+	AllocationCodecVersion uint8 = 25
 )
 
 //--------------------------------------------------------------------------
@@ -477,8 +477,10 @@ func (target *Allocation) MarshalBinaryWithContext(ctx *EncodingContext) (err er
 		// --- [end][write][struct](GPUAllocation) ---
 
 	}
-	buff.WriteFloat64(target.CPUCoreLimitAverage)  // write float64
-	buff.WriteFloat64(target.RAMBytesLimitAverage) // write float64
+	buff.WriteFloat64(target.CPUCoreLimitAverage)          // write float64
+	buff.WriteFloat64(target.RAMBytesLimitAverage)         // write float64
+	buff.WriteFloat64(target.NetworkNatGatewayEgressCost)  // write float64
+	buff.WriteFloat64(target.NetworkNatGatewayIngressCost) // write float64
 	return nil
 }
 
@@ -848,6 +850,24 @@ func (target *Allocation) UnmarshalBinaryWithContext(ctx *DecodingContext) (err
 		target.RAMBytesLimitAverage = float64(0) // default
 	}
 
+	// field version check
+	if uint8(25) <= version {
+		mmm := buff.ReadFloat64() // read float64
+		target.NetworkNatGatewayEgressCost = mmm
+
+	} else {
+		target.NetworkNatGatewayEgressCost = float64(0) // default
+	}
+
+	// field version check
+	if uint8(25) <= version {
+		nnn := buff.ReadFloat64() // read float64
+		target.NetworkNatGatewayIngressCost = nnn
+
+	} else {
+		target.NetworkNatGatewayIngressCost = float64(0) // default
+	}
+
 	// execute migration func if version delta detected
 	if version != AllocationCodecVersion {
 		migrateAllocation(target, version, AllocationCodecVersion)

+ 4 - 0
core/pkg/source/datasource.go

@@ -93,6 +93,8 @@ type MetricsQuerier interface {
 	QueryNetInternetGiB(start, end time.Time) *Future[NetInternetGiBResult]
 	QueryNetInternetPricePerGiB(start, end time.Time) *Future[NetInternetPricePerGiBResult]
 	QueryNetInternetServiceGiB(start, end time.Time) *Future[NetInternetServiceGiBResult]
+	QueryNetNatGatewayPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult]
+	QueryNetNatGatewayGiB(start, end time.Time) *Future[NetNatGatewayGiBResult]
 	QueryNetTransferBytes(start, end time.Time) *Future[NetTransferBytesResult]
 
 	// Network Ingress
@@ -100,6 +102,8 @@ type MetricsQuerier interface {
 	QueryNetRegionIngressGiB(start, end time.Time) *Future[NetRegionIngressGiBResult]
 	QueryNetInternetIngressGiB(start, end time.Time) *Future[NetInternetIngressGiBResult]
 	QueryNetInternetServiceIngressGiB(start, end time.Time) *Future[NetInternetServiceIngressGiBResult]
+	QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *Future[NetNatGatewayPricePerGiBResult]
+	QueryNetNatGatewayIngressGiB(start, end time.Time) *Future[NetNatGatewayIngressGiBResult]
 	QueryNetReceiveBytes(start, end time.Time) *Future[NetReceiveBytesResult]
 
 	// Annotations

+ 17 - 0
core/pkg/source/decoders.go

@@ -45,6 +45,7 @@ const (
 	InternetLabel        = "internet"
 	SameZoneLabel        = "same_zone"
 	SameRegionLabel      = "same_region"
+	NatGatewayLabel      = "nat_gateway"
 )
 
 const (
@@ -1115,10 +1116,14 @@ type NetInternetPricePerGiBResult = NetworkPricePerGiBResult
 
 type NetInternetServiceGiBResult = NetworkGiBResult
 
+type NetNatGatewayPricePerGiBResult = NetworkPricePerGiBResult
+type NetNatGatewayGiBResult = NetworkGiBResult
+
 type NetZoneIngressGiBResult = NetworkGiBResult
 type NetRegionIngressGiBResult = NetworkGiBResult
 type NetInternetIngressGiBResult = NetworkGiBResult
 type NetInternetServiceIngressGiBResult = NetworkGiBResult
+type NetNatGatewayIngressGiBResult = NetworkGiBResult
 
 func DecodeNetZoneGiBResult(result *QueryResult) *NetZoneGiBResult {
 	return DecodeNetworkGiBResult(result)
@@ -1148,6 +1153,14 @@ func DecodeNetInternetServiceGiBResult(result *QueryResult) *NetInternetServiceG
 	return DecodeNetworkGiBResult(result)
 }
 
+func DecodeNetNatGatewayPricePerGiBResult(result *QueryResult) *NetNatGatewayPricePerGiBResult {
+	return DecodeNetworkPricePerGiBResult(result)
+}
+
+func DecodeNetNatGatewayGiBResult(result *QueryResult) *NetNatGatewayGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
 func DecodeNetZoneIngressGiBResult(result *QueryResult) *NetZoneIngressGiBResult {
 	return DecodeNetworkGiBResult(result)
 }
@@ -1164,6 +1177,10 @@ func DecodeNetInternetServiceIngressGiBResult(result *QueryResult) *NetInternetS
 	return DecodeNetworkGiBResult(result)
 }
 
+func DecodeNetNatGatewayIngressGiBResult(result *QueryResult) *NetNatGatewayIngressGiBResult {
+	return DecodeNetworkGiBResult(result)
+}
+
 type NetReceiveBytesResult struct {
 	UID       string
 	Cluster   string

+ 94 - 0
modules/collector-source/pkg/collector/collector.go

@@ -69,11 +69,15 @@ func NewOpenCostMetricStore() metric.MetricStore {
 	memStore.Register(NewNetInternetGiBMetricCollector())
 	memStore.Register(NewNetInternetPricePerGiBMetricCollector())
 	memStore.Register(NewNetInternetServiceGiBMetricCollector())
+	memStore.Register(NewNetNatGatewayGiBMetricCollector())
+	memStore.Register(NewNetNatGatewayPricePerGiBMetricCollector())
 	memStore.Register(NewNetReceiveBytesMetricCollector())
 	memStore.Register(NewNetZoneIngressGiBMetricCollector())
 	memStore.Register(NewNetRegionIngressGiBMetricCollector())
 	memStore.Register(NewNetInternetIngressGiBMetricCollector())
 	memStore.Register(NewNetInternetServiceIngressGiBMetricCollector())
+	memStore.Register(NewNetNatGatewayIngressPricePerGiBMetricCollector())
+	memStore.Register(NewNetNatGatewayIngressGiBMetricCollector())
 	memStore.Register(NewNetTransferBytesMetricCollector())
 	memStore.Register(NewNamespaceUptimeMetricCollector())
 	memStore.Register(NewNamespaceLabelsMetricCollector())
@@ -1502,6 +1506,51 @@ func NewNetInternetServiceGiBMetricCollector() *metric.MetricCollector {
 	)
 }
 
+//	sum(
+//		increase(
+//			kubecost_pod_network_egress_bytes_total{
+//				nat_gateway="true",
+//				<some_custom_filter>
+//			}[1h]
+//		)
+//	) by (pod_name, namespace, uid, cluster_id) / 1024 / 1024 / 1024
+
+func NewNetNatGatewayGiBMetricCollector() *metric.MetricCollector {
+	return metric.NewMetricCollector(
+		metric.NetNatGatewayGiBID,
+		metric.KubecostPodNetworkEgressBytesTotal,
+		[]string{
+			source.NamespaceLabel,
+			source.PodNameLabel,
+			source.UIDLabel,
+		},
+		aggregator.Increase,
+		func(labels map[string]string) bool {
+			natLabel, labelExists := labels[source.NatGatewayLabel]
+
+			return labelExists && natLabel == "true"
+		},
+	)
+}
+
+// avg(
+//      avg_over_time(
+// 			kubecost_network_nat_gateway_egress_cost{
+// 				<some_custom_filter>
+//			}[1h]
+//		)
+// ) by (cluster_id)
+
+func NewNetNatGatewayPricePerGiBMetricCollector() *metric.MetricCollector {
+	return metric.NewMetricCollector(
+		metric.NetNatGatewayPricePerGiBID,
+		metric.KubecostNetworkNatGatewayEgressCost,
+		[]string{},
+		aggregator.AverageOverTime,
+		nil,
+	)
+}
+
 //	sum(
 //		increase(
 //			container_network_receive_bytes_total{
@@ -1636,6 +1685,51 @@ func NewNetInternetServiceIngressGiBMetricCollector() *metric.MetricCollector {
 	)
 }
 
+// avg(
+//      avg_over_time(
+// 			kubecost_network_nat_gateway_ingress_cost{
+// 				<some_custom_filter>
+//			}[1h]
+//		)
+// ) by (cluster_id)
+
+func NewNetNatGatewayIngressPricePerGiBMetricCollector() *metric.MetricCollector {
+	return metric.NewMetricCollector(
+		metric.NetNatGatewayIngressPricePerGiBID,
+		metric.KubecostNetworkNatGatewayIngressCost,
+		[]string{},
+		aggregator.AverageOverTime,
+		nil,
+	)
+}
+
+//	sum(
+//		increase(
+//			kubecost_pod_network_ingress_bytes_total{
+//				nat_gateway="true",
+//				<some_custom_filter>
+//			}[1h]
+//		)
+//	) by (pod_name, namespace, uid, cluster_id) / 1024 / 1024 / 1024
+
+func NewNetNatGatewayIngressGiBMetricCollector() *metric.MetricCollector {
+	return metric.NewMetricCollector(
+		metric.NetNatGatewayIngressGiBID,
+		metric.KubecostPodNetworkIngressBytesTotal,
+		[]string{
+			source.NamespaceLabel,
+			source.PodNameLabel,
+			source.UIDLabel,
+		},
+		aggregator.Increase,
+		func(labels map[string]string) bool {
+			natLabel, labelExists := labels[source.NatGatewayLabel]
+
+			return labelExists && natLabel == "true"
+		},
+	)
+}
+
 //	sum(
 //		increase(
 //			container_network_transmit_bytes_total{

+ 16 - 0
modules/collector-source/pkg/collector/metricsquerier.go

@@ -452,6 +452,14 @@ func (c *collectorMetricsQuerier) QueryNetInternetServiceGiB(start, end time.Tim
 	return queryCollectorGiB(c, start, end, metric.NetInternetServiceGiBID, source.DecodeNetInternetServiceGiBResult)
 }
 
+func (c *collectorMetricsQuerier) QueryNetNatGatewayPricePerGiB(start, end time.Time) *source.Future[source.NetNatGatewayPricePerGiBResult] {
+	return queryCollector(c, start, end, metric.NetNatGatewayPricePerGiBID, source.DecodeNetNatGatewayPricePerGiBResult)
+}
+
+func (c *collectorMetricsQuerier) QueryNetNatGatewayGiB(start, end time.Time) *source.Future[source.NetNatGatewayGiBResult] {
+	return queryCollectorGiB(c, start, end, metric.NetNatGatewayGiBID, source.DecodeNetNatGatewayGiBResult)
+}
+
 func (c *collectorMetricsQuerier) QueryNetTransferBytes(start, end time.Time) *source.Future[source.NetTransferBytesResult] {
 	return queryCollector(c, start, end, metric.NetTransferBytesID, source.DecodeNetTransferBytesResult)
 }
@@ -472,6 +480,14 @@ func (c *collectorMetricsQuerier) QueryNetInternetServiceIngressGiB(start, end t
 	return queryCollectorGiB(c, start, end, metric.NetInternetServiceIngressGiBID, source.DecodeNetInternetServiceIngressGiBResult)
 }
 
+func (c *collectorMetricsQuerier) QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *source.Future[source.NetNatGatewayPricePerGiBResult] {
+	return queryCollector(c, start, end, metric.NetNatGatewayPricePerGiBID, source.DecodeNetNatGatewayPricePerGiBResult)
+}
+
+func (c *collectorMetricsQuerier) QueryNetNatGatewayIngressGiB(start, end time.Time) *source.Future[source.NetNatGatewayIngressGiBResult] {
+	return queryCollectorGiB(c, start, end, metric.NetNatGatewayIngressGiBID, source.DecodeNetNatGatewayIngressGiBResult)
+}
+
 func (c *collectorMetricsQuerier) QueryNetReceiveBytes(start, end time.Time) *source.Future[source.NetReceiveBytesResult] {
 	return queryCollector(c, start, end, metric.NetReceiveBytesID, source.DecodeNetReceiveBytesResult)
 }

+ 4 - 0
modules/collector-source/pkg/metric/collector.go

@@ -72,11 +72,15 @@ const (
 	NetInternetGiBID                           MetricCollectorID = "NetInternetGiB"
 	NetInternetPricePerGiBID                   MetricCollectorID = "NetInternetPricePerGiB"
 	NetInternetServiceGiBID                    MetricCollectorID = "NetInternetServiceGiB"
+	NetNatGatewayPricePerGiBID                 MetricCollectorID = "NetNatGatewayPricePerGiB"
+	NetNatGatewayIngressPricePerGiBID          MetricCollectorID = "NetNatGatewayIngressPricePerGiB"
+	NetNatGatewayGiBID                         MetricCollectorID = "NetNatGatewayGiB"
 	NetTransferBytesID                         MetricCollectorID = "NetTransferBytes"
 	NetZoneIngressGiBID                        MetricCollectorID = "NetZoneIngressGiB"
 	NetRegionIngressGiBID                      MetricCollectorID = "NetRegionIngressGiB"
 	NetInternetIngressGiBID                    MetricCollectorID = "NetInternetIngressGiB"
 	NetInternetServiceIngressGiBID             MetricCollectorID = "NetInternetServiceIngressGiB"
+	NetNatGatewayIngressGiBID                  MetricCollectorID = "NetNatGatewayIngressGiB"
 	NetReceiveBytesID                          MetricCollectorID = "NetReceiveBytes"
 	NamespaceUptimeID                          MetricCollectorID = "NamespaceUptime"
 	NamespaceLabelsID                          MetricCollectorID = "NamespaceLabels"

+ 18 - 16
modules/collector-source/pkg/metric/metrics.go

@@ -40,22 +40,24 @@ const (
 	KubecostPodNetworkIngressBytesTotal = "kubecost_pod_network_ingress_bytes_total"
 
 	// Opencost Metrics
-	KubecostClusterManagementCost     = "kubecost_cluster_management_cost"
-	KubecostNetworkZoneEgressCost     = "kubecost_network_zone_egress_cost"
-	KubecostNetworkRegionEgressCost   = "kubecost_network_region_egress_cost"
-	KubecostNetworkInternetEgressCost = "kubecost_network_internet_egress_cost"
-	PVHourlyCost                      = "pv_hourly_cost"
-	KubecostLoadBalancerCost          = "kubecost_load_balancer_cost"
-	NodeTotalHourlyCost               = "node_total_hourly_cost"
-	NodeCPUHourlyCost                 = "node_cpu_hourly_cost"
-	NodeRAMHourlyCost                 = "node_ram_hourly_cost"
-	NodeGPUHourlyCost                 = "node_gpu_hourly_cost"
-	NodeGPUCount                      = "node_gpu_count"
-	KubecostNodeIsSpot                = "kubecost_node_is_spot"
-	ContainerCPUAllocation            = "container_cpu_allocation"
-	ContainerMemoryAllocationBytes    = "container_memory_allocation_bytes"
-	ContainerGPUAllocation            = "container_gpu_allocation"
-	PodPVCAllocation                  = "pod_pvc_allocation"
+	KubecostClusterManagementCost        = "kubecost_cluster_management_cost"
+	KubecostNetworkZoneEgressCost        = "kubecost_network_zone_egress_cost"
+	KubecostNetworkRegionEgressCost      = "kubecost_network_region_egress_cost"
+	KubecostNetworkInternetEgressCost    = "kubecost_network_internet_egress_cost"
+	KubecostNetworkNatGatewayEgressCost  = "kubecost_network_nat_gateway_egress_cost"
+	KubecostNetworkNatGatewayIngressCost = "kubecost_network_nat_gateway_ingress_cost"
+	PVHourlyCost                         = "pv_hourly_cost"
+	KubecostLoadBalancerCost             = "kubecost_load_balancer_cost"
+	NodeTotalHourlyCost                  = "node_total_hourly_cost"
+	NodeCPUHourlyCost                    = "node_cpu_hourly_cost"
+	NodeRAMHourlyCost                    = "node_ram_hourly_cost"
+	NodeGPUHourlyCost                    = "node_gpu_hourly_cost"
+	NodeGPUCount                         = "node_gpu_count"
+	KubecostNodeIsSpot                   = "kubecost_node_is_spot"
+	ContainerCPUAllocation               = "container_cpu_allocation"
+	ContainerMemoryAllocationBytes       = "container_memory_allocation_bytes"
+	ContainerGPUAllocation               = "container_gpu_allocation"
+	PodPVCAllocation                     = "pod_pvc_allocation"
 
 	// Stat Summary Metrics
 	NodeCPUSecondsTotal                = "node_cpu_seconds_total"

+ 74 - 0
modules/prometheus-source/pkg/prom/metricsquerier.go

@@ -1175,6 +1175,43 @@ func (pds *PrometheusMetricsQuerier) QueryNetInternetServiceGiB(start, end time.
 	return source.NewFuture(source.DecodeNetInternetServiceGiBResult, ctx.QueryAtTime(queryNetInternetGiB, end))
 }
 
+func (pds *PrometheusMetricsQuerier) QueryNetNatGatewayPricePerGiB(start, end time.Time) *source.Future[source.NetNatGatewayPricePerGiBResult] {
+	const queryName = "QueryNetNatGatewayPricePerGiB"
+	const queryFmtNetNatGatewayPricePerGiB = `avg(avg_over_time(kubecost_network_nat_gateway_egress_cost{%s}[%s])) by (%s)`
+
+	cfg := pds.promConfig
+
+	durStr := timeutil.DurationString(end.Sub(start))
+	if durStr == "" {
+		panic(fmt.Sprintf("failed to parse duration string passed to %s", queryName))
+	}
+
+	queryNetNatGatewayPricePerGiB := fmt.Sprintf(queryFmtNetNatGatewayPricePerGiB, cfg.ClusterFilter, durStr, cfg.ClusterLabel)
+	log.Debugf(PrometheusMetricsQueryLogFormat, queryName, end.Unix(), queryNetNatGatewayPricePerGiB)
+
+	ctx := pds.promContexts.NewNamedContext(AllocationContextName)
+	return source.NewFuture(source.DecodeNetNatGatewayPricePerGiBResult, ctx.QueryAtTime(queryNetNatGatewayPricePerGiB, end))
+}
+
+func (pds *PrometheusMetricsQuerier) QueryNetNatGatewayGiB(start, end time.Time) *source.Future[source.NetNatGatewayGiBResult] {
+	const queryName = "QueryNetNatGatewayGiB"
+	const queryFmtNetNatGatewayGiB = `sum(increase(kubecost_pod_network_egress_bytes_total{nat_gateway="true", %s}[%s:%dm])) by (pod_name, namespace, service, uid, %s) / 1024 / 1024 / 1024`
+
+	cfg := pds.promConfig
+	minsPerResolution := cfg.DataResolutionMinutes
+
+	durStr := pds.durationStringFor(start, end, minsPerResolution, true)
+	if durStr == "" {
+		panic(fmt.Sprintf("failed to parse duration string passed to %s", queryName))
+	}
+
+	queryNetNatGatewayGiB := fmt.Sprintf(queryFmtNetNatGatewayGiB, cfg.ClusterFilter, durStr, minsPerResolution, cfg.ClusterLabel)
+	log.Debugf(PrometheusMetricsQueryLogFormat, queryName, end.Unix(), queryNetNatGatewayGiB)
+
+	ctx := pds.promContexts.NewNamedContext(NetworkInsightsContextName)
+	return source.NewFuture(source.DecodeNetNatGatewayGiBResult, ctx.QueryAtTime(queryNetNatGatewayGiB, end))
+}
+
 func (pds *PrometheusMetricsQuerier) QueryNetTransferBytes(start, end time.Time) *source.Future[source.NetTransferBytesResult] {
 	const queryName = "QueryNetTransferBytes"
 	const queryFmtNetTransferBytes = `sum(increase(container_network_transmit_bytes_total{pod!="", %s}[%s:%dm])) by (pod_name, pod, namespace, uid, %s)`
@@ -1270,6 +1307,43 @@ func (pds *PrometheusMetricsQuerier) QueryNetInternetServiceIngressGiB(start, en
 	return source.NewFuture(source.DecodeNetInternetServiceIngressGiBResult, ctx.QueryAtTime(queryNetIngInternetGiB, end))
 }
 
+func (pds *PrometheusMetricsQuerier) QueryNetNatGatewayIngressPricePerGiB(start, end time.Time) *source.Future[source.NetNatGatewayPricePerGiBResult] {
+	const queryName = "QueryNetNatGatewayIngressPricePerGiB"
+	const queryFmtNetNatGatewayIngressPricePerGiB = `avg(avg_over_time(kubecost_network_nat_gateway_ingress_cost{%s}[%s])) by (%s)`
+
+	cfg := pds.promConfig
+
+	durStr := timeutil.DurationString(end.Sub(start))
+	if durStr == "" {
+		panic(fmt.Sprintf("failed to parse duration string passed to %s", queryName))
+	}
+
+	queryNetNatGatewayIngressPricePerGiB := fmt.Sprintf(queryFmtNetNatGatewayIngressPricePerGiB, cfg.ClusterFilter, durStr, cfg.ClusterLabel)
+	log.Debugf(PrometheusMetricsQueryLogFormat, queryName, end.Unix(), queryNetNatGatewayIngressPricePerGiB)
+
+	ctx := pds.promContexts.NewNamedContext(AllocationContextName)
+	return source.NewFuture(source.DecodeNetNatGatewayPricePerGiBResult, ctx.QueryAtTime(queryNetNatGatewayIngressPricePerGiB, end))
+}
+
+func (pds *PrometheusMetricsQuerier) QueryNetNatGatewayIngressGiB(start, end time.Time) *source.Future[source.NetNatGatewayIngressGiBResult] {
+	const queryName = "QueryNetNatGatewayIngressGiB"
+	const queryFmtNetNatGatewayIngressGiB = `sum(increase(kubecost_pod_network_ingress_bytes_total{nat_gateway="true", %s}[%s:%dm])) by (pod_name, namespace, service, uid, %s) / 1024 / 1024 / 1024`
+
+	cfg := pds.promConfig
+	minsPerResolution := cfg.DataResolutionMinutes
+
+	durStr := pds.durationStringFor(start, end, minsPerResolution, true)
+	if durStr == "" {
+		panic(fmt.Sprintf("failed to parse duration string passed to %s", queryName))
+	}
+
+	queryNetNatGatewayIngressGiB := fmt.Sprintf(queryFmtNetNatGatewayIngressGiB, cfg.ClusterFilter, durStr, minsPerResolution, cfg.ClusterLabel)
+	log.Debugf(PrometheusMetricsQueryLogFormat, queryName, end.Unix(), queryNetNatGatewayIngressGiB)
+
+	ctx := pds.promContexts.NewNamedContext(NetworkInsightsContextName)
+	return source.NewFuture(source.DecodeNetNatGatewayIngressGiBResult, ctx.QueryAtTime(queryNetNatGatewayIngressGiB, end))
+}
+
 func (pds *PrometheusMetricsQuerier) QueryNetReceiveBytes(start, end time.Time) *source.Future[source.NetReceiveBytesResult] {
 	const queryName = "QueryNetReceiveBytes"
 	const queryFmtNetReceiveBytes = `sum(increase(container_network_receive_bytes_total{pod!="", %s}[%s:%dm])) by (pod_name, pod, namespace, uid, %s)`

+ 10 - 0
pkg/cloud/alibaba/provider.go

@@ -570,11 +570,21 @@ func (alibaba *Alibaba) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 10 - 0
pkg/cloud/aws/provider.go

@@ -1276,11 +1276,21 @@ func (aws *AWS) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 10 - 0
pkg/cloud/azure/provider.go

@@ -1284,11 +1284,21 @@ func (az *Azure) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 14 - 4
pkg/cloud/digitalocean/provider.go

@@ -738,9 +738,11 @@ func (do *DOKS) LoadBalancerPricing() (*models.LoadBalancer, error) {
 func (do *DOKS) NetworkPricing() (*models.Network, error) {
 	// fallback
 	const (
-		defaultZoneEgress     = 0.00
-		defaultRegionEgress   = 0.00
-		defaultInternetEgress = 0.01
+		defaultZoneEgress        = 0.00
+		defaultRegionEgress      = 0.00
+		defaultInternetEgress    = 0.01
+		defaultNatGatewayEgress  = 0.045
+		defaultNatGatewayIngress = 0.045
 	)
 
 	log.Infof("NetworkPricing: retrieving custom pricing data")
@@ -753,12 +755,16 @@ func (do *DOKS) NetworkPricing() (*models.Network, error) {
 			ZoneNetworkEgressCost:     defaultZoneEgress,
 			RegionNetworkEgressCost:   defaultRegionEgress,
 			InternetNetworkEgressCost: defaultInternetEgress,
+			NatGatewayEgressCost:      defaultNatGatewayEgress,
+			NatGatewayIngressCost:     defaultNatGatewayIngress,
 		}, nil
 	}
 
 	znec := parseWithDefault(cpricing.ZoneNetworkEgress, defaultZoneEgress, "ZoneNetworkEgress")
 	rnec := parseWithDefault(cpricing.RegionNetworkEgress, defaultRegionEgress, "RegionNetworkEgress")
 	inec := parseWithDefault(cpricing.InternetNetworkEgress, defaultInternetEgress, "InternetNetworkEgress")
+	nge := parseWithDefault(cpricing.NatGatewayEgress, defaultNatGatewayEgress, "NatGatewayEgress")
+	ngi := parseWithDefault(cpricing.NatGatewayIngress, defaultNatGatewayIngress, "NatGatewayIngress")
 
 	log.Infof("NetworkPricing: using parsed values: zone=%.4f/GiB, region=%.4f/GiB, internet=%.4f/GIB", znec, rnec, inec)
 
@@ -766,6 +772,8 @@ func (do *DOKS) NetworkPricing() (*models.Network, error) {
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 
@@ -786,7 +794,9 @@ func isDefaultNetworkPricing(cp *models.CustomPricing) bool {
 	return cp != nil &&
 		cp.ZoneNetworkEgress == "0.01" &&
 		cp.RegionNetworkEgress == "0.01" &&
-		cp.InternetNetworkEgress == "0.12"
+		cp.InternetNetworkEgress == "0.12" &&
+		cp.NatGatewayEgress == "0.045" &&
+		cp.NatGatewayIngress == "0.045"
 }
 
 func (do *DOKS) AllNodePricing() (interface{}, error) {

+ 10 - 0
pkg/cloud/gcp/provider.go

@@ -1142,11 +1142,21 @@ func (gcp *GCP) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 2 - 0
pkg/cloud/gcp/provider_test.go

@@ -1261,6 +1261,8 @@ func (m *mockConfig) GetCustomPricingData() (*models.CustomPricing, error) {
 		ZoneNetworkEgress:     "0.12",
 		RegionNetworkEgress:   "0.08",
 		InternetNetworkEgress: "0.15",
+		NatGatewayEgress:      "0.45",
+		NatGatewayIngress:     "0.45",
 	}, nil
 }
 

+ 3 - 1
pkg/cloud/models/models.go

@@ -137,6 +137,8 @@ type CustomPricing struct {
 	ZoneNetworkEgress            string `json:"zoneNetworkEgress"`
 	RegionNetworkEgress          string `json:"regionNetworkEgress"`
 	InternetNetworkEgress        string `json:"internetNetworkEgress"`
+	NatGatewayEgress             string `json:"natGatewayEgress"`
+	NatGatewayIngress            string `json:"natGatewayIngress"`
 	FirstFiveForwardingRulesCost string `json:"firstFiveForwardingRulesCost"`
 	AdditionalForwardingRuleCost string `json:"additionalForwardingRuleCost"`
 	LBIngressDataCost            string `json:"LBIngressDataCost"`
@@ -215,7 +217,7 @@ func SetCustomPricingField(obj *CustomPricing, name string, value string) error
 	// validation work in order to prevent "NaN" and other invalid strings
 	// from getting set here.
 	switch strings.ToLower(name) {
-	case "cpu", "gpu", "ram", "spotcpu", "spotgpu", "spotram", "storage", "zonenetworkegress", "regionnetworkegress", "internetnetworkegress":
+	case "cpu", "gpu", "ram", "spotcpu", "spotgpu", "spotram", "storage", "zonenetworkegress", "regionnetworkegress", "internetnetworkegress", "natgatewayegress", "natgatewayingress":
 		// If we are sent an empty string, ignore the key and don't change the value
 		if value == "" {
 			return nil

+ 4 - 0
pkg/cloud/models/models_test.go

@@ -66,6 +66,8 @@ func TestSetSetCustomPricingField(t *testing.T) {
 		"ZoneNetworkEgress",
 		"RegionNetworkEgress",
 		"InternetNetworkEgress",
+		"NatGatewayEgress",
+		"NatGatewayIngress",
 	}
 
 	testCases := []testCase{}
@@ -98,6 +100,8 @@ func TestSetSetCustomPricingField(t *testing.T) {
 				ZoneNetworkEgress:     defaultValue,
 				RegionNetworkEgress:   defaultValue,
 				InternetNetworkEgress: defaultValue,
+				NatGatewayEgress:      defaultValue,
+				NatGatewayIngress:     defaultValue,
 			}
 			err := SetCustomPricingField(cp, tc.fieldName, tc.fieldValue)
 			if err != nil && tc.expError == nil {

+ 2 - 0
pkg/cloud/models/network.go

@@ -11,6 +11,8 @@ type Network struct {
 	ZoneNetworkEgressCost     float64
 	RegionNetworkEgressCost   float64
 	InternetNetworkEgressCost float64
+	NatGatewayEgressCost      float64
+	NatGatewayIngressCost     float64
 }
 
 // LoadBalancer is the interface by which the provider and cost model communicate LoadBalancer prices.

+ 2 - 0
pkg/cloud/oracle/ratecard.go

@@ -103,6 +103,8 @@ func (rcs *RateCardStore) ForEgressRegion(region string, defaultPricing DefaultP
 		ZoneNetworkEgressCost:     0,
 		RegionNetworkEgressCost:   egressCost,
 		InternetNetworkEgressCost: egressCost,
+		NatGatewayEgressCost:      0,
+		NatGatewayIngressCost:     0,
 	}, nil
 }
 

+ 10 - 0
pkg/cloud/otc/provider.go

@@ -243,11 +243,21 @@ func (otc *OTC) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 10 - 0
pkg/cloud/provider/customprovider.go

@@ -287,11 +287,21 @@ func (cp *CustomProvider) NetworkPricing() (*models.Network, error) {
 	if err != nil {
 		return nil, err
 	}
+	nge, err := strconv.ParseFloat(cpricing.NatGatewayEgress, 64)
+	if err != nil {
+		return nil, err
+	}
+	ngi, err := strconv.ParseFloat(cpricing.NatGatewayIngress, 64)
+	if err != nil {
+		return nil, err
+	}
 
 	return &models.Network{
 		ZoneNetworkEgressCost:     znec,
 		RegionNetworkEgressCost:   rnec,
 		InternetNetworkEgressCost: inec,
+		NatGatewayEgressCost:      nge,
+		NatGatewayIngressCost:     ngi,
 	}, nil
 }
 

+ 28 - 12
pkg/cloud/provider/providerconfig.go

@@ -26,7 +26,7 @@ const closedSourceConfigMount = "models/"
 // ProviderConfig is a utility class that provides a thread-safe configuration storage/cache for all Provider
 // implementations
 type ProviderConfig struct {
-	lock            *sync.Mutex
+	lock            sync.Mutex
 	configManager   *config.ConfigFileManager
 	configFile      *config.ConfigFile
 	customPricing   *models.CustomPricing
@@ -37,7 +37,6 @@ type ProviderConfig struct {
 func NewProviderConfig(configManager *config.ConfigFileManager, fileName string) *ProviderConfig {
 	configFile := configManager.ConfigFileAt(coreenv.GetPathFromConfig(fileName))
 	pc := &ProviderConfig{
-		lock:          new(sync.Mutex),
 		configManager: configManager,
 		configFile:    configFile,
 		customPricing: nil,
@@ -69,10 +68,7 @@ func (pc *ProviderConfig) onConfigFileUpdated(changeType config.ChangeType, data
 			customPricing = DefaultPricing()
 		}
 
-		pc.customPricing = customPricing
-		if pc.customPricing.SpotGPU == "" {
-			pc.customPricing.SpotGPU = DefaultPricing().SpotGPU // Migration for users without this value set by default.
-		}
+		pc.customPricing = updateDefaultsOnEmpty(customPricing)
 	}
 }
 
@@ -136,10 +132,7 @@ func (pc *ProviderConfig) loadConfig(writeIfNotExists bool) (*models.CustomPrici
 		return DefaultPricing(), err
 	}
 
-	pc.customPricing = &customPricing
-	if pc.customPricing.SpotGPU == "" {
-		pc.customPricing.SpotGPU = DefaultPricing().SpotGPU // Migration for users without this value set by default.
-	}
+	pc.customPricing = updateDefaultsOnEmpty(&customPricing)
 
 	return pc.customPricing, nil
 }
@@ -177,7 +170,7 @@ func (pc *ProviderConfig) Update(updateFunc func(*models.CustomPricing) error) (
 	}
 
 	// Cache Update (possible the ptr already references the cached value)
-	pc.customPricing = c
+	pc.customPricing = updateDefaultsOnEmpty(c)
 
 	cj, err := json.Marshal(c)
 	if err != nil {
@@ -247,10 +240,33 @@ func DefaultPricing() *models.CustomPricing {
 		ZoneNetworkEgress:     "0.01",
 		RegionNetworkEgress:   "0.01",
 		InternetNetworkEgress: "0.12",
+		NatGatewayEgress:      "0.045",
+		NatGatewayIngress:     "0.045",
 		CustomPricesEnabled:   "false",
 	}
 }
 
+// Helper to default fields that may be left unset or empty due to config age
+func updateDefaultsOnEmpty(pricing *models.CustomPricing) *models.CustomPricing {
+	if pricing == nil {
+		return pricing
+	}
+
+	defaultPricing := DefaultPricing()
+
+	if pricing.SpotGPU == "" {
+		pricing.SpotGPU = defaultPricing.SpotGPU // Migration for users without this value set by default.
+	}
+	if pricing.NatGatewayEgress == "" {
+		pricing.NatGatewayEgress = defaultPricing.NatGatewayEgress
+	}
+	if pricing.NatGatewayIngress == "" {
+		pricing.NatGatewayIngress = defaultPricing.NatGatewayIngress
+	}
+
+	return pricing
+}
+
 // Gives the config file name in a full qualified file name
 func filenameInConfigPath(fqfn string) string {
 	_, fileName := gopath.Split(fqfn)
@@ -277,7 +293,7 @@ func ReturnPricingFromConfigs(filename string) (*models.CustomPricing, error) {
 	if err != nil {
 		return &models.CustomPricing{}, fmt.Errorf("ReturnPricingFromConfigs: unable to open file %s with err: %v", providerConfigFile, err)
 	}
-	return defaultPricing, nil
+	return updateDefaultsOnEmpty(defaultPricing), nil
 }
 
 func ExtractConfigFromProviders(prov models.Provider) models.ProviderConfig {

+ 2 - 0
pkg/cloud/scaleway/provider.go

@@ -176,6 +176,8 @@ func (c *Scaleway) NetworkPricing() (*models.Network, error) {
 		ZoneNetworkEgressCost:     0,
 		RegionNetworkEgressCost:   0,
 		InternetNetworkEgressCost: 0,
+		NatGatewayEgressCost:      0,
+		NatGatewayIngressCost:     0,
 	}, nil
 }
 

+ 12 - 0
pkg/costmodel/allocation.go

@@ -324,6 +324,12 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	resChNetInternetGiB := source.WithGroup(grp, ds.QueryNetInternetGiB(start, end))
 	resChNetInternetPricePerGiB := source.WithGroup(grp, ds.QueryNetInternetPricePerGiB(start, end))
 
+	resChNetNatGatewayGiB := source.WithGroup(grp, ds.QueryNetNatGatewayGiB(start, end))
+	resChNetNatGatewayEgressPricePerGiB := source.WithGroup(grp, ds.QueryNetNatGatewayPricePerGiB(start, end))
+
+	resChNetNatGatewayIngressGiB := source.WithGroup(grp, ds.QueryNetNatGatewayIngressGiB(start, end))
+	resChNetNatGatewayIngressPricePerGiB := source.WithGroup(grp, ds.QueryNetNatGatewayIngressPricePerGiB(start, end))
+
 	var resChNodeLabels *source.QueryGroupFuture[source.NodeLabelsResult]
 	if env.IsAllocationNodeLabelsEnabled() {
 		resChNodeLabels = source.WithGroup(grp, ds.QueryNodeLabels(start, end))
@@ -389,6 +395,10 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	resNetRegionPricePerGiB, _ := resChNetRegionPricePerGiB.Await()
 	resNetInternetGiB, _ := resChNetInternetGiB.Await()
 	resNetInternetPricePerGiB, _ := resChNetInternetPricePerGiB.Await()
+	resNetNatGatewayGiB, _ := resChNetNatGatewayGiB.Await()
+	resNetNatGatewayEgressPricePerGiB, _ := resChNetNatGatewayEgressPricePerGiB.Await()
+	resNetNatGatewayIngressGiB, _ := resChNetNatGatewayIngressGiB.Await()
+	resNetNatGatewayIngressPricePerGiB, _ := resChNetNatGatewayIngressPricePerGiB.Await()
 
 	var resNodeLabels []*source.NodeLabelsResult
 	if env.IsAllocationNodeLabelsEnabled() {
@@ -439,6 +449,8 @@ func (cm *CostModel) computeAllocation(start, end time.Time) (*opencost.Allocati
 	applyNetworkAllocation(podMap, resNetZoneGiB, resNetZonePricePerGiB, podUIDKeyMap, applyCrossZoneNetworkAllocation)
 	applyNetworkAllocation(podMap, resNetRegionGiB, resNetRegionPricePerGiB, podUIDKeyMap, applyCrossRegionNetworkAllocation)
 	applyNetworkAllocation(podMap, resNetInternetGiB, resNetInternetPricePerGiB, podUIDKeyMap, applyInternetNetworkAllocation)
+	applyNetworkAllocation(podMap, resNetNatGatewayGiB, resNetNatGatewayEgressPricePerGiB, podUIDKeyMap, applyNatGatewayEgressAllocation)
+	applyNetworkAllocation(podMap, resNetNatGatewayIngressGiB, resNetNatGatewayIngressPricePerGiB, podUIDKeyMap, applyNatGatewayIngressAllocation)
 
 	// In the case that a two pods with the same name had different containers,
 	// we will double-count the containers. There is no way to associate each

+ 8 - 0
pkg/costmodel/allocation_helpers.go

@@ -990,6 +990,14 @@ func applyInternetNetworkAllocation(alloc *opencost.Allocation, networkSubCost f
 	alloc.NetworkInternetCost = networkSubCost
 }
 
+func applyNatGatewayEgressAllocation(alloc *opencost.Allocation, networkSubCost float64) {
+	alloc.NetworkNatGatewayEgressCost = networkSubCost
+}
+
+func applyNatGatewayIngressAllocation(alloc *opencost.Allocation, networkSubCost float64) {
+	alloc.NetworkNatGatewayIngressCost = networkSubCost
+}
+
 func applyNetworkAllocation(podMap map[podKey]*pod, resNetworkGiB []*source.NetworkGiBResult, resNetworkCostPerGiB []*source.NetworkPricePerGiBResult, podUIDKeyMap map[podKey][]podKey, applyCostFunc func(*opencost.Allocation, float64)) {
 	costPerGiBByCluster := map[string]float64{}
 

+ 9 - 5
pkg/costmodel/costmodel.go

@@ -188,7 +188,7 @@ func (cm *CostModel) ComputeCostData(start, end time.Time) (map[string]*CostData
 	}
 
 	// Get metrics data
-	resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, err := queryMetrics(mq, start, end)
+	resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, resNetNatGatewayRequests, resNetNatGatewayIngressRequests, err := queryMetrics(mq, start, end)
 	if err != nil {
 		log.Warnf("ComputeCostData: continuing despite metrics errors: %s", err)
 	}
@@ -218,7 +218,7 @@ func (cm *CostModel) ComputeCostData(start, end time.Time) (map[string]*CostData
 		}
 	}
 
-	networkUsageMap, err := GetNetworkUsageData(resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, clusterID)
+	networkUsageMap, err := GetNetworkUsageData(resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, resNetNatGatewayRequests, resNetNatGatewayIngressRequests, clusterID)
 	if err != nil {
 		log.Warnf("Unable to get Network Cost Data: %s", err.Error())
 		networkUsageMap = make(map[string]*NetworkUsageData)
@@ -563,7 +563,7 @@ func (cm *CostModel) ComputeCostData(start, end time.Time) (map[string]*CostData
 	return containerNameCost, err
 }
 
-func queryMetrics(mq source.MetricsQuerier, start, end time.Time) ([]*source.ContainerMetricResult, []*source.ContainerMetricResult, []*source.NetZoneGiBResult, []*source.NetRegionGiBResult, []*source.NetInternetGiBResult, error) {
+func queryMetrics(mq source.MetricsQuerier, start, end time.Time) ([]*source.ContainerMetricResult, []*source.ContainerMetricResult, []*source.NetZoneGiBResult, []*source.NetRegionGiBResult, []*source.NetInternetGiBResult, []*source.NetNatGatewayGiBResult, []*source.NetNatGatewayIngressGiBResult, error) {
 	grp := source.NewQueryGroup()
 
 	resChRAMUsage := source.WithGroup(grp, mq.QueryRAMUsageAvg(start, end))
@@ -571,6 +571,8 @@ func queryMetrics(mq source.MetricsQuerier, start, end time.Time) ([]*source.Con
 	resChNetZoneRequests := source.WithGroup(grp, mq.QueryNetZoneGiB(start, end))
 	resChNetRegionRequests := source.WithGroup(grp, mq.QueryNetRegionGiB(start, end))
 	resChNetInternetRequests := source.WithGroup(grp, mq.QueryNetInternetGiB(start, end))
+	resChNetNatGatewayEgressRequests := source.WithGroup(grp, mq.QueryNetNatGatewayGiB(start, end))
+	resChNetNatGatewayIngressRequests := source.WithGroup(grp, mq.QueryNetNatGatewayIngressGiB(start, end))
 
 	// Process metrics query results. Handle errors using ctx.Errors.
 	resRAMUsage, _ := resChRAMUsage.Await()
@@ -578,6 +580,8 @@ func queryMetrics(mq source.MetricsQuerier, start, end time.Time) ([]*source.Con
 	resNetZoneRequests, _ := resChNetZoneRequests.Await()
 	resNetRegionRequests, _ := resChNetRegionRequests.Await()
 	resNetInternetRequests, _ := resChNetInternetRequests.Await()
+	resNetNatGatewayEgressRequests, _ := resChNetNatGatewayEgressRequests.Await()
+	resNetNatGatewayIngressRequests, _ := resChNetNatGatewayIngressRequests.Await()
 
 	// NOTE: The way we currently handle errors and warnings only early returns if there is an error. Warnings
 	// NOTE: will not propagate unless coupled with errors.
@@ -595,10 +599,10 @@ func queryMetrics(mq source.MetricsQuerier, start, end time.Time) ([]*source.Con
 
 		// ErrorCollection is an collection of errors wrapped in a single error implementation
 		// We opt to not return an error for the sake of running as a pure exporter.
-		return resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, grp.Error()
+		return resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, resNetNatGatewayEgressRequests, resNetNatGatewayIngressRequests, grp.Error()
 	}
 
-	return resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, nil
+	return resRAMUsage, resCPUUsage, resNetZoneRequests, resNetRegionRequests, resNetInternetRequests, resNetNatGatewayEgressRequests, resNetNatGatewayIngressRequests, nil
 }
 
 func findUnmountedPVCostData(clusterMap clusters.ClusterMap, unmountedPVs map[string][]*PersistentVolumeClaimData, namespaceLabelsMapping map[string]map[string]string, namespaceAnnotationsMapping map[string]map[string]string) map[string]*CostData {

+ 75 - 51
pkg/costmodel/metrics.go

@@ -118,22 +118,24 @@ func toStringPtr(s string) *string { return &s }
 var metricsInit sync.Once
 
 var (
-	cpuGv                      *prometheus.GaugeVec
-	ramGv                      *prometheus.GaugeVec
-	gpuGv                      *prometheus.GaugeVec
-	gpuCountGv                 *prometheus.GaugeVec
-	pvGv                       *prometheus.GaugeVec
-	spotGv                     *prometheus.GaugeVec
-	totalGv                    *prometheus.GaugeVec
-	ramAllocGv                 *prometheus.GaugeVec
-	cpuAllocGv                 *prometheus.GaugeVec
-	gpuAllocGv                 *prometheus.GaugeVec
-	pvAllocGv                  *prometheus.GaugeVec
-	networkZoneEgressCostG     prometheus.Gauge
-	networkRegionEgressCostG   prometheus.Gauge
-	networkInternetEgressCostG prometheus.Gauge
-	clusterManagementCostGv    *prometheus.GaugeVec
-	lbCostGv                   *prometheus.GaugeVec
+	cpuGv                         *prometheus.GaugeVec
+	ramGv                         *prometheus.GaugeVec
+	gpuGv                         *prometheus.GaugeVec
+	gpuCountGv                    *prometheus.GaugeVec
+	pvGv                          *prometheus.GaugeVec
+	spotGv                        *prometheus.GaugeVec
+	totalGv                       *prometheus.GaugeVec
+	ramAllocGv                    *prometheus.GaugeVec
+	cpuAllocGv                    *prometheus.GaugeVec
+	gpuAllocGv                    *prometheus.GaugeVec
+	pvAllocGv                     *prometheus.GaugeVec
+	networkZoneEgressCostG        prometheus.Gauge
+	networkRegionEgressCostG      prometheus.Gauge
+	networkInternetEgressCostG    prometheus.Gauge
+	networkNatGatewayEgressCostG  prometheus.Gauge
+	networkNatGatewayIngressCostG prometheus.Gauge
+	clusterManagementCostGv       *prometheus.GaugeVec
+	lbCostGv                      *prometheus.GaugeVec
 )
 
 // initCostModelMetrics uses a sync.Once to ensure that these metrics are only created once
@@ -257,6 +259,22 @@ func initCostModelMetrics(clusterInfo clusters.ClusterInfoProvider, metricsConfi
 			toRegisterGauge = append(toRegisterGauge, networkInternetEgressCostG)
 		}
 
+		networkNatGatewayEgressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "kubecost_network_nat_gateway_egress_cost",
+			Help: "kubecost_network_nat_gateway_egress_cost Total cost per GB of nat gateway egress.",
+		})
+		if _, disabled := disabledMetrics["kubecost_network_nat_gateway_egress_cost"]; !disabled {
+			toRegisterGauge = append(toRegisterGauge, networkNatGatewayEgressCostG)
+		}
+
+		networkNatGatewayIngressCostG = prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "kubecost_network_nat_gateway_ingress_cost",
+			Help: "kubecost_network_nat_gateway_ingress_cost Total cost per GB of nat gateway ingress.",
+		})
+		if _, disabled := disabledMetrics["kubecost_network_nat_gateway_ingress_cost"]; !disabled {
+			toRegisterGauge = append(toRegisterGauge, networkNatGatewayIngressCostG)
+		}
+
 		clusterManagementCostGv = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 			Name: "kubecost_cluster_management_cost",
 			Help: "kubecost_cluster_management_cost Hourly cost paid as a cluster management fee.",
@@ -302,22 +320,24 @@ type CostModelMetricsEmitter struct {
 	Model            *CostModel
 
 	// Metrics
-	CPUPriceRecorder              *prometheus.GaugeVec
-	RAMPriceRecorder              *prometheus.GaugeVec
-	PersistentVolumePriceRecorder *prometheus.GaugeVec
-	GPUPriceRecorder              *prometheus.GaugeVec
-	GPUCountRecorder              *prometheus.GaugeVec
-	PVAllocationRecorder          *prometheus.GaugeVec
-	NodeSpotRecorder              *prometheus.GaugeVec
-	NodeTotalPriceRecorder        *prometheus.GaugeVec
-	RAMAllocationRecorder         *prometheus.GaugeVec
-	CPUAllocationRecorder         *prometheus.GaugeVec
-	GPUAllocationRecorder         *prometheus.GaugeVec
-	ClusterManagementCostRecorder *prometheus.GaugeVec
-	LBCostRecorder                *prometheus.GaugeVec
-	NetworkZoneEgressRecorder     prometheus.Gauge
-	NetworkRegionEgressRecorder   prometheus.Gauge
-	NetworkInternetEgressRecorder prometheus.Gauge
+	CPUPriceRecorder                 *prometheus.GaugeVec
+	RAMPriceRecorder                 *prometheus.GaugeVec
+	PersistentVolumePriceRecorder    *prometheus.GaugeVec
+	GPUPriceRecorder                 *prometheus.GaugeVec
+	GPUCountRecorder                 *prometheus.GaugeVec
+	PVAllocationRecorder             *prometheus.GaugeVec
+	NodeSpotRecorder                 *prometheus.GaugeVec
+	NodeTotalPriceRecorder           *prometheus.GaugeVec
+	RAMAllocationRecorder            *prometheus.GaugeVec
+	CPUAllocationRecorder            *prometheus.GaugeVec
+	GPUAllocationRecorder            *prometheus.GaugeVec
+	ClusterManagementCostRecorder    *prometheus.GaugeVec
+	LBCostRecorder                   *prometheus.GaugeVec
+	NetworkZoneEgressRecorder        prometheus.Gauge
+	NetworkRegionEgressRecorder      prometheus.Gauge
+	NetworkInternetEgressRecorder    prometheus.Gauge
+	NetworkNatGatewayEgressRecorder  prometheus.Gauge
+	NetworkNatGatewayIngressRecorder prometheus.Gauge
 
 	// Concurrent Flow Control - Manages the run state of the metric emitter
 	runState atomic.AtomicRunState
@@ -351,25 +371,27 @@ func NewCostModelMetricsEmitter(clusterCache clustercache.ClusterCache, provider
 	metrics.InitOpencostTelemetry(metricsConfig)
 
 	return &CostModelMetricsEmitter{
-		KubeClusterCache:              clusterCache,
-		CloudProvider:                 provider,
-		Model:                         model,
-		CPUPriceRecorder:              cpuGv,
-		RAMPriceRecorder:              ramGv,
-		GPUPriceRecorder:              gpuGv,
-		GPUCountRecorder:              gpuCountGv,
-		PersistentVolumePriceRecorder: pvGv,
-		NodeSpotRecorder:              spotGv,
-		NodeTotalPriceRecorder:        totalGv,
-		RAMAllocationRecorder:         ramAllocGv,
-		CPUAllocationRecorder:         cpuAllocGv,
-		GPUAllocationRecorder:         gpuAllocGv,
-		PVAllocationRecorder:          pvAllocGv,
-		NetworkZoneEgressRecorder:     networkZoneEgressCostG,
-		NetworkRegionEgressRecorder:   networkRegionEgressCostG,
-		NetworkInternetEgressRecorder: networkInternetEgressCostG,
-		ClusterManagementCostRecorder: clusterManagementCostGv,
-		LBCostRecorder:                lbCostGv,
+		KubeClusterCache:                 clusterCache,
+		CloudProvider:                    provider,
+		Model:                            model,
+		CPUPriceRecorder:                 cpuGv,
+		RAMPriceRecorder:                 ramGv,
+		GPUPriceRecorder:                 gpuGv,
+		GPUCountRecorder:                 gpuCountGv,
+		PersistentVolumePriceRecorder:    pvGv,
+		NodeSpotRecorder:                 spotGv,
+		NodeTotalPriceRecorder:           totalGv,
+		RAMAllocationRecorder:            ramAllocGv,
+		CPUAllocationRecorder:            cpuAllocGv,
+		GPUAllocationRecorder:            gpuAllocGv,
+		PVAllocationRecorder:             pvAllocGv,
+		NetworkZoneEgressRecorder:        networkZoneEgressCostG,
+		NetworkRegionEgressRecorder:      networkRegionEgressCostG,
+		NetworkInternetEgressRecorder:    networkInternetEgressCostG,
+		NetworkNatGatewayEgressRecorder:  networkNatGatewayEgressCostG,
+		NetworkNatGatewayIngressRecorder: networkNatGatewayIngressCostG,
+		ClusterManagementCostRecorder:    clusterManagementCostGv,
+		LBCostRecorder:                   lbCostGv,
 	}
 }
 
@@ -474,6 +496,8 @@ func (cmme *CostModelMetricsEmitter) Start() bool {
 				cmme.NetworkZoneEgressRecorder.Set(networkCosts.ZoneNetworkEgressCost)
 				cmme.NetworkRegionEgressRecorder.Set(networkCosts.RegionNetworkEgressCost)
 				cmme.NetworkInternetEgressRecorder.Set(networkCosts.InternetNetworkEgressCost)
+				cmme.NetworkNatGatewayEgressRecorder.Set(networkCosts.NatGatewayEgressCost)
+				cmme.NetworkNatGatewayIngressRecorder.Set(networkCosts.NatGatewayIngressCost)
 			}
 
 			end := time.Now()

+ 72 - 9
pkg/costmodel/networkcosts.go

@@ -8,14 +8,16 @@ import (
 	costAnalyzerCloud "github.com/opencost/opencost/pkg/cloud/models"
 )
 
-// NetworkUsageVNetworkUsageDataector contains the network usage values for egress network traffic
+// NetworkUsageData contains the network usage values for egress network traffic and nat gateway
 type NetworkUsageData struct {
-	ClusterID             string
-	PodName               string
-	Namespace             string
-	NetworkZoneEgress     []*util.Vector
-	NetworkRegionEgress   []*util.Vector
-	NetworkInternetEgress []*util.Vector
+	ClusterID                string
+	PodName                  string
+	Namespace                string
+	NetworkZoneEgress        []*util.Vector
+	NetworkRegionEgress      []*util.Vector
+	NetworkInternetEgress    []*util.Vector
+	NetworkNatGatewayEgress  []*util.Vector
+	NetworkNatGatewayIngress []*util.Vector
 }
 
 // NetworkUsageVector contains a network usage vector for egress network traffic
@@ -28,7 +30,14 @@ type NetworkUsageVector struct {
 
 // GetNetworkUsageData performs a join of the the results of zone, region, and internet usage queries to return a single
 // map containing network costs for each namespace+pod
-func GetNetworkUsageData(zr []*source.NetZoneGiBResult, rr []*source.NetRegionGiBResult, ir []*source.NetInternetGiBResult, defaultClusterID string) (map[string]*NetworkUsageData, error) {
+func GetNetworkUsageData(
+	zr []*source.NetZoneGiBResult,
+	rr []*source.NetRegionGiBResult,
+	ir []*source.NetInternetGiBResult,
+	nge []*source.NetNatGatewayGiBResult,
+	ngi []*source.NetNatGatewayIngressGiBResult,
+	defaultClusterID string,
+) (map[string]*NetworkUsageData, error) {
 	zoneNetworkMap, err := getNetworkUsage(zr, defaultClusterID)
 	if err != nil {
 		return nil, err
@@ -44,6 +53,16 @@ func GetNetworkUsageData(zr []*source.NetZoneGiBResult, rr []*source.NetRegionGi
 		return nil, err
 	}
 
+	natGatewayEgressNetMap, err := getNetworkUsage(nge, defaultClusterID)
+	if err != nil {
+		return nil, err
+	}
+
+	natGatewayIngressNetMap, err := getNetworkUsage(ngi, defaultClusterID)
+	if err != nil {
+		return nil, err
+	}
+
 	usageData := make(map[string]*NetworkUsageData)
 	for k, v := range zoneNetworkMap {
 		existing, ok := usageData[k]
@@ -90,6 +109,36 @@ func GetNetworkUsageData(zr []*source.NetZoneGiBResult, rr []*source.NetRegionGi
 		existing.NetworkInternetEgress = v.Values
 	}
 
+	for k, v := range natGatewayEgressNetMap {
+		existing, ok := usageData[k]
+		if !ok {
+			usageData[k] = &NetworkUsageData{
+				ClusterID:               v.ClusterID,
+				PodName:                 v.PodName,
+				Namespace:               v.Namespace,
+				NetworkNatGatewayEgress: v.Values,
+			}
+			continue
+		}
+
+		existing.NetworkNatGatewayEgress = v.Values
+	}
+
+	for k, v := range natGatewayIngressNetMap {
+		existing, ok := usageData[k]
+		if !ok {
+			usageData[k] = &NetworkUsageData{
+				ClusterID:                v.ClusterID,
+				PodName:                  v.PodName,
+				Namespace:                v.Namespace,
+				NetworkNatGatewayIngress: v.Values,
+			}
+			continue
+		}
+
+		existing.NetworkNatGatewayIngress = v.Values
+	}
+
 	return usageData, nil
 }
 
@@ -104,12 +153,16 @@ func GetNetworkCost(usage *NetworkUsageData, cloud costAnalyzerCloud.Provider) (
 	zoneCost := pricing.ZoneNetworkEgressCost
 	regionCost := pricing.RegionNetworkEgressCost
 	internetCost := pricing.InternetNetworkEgressCost
+	natGatewayEgressCost := pricing.NatGatewayEgressCost
+	natGatewayIngressCost := pricing.NatGatewayIngressCost
 
 	zlen := len(usage.NetworkZoneEgress)
 	rlen := len(usage.NetworkRegionEgress)
 	ilen := len(usage.NetworkInternetEgress)
+	ngelen := len(usage.NetworkNatGatewayEgress)
+	ngilen := len(usage.NetworkNatGatewayIngress)
 
-	l := max(zlen, rlen, ilen)
+	l := max(zlen, rlen, ilen, ngelen, ngilen)
 	for i := 0; i < l; i++ {
 		var cost float64 = 0
 		var timestamp float64
@@ -129,6 +182,16 @@ func GetNetworkCost(usage *NetworkUsageData, cloud costAnalyzerCloud.Provider) (
 			timestamp = usage.NetworkInternetEgress[i].Timestamp
 		}
 
+		if i < ngelen {
+			cost += usage.NetworkNatGatewayEgress[i].Value * natGatewayEgressCost
+			timestamp = usage.NetworkNatGatewayEgress[i].Timestamp
+		}
+
+		if i < ngilen {
+			cost += usage.NetworkNatGatewayIngress[i].Value * natGatewayIngressCost
+			timestamp = usage.NetworkNatGatewayIngress[i].Timestamp
+		}
+
 		results = append(results, &util.Vector{
 			Value:     cost,
 			Timestamp: timestamp,

+ 618 - 0
pkg/costmodel/networkcosts_test.go

@@ -0,0 +1,618 @@
+package costmodel
+
+import (
+	"io"
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/source"
+	"github.com/opencost/opencost/core/pkg/util"
+	"github.com/opencost/opencost/pkg/cloud/models"
+)
+
+// mockProvider is a mock implementation of the Provider interface for testing
+type mockProvider struct {
+	network *models.Network
+	err     error
+}
+
+func (m *mockProvider) NetworkPricing() (*models.Network, error) {
+	return m.network, m.err
+}
+
+func (m *mockProvider) GetKey(map[string]string, *clustercache.Node) models.Key {
+	return nil
+}
+
+func (m *mockProvider) PVPricing(models.PVKey) (*models.PV, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) NodePricing(models.Key) (*models.Node, models.PricingMetadata, error) {
+	return nil, models.PricingMetadata{}, nil
+}
+
+func (m *mockProvider) LoadBalancerPricing() (*models.LoadBalancer, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) AllNodePricing() (interface{}, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) ClusterInfo() (map[string]string, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GetAddresses() ([]byte, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GetDisks() ([]byte, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GetOrphanedResources() ([]models.OrphanedResource, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GpuPricing(map[string]string) (string, error) {
+	return "", nil
+}
+
+func (m *mockProvider) DownloadPricingData() error {
+	return nil
+}
+
+func (m *mockProvider) GetPVKey(*clustercache.PersistentVolume, map[string]string, string) models.PVKey {
+	return nil
+}
+
+func (m *mockProvider) UpdateConfig(io.Reader, string) (*models.CustomPricing, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) UpdateConfigFromConfigMap(map[string]string) (*models.CustomPricing, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GetConfig() (*models.CustomPricing, error) {
+	return nil, nil
+}
+
+func (m *mockProvider) GetManagementPlatform() (string, error) {
+	return "", nil
+}
+
+func (m *mockProvider) ApplyReservedInstancePricing(map[string]*models.Node) {
+}
+
+func (m *mockProvider) ServiceAccountStatus() *models.ServiceAccountStatus {
+	return nil
+}
+
+func (m *mockProvider) PricingSourceStatus() map[string]*models.PricingSource {
+	return nil
+}
+
+func (m *mockProvider) ClusterManagementPricing() (string, float64, error) {
+	return "", 0.0, nil
+}
+
+func (m *mockProvider) CombinedDiscountForNode(string, bool, float64, float64) float64 {
+	return 0.0
+}
+
+func (m *mockProvider) Regions() []string {
+	return nil
+}
+
+func (m *mockProvider) PricingSourceSummary() interface{} {
+	return nil
+}
+
+// TestGetNetworkUsageData tests the aggregation of NAT Gateway egress and ingress data
+func TestGetNetworkUsageData(t *testing.T) {
+	defaultClusterID := "default-cluster"
+
+	testCases := []struct {
+		name                     string
+		zoneResults              []*source.NetZoneGiBResult
+		regionResults            []*source.NetRegionGiBResult
+		internetResults          []*source.NetInternetGiBResult
+		natGatewayEgressResults  []*source.NetNatGatewayGiBResult
+		natGatewayIngressResults []*source.NetNatGatewayIngressGiBResult
+		expectedKeys             []string
+		validateFunc             func(t *testing.T, result map[string]*NetworkUsageData)
+	}{
+		{
+			name:            "NAT Gateway egress only",
+			zoneResults:     nil,
+			regionResults:   nil,
+			internetResults: nil,
+			natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
+				{
+					Pod:       "pod1",
+					Namespace: "ns1",
+					Cluster:   "cluster1",
+					Data: []*util.Vector{
+						{Value: 10.5, Timestamp: 1000},
+						{Value: 20.3, Timestamp: 2000},
+					},
+				},
+			},
+			natGatewayIngressResults: nil,
+			expectedKeys:             []string{"ns1,pod1,cluster1"},
+			validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
+				key := "ns1,pod1,cluster1"
+				if data, ok := result[key]; ok {
+					if len(data.NetworkNatGatewayEgress) != 2 {
+						t.Errorf("expected 2 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
+					}
+					if data.NetworkNatGatewayEgress[0].Value != 10.5 {
+						t.Errorf("expected first egress value 10.5, got %f", data.NetworkNatGatewayEgress[0].Value)
+					}
+					if len(data.NetworkNatGatewayIngress) != 0 {
+						t.Errorf("expected 0 NAT Gateway ingress vectors, got %d", len(data.NetworkNatGatewayIngress))
+					}
+				} else {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			},
+		},
+		{
+			name:                    "NAT Gateway ingress only",
+			zoneResults:             nil,
+			regionResults:           nil,
+			internetResults:         nil,
+			natGatewayEgressResults: nil,
+			natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
+				{
+					Pod:       "pod2",
+					Namespace: "ns2",
+					Cluster:   "cluster2",
+					Data: []*util.Vector{
+						{Value: 5.2, Timestamp: 1000},
+					},
+				},
+			},
+			expectedKeys: []string{"ns2,pod2,cluster2"},
+			validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
+				key := "ns2,pod2,cluster2"
+				if data, ok := result[key]; ok {
+					if len(data.NetworkNatGatewayIngress) != 1 {
+						t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
+					}
+					if data.NetworkNatGatewayIngress[0].Value != 5.2 {
+						t.Errorf("expected ingress value 5.2, got %f", data.NetworkNatGatewayIngress[0].Value)
+					}
+					if len(data.NetworkNatGatewayEgress) != 0 {
+						t.Errorf("expected 0 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
+					}
+				} else {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			},
+		},
+		{
+			name:            "NAT Gateway egress and ingress for same pod",
+			zoneResults:     nil,
+			regionResults:   nil,
+			internetResults: nil,
+			natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
+				{
+					Pod:       "pod3",
+					Namespace: "ns3",
+					Cluster:   "cluster3",
+					Data: []*util.Vector{
+						{Value: 15.0, Timestamp: 1000},
+					},
+				},
+			},
+			natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
+				{
+					Pod:       "pod3",
+					Namespace: "ns3",
+					Cluster:   "cluster3",
+					Data: []*util.Vector{
+						{Value: 8.5, Timestamp: 1000},
+					},
+				},
+			},
+			expectedKeys: []string{"ns3,pod3,cluster3"},
+			validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
+				key := "ns3,pod3,cluster3"
+				if data, ok := result[key]; ok {
+					if len(data.NetworkNatGatewayEgress) != 1 {
+						t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
+					}
+					if data.NetworkNatGatewayEgress[0].Value != 15.0 {
+						t.Errorf("expected egress value 15.0, got %f", data.NetworkNatGatewayEgress[0].Value)
+					}
+					if len(data.NetworkNatGatewayIngress) != 1 {
+						t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
+					}
+					if data.NetworkNatGatewayIngress[0].Value != 8.5 {
+						t.Errorf("expected ingress value 8.5, got %f", data.NetworkNatGatewayIngress[0].Value)
+					}
+				} else {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			},
+		},
+		{
+			name: "Mixed network traffic with NAT Gateway",
+			zoneResults: []*source.NetZoneGiBResult{
+				{
+					Pod:       "pod4",
+					Namespace: "ns4",
+					Cluster:   "cluster4",
+					Data: []*util.Vector{
+						{Value: 3.0, Timestamp: 1000},
+					},
+				},
+			},
+			regionResults: []*source.NetRegionGiBResult{
+				{
+					Pod:       "pod4",
+					Namespace: "ns4",
+					Cluster:   "cluster4",
+					Data: []*util.Vector{
+						{Value: 7.0, Timestamp: 1000},
+					},
+				},
+			},
+			internetResults: []*source.NetInternetGiBResult{
+				{
+					Pod:       "pod4",
+					Namespace: "ns4",
+					Cluster:   "cluster4",
+					Data: []*util.Vector{
+						{Value: 12.0, Timestamp: 1000},
+					},
+				},
+			},
+			natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
+				{
+					Pod:       "pod4",
+					Namespace: "ns4",
+					Cluster:   "cluster4",
+					Data: []*util.Vector{
+						{Value: 18.0, Timestamp: 1000},
+					},
+				},
+			},
+			natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
+				{
+					Pod:       "pod4",
+					Namespace: "ns4",
+					Cluster:   "cluster4",
+					Data: []*util.Vector{
+						{Value: 9.0, Timestamp: 1000},
+					},
+				},
+			},
+			expectedKeys: []string{"ns4,pod4,cluster4"},
+			validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
+				key := "ns4,pod4,cluster4"
+				if data, ok := result[key]; ok {
+					// Verify all network types are present
+					if len(data.NetworkZoneEgress) != 1 {
+						t.Errorf("expected 1 zone egress vector, got %d", len(data.NetworkZoneEgress))
+					}
+					if len(data.NetworkRegionEgress) != 1 {
+						t.Errorf("expected 1 region egress vector, got %d", len(data.NetworkRegionEgress))
+					}
+					if len(data.NetworkInternetEgress) != 1 {
+						t.Errorf("expected 1 internet egress vector, got %d", len(data.NetworkInternetEgress))
+					}
+					if len(data.NetworkNatGatewayEgress) != 1 {
+						t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
+					}
+					if len(data.NetworkNatGatewayIngress) != 1 {
+						t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
+					}
+
+					// Verify values
+					if data.NetworkNatGatewayEgress[0].Value != 18.0 {
+						t.Errorf("expected NAT Gateway egress 18.0, got %f", data.NetworkNatGatewayEgress[0].Value)
+					}
+					if data.NetworkNatGatewayIngress[0].Value != 9.0 {
+						t.Errorf("expected NAT Gateway ingress 9.0, got %f", data.NetworkNatGatewayIngress[0].Value)
+					}
+				} else {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			},
+		},
+		{
+			name:            "Default cluster ID fallback for NAT Gateway",
+			zoneResults:     nil,
+			regionResults:   nil,
+			internetResults: nil,
+			natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
+				{
+					Pod:       "pod5",
+					Namespace: "ns5",
+					Cluster:   "", // Empty cluster ID should use default
+					Data: []*util.Vector{
+						{Value: 5.0, Timestamp: 1000},
+					},
+				},
+			},
+			natGatewayIngressResults: nil,
+			expectedKeys:             []string{"ns5,pod5,default-cluster"},
+			validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
+				key := "ns5,pod5,default-cluster"
+				if data, ok := result[key]; ok {
+					if data.ClusterID != "default-cluster" {
+						t.Errorf("expected cluster ID 'default-cluster', got %s", data.ClusterID)
+					}
+				} else {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result, err := GetNetworkUsageData(
+				tc.zoneResults,
+				tc.regionResults,
+				tc.internetResults,
+				tc.natGatewayEgressResults,
+				tc.natGatewayIngressResults,
+				defaultClusterID,
+			)
+
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+
+			if len(result) != len(tc.expectedKeys) {
+				t.Errorf("expected %d keys, got %d", len(tc.expectedKeys), len(result))
+			}
+
+			for _, key := range tc.expectedKeys {
+				if _, ok := result[key]; !ok {
+					t.Errorf("expected key %s not found in result", key)
+				}
+			}
+
+			if tc.validateFunc != nil {
+				tc.validateFunc(t, result)
+			}
+		})
+	}
+}
+
+// TestGetNetworkCost tests the calculation of NAT Gateway costs
+func TestGetNetworkCost(t *testing.T) {
+	testCases := []struct {
+		name           string
+		usage          *NetworkUsageData
+		pricing        *models.Network
+		expectedCost   float64
+		expectedLength int
+	}{
+		{
+			name: "NAT Gateway egress cost only",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkNatGatewayEgress: []*util.Vector{
+					{Value: 10.0, Timestamp: 1000}, // 10 GiB
+				},
+			},
+			pricing: &models.Network{
+				NatGatewayEgressCost: 0.05, // $0.05 per GiB
+			},
+			expectedCost:   0.50, // 10 * 0.05 = 0.50
+			expectedLength: 1,
+		},
+		{
+			name: "NAT Gateway ingress cost only",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkNatGatewayIngress: []*util.Vector{
+					{Value: 20.0, Timestamp: 1000}, // 20 GiB
+				},
+			},
+			pricing: &models.Network{
+				NatGatewayIngressCost: 0.02, // $0.02 per GiB
+			},
+			expectedCost:   0.40, // 20 * 0.02 = 0.40
+			expectedLength: 1,
+		},
+		{
+			name: "NAT Gateway egress and ingress costs",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkNatGatewayEgress: []*util.Vector{
+					{Value: 10.0, Timestamp: 1000},
+				},
+				NetworkNatGatewayIngress: []*util.Vector{
+					{Value: 5.0, Timestamp: 1000},
+				},
+			},
+			pricing: &models.Network{
+				NatGatewayEgressCost:  0.05, // $0.05 per GiB
+				NatGatewayIngressCost: 0.02, // $0.02 per GiB
+			},
+			expectedCost:   0.60, // (10 * 0.05) + (5 * 0.02) = 0.50 + 0.10 = 0.60
+			expectedLength: 1,
+		},
+		{
+			name: "Mixed network costs with NAT Gateway",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkZoneEgress: []*util.Vector{
+					{Value: 5.0, Timestamp: 1000},
+				},
+				NetworkRegionEgress: []*util.Vector{
+					{Value: 8.0, Timestamp: 1000},
+				},
+				NetworkInternetEgress: []*util.Vector{
+					{Value: 12.0, Timestamp: 1000},
+				},
+				NetworkNatGatewayEgress: []*util.Vector{
+					{Value: 15.0, Timestamp: 1000},
+				},
+				NetworkNatGatewayIngress: []*util.Vector{
+					{Value: 10.0, Timestamp: 1000},
+				},
+			},
+			pricing: &models.Network{
+				ZoneNetworkEgressCost:     0.01,
+				RegionNetworkEgressCost:   0.02,
+				InternetNetworkEgressCost: 0.09,
+				NatGatewayEgressCost:      0.05,
+				NatGatewayIngressCost:     0.02,
+			},
+			expectedCost:   2.24, // (5*0.01) + (8*0.02) + (12*0.09) + (15*0.05) + (10*0.02) = 0.05 + 0.16 + 1.08 + 0.75 + 0.20 = 2.24
+			expectedLength: 1,
+		},
+		{
+			name: "Multiple time points with NAT Gateway",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkNatGatewayEgress: []*util.Vector{
+					{Value: 10.0, Timestamp: 1000},
+					{Value: 15.0, Timestamp: 2000},
+					{Value: 20.0, Timestamp: 3000},
+				},
+				NetworkNatGatewayIngress: []*util.Vector{
+					{Value: 5.0, Timestamp: 1000},
+					{Value: 8.0, Timestamp: 2000},
+					{Value: 12.0, Timestamp: 3000},
+				},
+			},
+			pricing: &models.Network{
+				NatGatewayEgressCost:  0.05,
+				NatGatewayIngressCost: 0.02,
+			},
+			expectedLength: 3,
+		},
+		{
+			name: "Zero NAT Gateway costs",
+			usage: &NetworkUsageData{
+				ClusterID: "cluster1",
+				PodName:   "pod1",
+				Namespace: "ns1",
+				NetworkNatGatewayEgress: []*util.Vector{
+					{Value: 100.0, Timestamp: 1000},
+				},
+				NetworkNatGatewayIngress: []*util.Vector{
+					{Value: 50.0, Timestamp: 1000},
+				},
+			},
+			pricing: &models.Network{
+				NatGatewayEgressCost:  0.0,
+				NatGatewayIngressCost: 0.0,
+			},
+			expectedCost:   0.0,
+			expectedLength: 1,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			provider := &mockProvider{
+				network: tc.pricing,
+			}
+
+			result, err := GetNetworkCost(tc.usage, provider)
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+
+			if len(result) != tc.expectedLength {
+				t.Errorf("expected %d result vectors, got %d", tc.expectedLength, len(result))
+			}
+
+			if tc.expectedLength > 0 {
+				totalCost := 0.0
+				for _, v := range result {
+					totalCost += v.Value
+				}
+
+				if tc.expectedCost > 0 {
+					if diff := totalCost - tc.expectedCost; diff > 0.001 || diff < -0.001 {
+						t.Errorf("expected total cost %f, got %f", tc.expectedCost, totalCost)
+					}
+				}
+			}
+		})
+	}
+}
+
+// TestGetNetworkCost_NATGatewayMisalignedVectors tests NAT Gateway cost calculation with different vector lengths
+func TestGetNetworkCost_NATGatewayMisalignedVectors(t *testing.T) {
+	usage := &NetworkUsageData{
+		ClusterID: "cluster1",
+		PodName:   "pod1",
+		Namespace: "ns1",
+		NetworkZoneEgress: []*util.Vector{
+			{Value: 5.0, Timestamp: 1000},
+			{Value: 6.0, Timestamp: 2000},
+		},
+		NetworkNatGatewayEgress: []*util.Vector{
+			{Value: 10.0, Timestamp: 1000},
+			{Value: 15.0, Timestamp: 2000},
+			{Value: 20.0, Timestamp: 3000}, // Extra NAT Gateway data point
+		},
+		NetworkNatGatewayIngress: []*util.Vector{
+			{Value: 5.0, Timestamp: 1000}, // Only one ingress data point
+		},
+	}
+
+	pricing := &models.Network{
+		ZoneNetworkEgressCost: 0.01,
+		NatGatewayEgressCost:  0.05,
+		NatGatewayIngressCost: 0.02,
+	}
+
+	provider := &mockProvider{
+		network: pricing,
+	}
+
+	result, err := GetNetworkCost(usage, provider)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	// Should have 3 result vectors (max of all vector lengths including NAT Gateway)
+	if len(result) != 3 {
+		t.Errorf("expected 3 result vectors (max of all vectors including NAT Gateway), got %d", len(result))
+	}
+
+	// First vector: zone (5*0.01) + natEgress (10*0.05) + natIngress (5*0.02) = 0.05 + 0.50 + 0.10 = 0.65
+	expectedFirst := (5.0 * 0.01) + (10.0 * 0.05) + (5.0 * 0.02)
+	if diff := result[0].Value - expectedFirst; diff > 0.001 || diff < -0.001 {
+		t.Errorf("expected first vector cost %f, got %f", expectedFirst, result[0].Value)
+	}
+
+	// Second vector: zone (6*0.01) + natEgress (15*0.05) = 0.06 + 0.75 = 0.81
+	// (no NAT ingress for second time point)
+	expectedSecond := (6.0 * 0.01) + (15.0 * 0.05)
+	if diff := result[1].Value - expectedSecond; diff > 0.001 || diff < -0.001 {
+		t.Errorf("expected second vector cost %f, got %f", expectedSecond, result[1].Value)
+	}
+
+	// Third vector: only natEgress (20*0.05) = 1.00
+	// (no zone, region, internet, or NAT ingress for third time point)
+	expectedThird := 20.0 * 0.05
+	if diff := result[2].Value - expectedThird; diff > 0.001 || diff < -0.001 {
+		t.Errorf("expected third vector cost %f, got %f", expectedThird, result[2].Value)
+	}
+}