Configure GCP Cloud Router NAT

The gcp:compute/routerNat:RouterNat resource, part of the Pulumi GCP provider, configures Cloud NAT on a Cloud Router to enable outbound connectivity and address translation for private instances. This guide focuses on three capabilities: automatic IP allocation for outbound internet access, manual IP allocation with traffic steering rules, and private NAT for inter-VPC communication.

Cloud NAT configurations attach to existing Cloud Routers and reference VPC networks, subnets, and optionally static IP addresses or Network Connectivity resources. The examples are intentionally small. Combine them with your own network infrastructure and routing policies.

Enable outbound internet access with automatic IP allocation

Most VPC deployments need outbound internet connectivity for private instances to reach external APIs, download packages, or access cloud services without assigning public IPs to individual VMs.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const net = new gcp.compute.Network("net", {name: "my-network"});
const subnet = new gcp.compute.Subnetwork("subnet", {
    name: "my-subnetwork",
    network: net.id,
    ipCidrRange: "10.0.0.0/16",
    region: "us-central1",
});
const router = new gcp.compute.Router("router", {
    name: "my-router",
    region: subnet.region,
    network: net.id,
    bgp: {
        asn: 64514,
    },
});
const nat = new gcp.compute.RouterNat("nat", {
    name: "my-router-nat",
    router: router.name,
    region: router.region,
    natIpAllocateOption: "AUTO_ONLY",
    sourceSubnetworkIpRangesToNat: "ALL_SUBNETWORKS_ALL_IP_RANGES",
    logConfig: {
        enable: true,
        filter: "ERRORS_ONLY",
    },
});
import pulumi
import pulumi_gcp as gcp

net = gcp.compute.Network("net", name="my-network")
subnet = gcp.compute.Subnetwork("subnet",
    name="my-subnetwork",
    network=net.id,
    ip_cidr_range="10.0.0.0/16",
    region="us-central1")
router = gcp.compute.Router("router",
    name="my-router",
    region=subnet.region,
    network=net.id,
    bgp={
        "asn": 64514,
    })
nat = gcp.compute.RouterNat("nat",
    name="my-router-nat",
    router=router.name,
    region=router.region,
    nat_ip_allocate_option="AUTO_ONLY",
    source_subnetwork_ip_ranges_to_nat="ALL_SUBNETWORKS_ALL_IP_RANGES",
    log_config={
        "enable": True,
        "filter": "ERRORS_ONLY",
    })
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		net, err := compute.NewNetwork(ctx, "net", &compute.NetworkArgs{
			Name: pulumi.String("my-network"),
		})
		if err != nil {
			return err
		}
		subnet, err := compute.NewSubnetwork(ctx, "subnet", &compute.SubnetworkArgs{
			Name:        pulumi.String("my-subnetwork"),
			Network:     net.ID(),
			IpCidrRange: pulumi.String("10.0.0.0/16"),
			Region:      pulumi.String("us-central1"),
		})
		if err != nil {
			return err
		}
		router, err := compute.NewRouter(ctx, "router", &compute.RouterArgs{
			Name:    pulumi.String("my-router"),
			Region:  subnet.Region,
			Network: net.ID(),
			Bgp: &compute.RouterBgpArgs{
				Asn: pulumi.Int(64514),
			},
		})
		if err != nil {
			return err
		}
		_, err = compute.NewRouterNat(ctx, "nat", &compute.RouterNatArgs{
			Name:                          pulumi.String("my-router-nat"),
			Router:                        router.Name,
			Region:                        router.Region,
			NatIpAllocateOption:           pulumi.String("AUTO_ONLY"),
			SourceSubnetworkIpRangesToNat: pulumi.String("ALL_SUBNETWORKS_ALL_IP_RANGES"),
			LogConfig: &compute.RouterNatLogConfigArgs{
				Enable: pulumi.Bool(true),
				Filter: pulumi.String("ERRORS_ONLY"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var net = new Gcp.Compute.Network("net", new()
    {
        Name = "my-network",
    });

    var subnet = new Gcp.Compute.Subnetwork("subnet", new()
    {
        Name = "my-subnetwork",
        Network = net.Id,
        IpCidrRange = "10.0.0.0/16",
        Region = "us-central1",
    });

    var router = new Gcp.Compute.Router("router", new()
    {
        Name = "my-router",
        Region = subnet.Region,
        Network = net.Id,
        Bgp = new Gcp.Compute.Inputs.RouterBgpArgs
        {
            Asn = 64514,
        },
    });

    var nat = new Gcp.Compute.RouterNat("nat", new()
    {
        Name = "my-router-nat",
        Router = router.Name,
        Region = router.Region,
        NatIpAllocateOption = "AUTO_ONLY",
        SourceSubnetworkIpRangesToNat = "ALL_SUBNETWORKS_ALL_IP_RANGES",
        LogConfig = new Gcp.Compute.Inputs.RouterNatLogConfigArgs
        {
            Enable = true,
            Filter = "ERRORS_ONLY",
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Network;
import com.pulumi.gcp.compute.NetworkArgs;
import com.pulumi.gcp.compute.Subnetwork;
import com.pulumi.gcp.compute.SubnetworkArgs;
import com.pulumi.gcp.compute.Router;
import com.pulumi.gcp.compute.RouterArgs;
import com.pulumi.gcp.compute.inputs.RouterBgpArgs;
import com.pulumi.gcp.compute.RouterNat;
import com.pulumi.gcp.compute.RouterNatArgs;
import com.pulumi.gcp.compute.inputs.RouterNatLogConfigArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var net = new Network("net", NetworkArgs.builder()
            .name("my-network")
            .build());

        var subnet = new Subnetwork("subnet", SubnetworkArgs.builder()
            .name("my-subnetwork")
            .network(net.id())
            .ipCidrRange("10.0.0.0/16")
            .region("us-central1")
            .build());

        var router = new Router("router", RouterArgs.builder()
            .name("my-router")
            .region(subnet.region())
            .network(net.id())
            .bgp(RouterBgpArgs.builder()
                .asn(64514)
                .build())
            .build());

        var nat = new RouterNat("nat", RouterNatArgs.builder()
            .name("my-router-nat")
            .router(router.name())
            .region(router.region())
            .natIpAllocateOption("AUTO_ONLY")
            .sourceSubnetworkIpRangesToNat("ALL_SUBNETWORKS_ALL_IP_RANGES")
            .logConfig(RouterNatLogConfigArgs.builder()
                .enable(true)
                .filter("ERRORS_ONLY")
                .build())
            .build());

    }
}
resources:
  net:
    type: gcp:compute:Network
    properties:
      name: my-network
  subnet:
    type: gcp:compute:Subnetwork
    properties:
      name: my-subnetwork
      network: ${net.id}
      ipCidrRange: 10.0.0.0/16
      region: us-central1
  router:
    type: gcp:compute:Router
    properties:
      name: my-router
      region: ${subnet.region}
      network: ${net.id}
      bgp:
        asn: 64514
  nat:
    type: gcp:compute:RouterNat
    properties:
      name: my-router-nat
      router: ${router.name}
      region: ${router.region}
      natIpAllocateOption: AUTO_ONLY
      sourceSubnetworkIpRangesToNat: ALL_SUBNETWORKS_ALL_IP_RANGES
      logConfig:
        enable: true
        filter: ERRORS_ONLY

When natIpAllocateOption is set to AUTO_ONLY, Google Cloud automatically allocates and manages external IPs for NAT. The sourceSubnetworkIpRangesToNat property controls which subnets can use NAT; ALL_SUBNETWORKS_ALL_IP_RANGES allows all instances in all subnets to initiate outbound connections. The logConfig block enables connection logging, useful for troubleshooting connectivity issues.

Route traffic through specific IPs with NAT rules

Applications that need predictable source IPs for allowlisting or compliance can use NAT rules to route specific traffic through designated addresses.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const net = new gcp.compute.Network("net", {
    name: "my-network",
    autoCreateSubnetworks: false,
});
const subnet = new gcp.compute.Subnetwork("subnet", {
    name: "my-subnetwork",
    network: net.id,
    ipCidrRange: "10.0.0.0/16",
    region: "us-central1",
});
const router = new gcp.compute.Router("router", {
    name: "my-router",
    region: subnet.region,
    network: net.id,
});
const addr1 = new gcp.compute.Address("addr1", {
    name: "nat-address1",
    region: subnet.region,
});
const addr2 = new gcp.compute.Address("addr2", {
    name: "nat-address2",
    region: subnet.region,
});
const addr3 = new gcp.compute.Address("addr3", {
    name: "nat-address3",
    region: subnet.region,
});
const natRules = new gcp.compute.RouterNat("nat_rules", {
    name: "my-router-nat",
    router: router.name,
    region: router.region,
    natIpAllocateOption: "MANUAL_ONLY",
    natIps: [addr1.selfLink],
    sourceSubnetworkIpRangesToNat: "LIST_OF_SUBNETWORKS",
    subnetworks: [{
        name: subnet.id,
        sourceIpRangesToNats: ["ALL_IP_RANGES"],
    }],
    rules: [{
        ruleNumber: 100,
        description: "nat rules example",
        match: "inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')",
        action: {
            sourceNatActiveIps: [
                addr2.selfLink,
                addr3.selfLink,
            ],
        },
    }],
    enableEndpointIndependentMapping: false,
});
import pulumi
import pulumi_gcp as gcp

net = gcp.compute.Network("net",
    name="my-network",
    auto_create_subnetworks=False)
subnet = gcp.compute.Subnetwork("subnet",
    name="my-subnetwork",
    network=net.id,
    ip_cidr_range="10.0.0.0/16",
    region="us-central1")
router = gcp.compute.Router("router",
    name="my-router",
    region=subnet.region,
    network=net.id)
addr1 = gcp.compute.Address("addr1",
    name="nat-address1",
    region=subnet.region)
addr2 = gcp.compute.Address("addr2",
    name="nat-address2",
    region=subnet.region)
addr3 = gcp.compute.Address("addr3",
    name="nat-address3",
    region=subnet.region)
nat_rules = gcp.compute.RouterNat("nat_rules",
    name="my-router-nat",
    router=router.name,
    region=router.region,
    nat_ip_allocate_option="MANUAL_ONLY",
    nat_ips=[addr1.self_link],
    source_subnetwork_ip_ranges_to_nat="LIST_OF_SUBNETWORKS",
    subnetworks=[{
        "name": subnet.id,
        "source_ip_ranges_to_nats": ["ALL_IP_RANGES"],
    }],
    rules=[{
        "rule_number": 100,
        "description": "nat rules example",
        "match": "inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')",
        "action": {
            "source_nat_active_ips": [
                addr2.self_link,
                addr3.self_link,
            ],
        },
    }],
    enable_endpoint_independent_mapping=False)
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		net, err := compute.NewNetwork(ctx, "net", &compute.NetworkArgs{
			Name:                  pulumi.String("my-network"),
			AutoCreateSubnetworks: pulumi.Bool(false),
		})
		if err != nil {
			return err
		}
		subnet, err := compute.NewSubnetwork(ctx, "subnet", &compute.SubnetworkArgs{
			Name:        pulumi.String("my-subnetwork"),
			Network:     net.ID(),
			IpCidrRange: pulumi.String("10.0.0.0/16"),
			Region:      pulumi.String("us-central1"),
		})
		if err != nil {
			return err
		}
		router, err := compute.NewRouter(ctx, "router", &compute.RouterArgs{
			Name:    pulumi.String("my-router"),
			Region:  subnet.Region,
			Network: net.ID(),
		})
		if err != nil {
			return err
		}
		addr1, err := compute.NewAddress(ctx, "addr1", &compute.AddressArgs{
			Name:   pulumi.String("nat-address1"),
			Region: subnet.Region,
		})
		if err != nil {
			return err
		}
		addr2, err := compute.NewAddress(ctx, "addr2", &compute.AddressArgs{
			Name:   pulumi.String("nat-address2"),
			Region: subnet.Region,
		})
		if err != nil {
			return err
		}
		addr3, err := compute.NewAddress(ctx, "addr3", &compute.AddressArgs{
			Name:   pulumi.String("nat-address3"),
			Region: subnet.Region,
		})
		if err != nil {
			return err
		}
		_, err = compute.NewRouterNat(ctx, "nat_rules", &compute.RouterNatArgs{
			Name:                pulumi.String("my-router-nat"),
			Router:              router.Name,
			Region:              router.Region,
			NatIpAllocateOption: pulumi.String("MANUAL_ONLY"),
			NatIps: pulumi.StringArray{
				addr1.SelfLink,
			},
			SourceSubnetworkIpRangesToNat: pulumi.String("LIST_OF_SUBNETWORKS"),
			Subnetworks: compute.RouterNatSubnetworkArray{
				&compute.RouterNatSubnetworkArgs{
					Name: subnet.ID(),
					SourceIpRangesToNats: pulumi.StringArray{
						pulumi.String("ALL_IP_RANGES"),
					},
				},
			},
			Rules: compute.RouterNatRuleArray{
				&compute.RouterNatRuleArgs{
					RuleNumber:  pulumi.Int(100),
					Description: pulumi.String("nat rules example"),
					Match:       pulumi.String("inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')"),
					Action: &compute.RouterNatRuleActionArgs{
						SourceNatActiveIps: pulumi.StringArray{
							addr2.SelfLink,
							addr3.SelfLink,
						},
					},
				},
			},
			EnableEndpointIndependentMapping: pulumi.Bool(false),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var net = new Gcp.Compute.Network("net", new()
    {
        Name = "my-network",
        AutoCreateSubnetworks = false,
    });

    var subnet = new Gcp.Compute.Subnetwork("subnet", new()
    {
        Name = "my-subnetwork",
        Network = net.Id,
        IpCidrRange = "10.0.0.0/16",
        Region = "us-central1",
    });

    var router = new Gcp.Compute.Router("router", new()
    {
        Name = "my-router",
        Region = subnet.Region,
        Network = net.Id,
    });

    var addr1 = new Gcp.Compute.Address("addr1", new()
    {
        Name = "nat-address1",
        Region = subnet.Region,
    });

    var addr2 = new Gcp.Compute.Address("addr2", new()
    {
        Name = "nat-address2",
        Region = subnet.Region,
    });

    var addr3 = new Gcp.Compute.Address("addr3", new()
    {
        Name = "nat-address3",
        Region = subnet.Region,
    });

    var natRules = new Gcp.Compute.RouterNat("nat_rules", new()
    {
        Name = "my-router-nat",
        Router = router.Name,
        Region = router.Region,
        NatIpAllocateOption = "MANUAL_ONLY",
        NatIps = new[]
        {
            addr1.SelfLink,
        },
        SourceSubnetworkIpRangesToNat = "LIST_OF_SUBNETWORKS",
        Subnetworks = new[]
        {
            new Gcp.Compute.Inputs.RouterNatSubnetworkArgs
            {
                Name = subnet.Id,
                SourceIpRangesToNats = new[]
                {
                    "ALL_IP_RANGES",
                },
            },
        },
        Rules = new[]
        {
            new Gcp.Compute.Inputs.RouterNatRuleArgs
            {
                RuleNumber = 100,
                Description = "nat rules example",
                Match = "inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')",
                Action = new Gcp.Compute.Inputs.RouterNatRuleActionArgs
                {
                    SourceNatActiveIps = new[]
                    {
                        addr2.SelfLink,
                        addr3.SelfLink,
                    },
                },
            },
        },
        EnableEndpointIndependentMapping = false,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Network;
import com.pulumi.gcp.compute.NetworkArgs;
import com.pulumi.gcp.compute.Subnetwork;
import com.pulumi.gcp.compute.SubnetworkArgs;
import com.pulumi.gcp.compute.Router;
import com.pulumi.gcp.compute.RouterArgs;
import com.pulumi.gcp.compute.Address;
import com.pulumi.gcp.compute.AddressArgs;
import com.pulumi.gcp.compute.RouterNat;
import com.pulumi.gcp.compute.RouterNatArgs;
import com.pulumi.gcp.compute.inputs.RouterNatSubnetworkArgs;
import com.pulumi.gcp.compute.inputs.RouterNatRuleArgs;
import com.pulumi.gcp.compute.inputs.RouterNatRuleActionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var net = new Network("net", NetworkArgs.builder()
            .name("my-network")
            .autoCreateSubnetworks(false)
            .build());

        var subnet = new Subnetwork("subnet", SubnetworkArgs.builder()
            .name("my-subnetwork")
            .network(net.id())
            .ipCidrRange("10.0.0.0/16")
            .region("us-central1")
            .build());

        var router = new Router("router", RouterArgs.builder()
            .name("my-router")
            .region(subnet.region())
            .network(net.id())
            .build());

        var addr1 = new Address("addr1", AddressArgs.builder()
            .name("nat-address1")
            .region(subnet.region())
            .build());

        var addr2 = new Address("addr2", AddressArgs.builder()
            .name("nat-address2")
            .region(subnet.region())
            .build());

        var addr3 = new Address("addr3", AddressArgs.builder()
            .name("nat-address3")
            .region(subnet.region())
            .build());

        var natRules = new RouterNat("natRules", RouterNatArgs.builder()
            .name("my-router-nat")
            .router(router.name())
            .region(router.region())
            .natIpAllocateOption("MANUAL_ONLY")
            .natIps(addr1.selfLink())
            .sourceSubnetworkIpRangesToNat("LIST_OF_SUBNETWORKS")
            .subnetworks(RouterNatSubnetworkArgs.builder()
                .name(subnet.id())
                .sourceIpRangesToNats("ALL_IP_RANGES")
                .build())
            .rules(RouterNatRuleArgs.builder()
                .ruleNumber(100)
                .description("nat rules example")
                .match("inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')")
                .action(RouterNatRuleActionArgs.builder()
                    .sourceNatActiveIps(                    
                        addr2.selfLink(),
                        addr3.selfLink())
                    .build())
                .build())
            .enableEndpointIndependentMapping(false)
            .build());

    }
}
resources:
  net:
    type: gcp:compute:Network
    properties:
      name: my-network
      autoCreateSubnetworks: false
  subnet:
    type: gcp:compute:Subnetwork
    properties:
      name: my-subnetwork
      network: ${net.id}
      ipCidrRange: 10.0.0.0/16
      region: us-central1
  router:
    type: gcp:compute:Router
    properties:
      name: my-router
      region: ${subnet.region}
      network: ${net.id}
  addr1:
    type: gcp:compute:Address
    properties:
      name: nat-address1
      region: ${subnet.region}
  addr2:
    type: gcp:compute:Address
    properties:
      name: nat-address2
      region: ${subnet.region}
  addr3:
    type: gcp:compute:Address
    properties:
      name: nat-address3
      region: ${subnet.region}
  natRules:
    type: gcp:compute:RouterNat
    name: nat_rules
    properties:
      name: my-router-nat
      router: ${router.name}
      region: ${router.region}
      natIpAllocateOption: MANUAL_ONLY
      natIps:
        - ${addr1.selfLink}
      sourceSubnetworkIpRangesToNat: LIST_OF_SUBNETWORKS
      subnetworks:
        - name: ${subnet.id}
          sourceIpRangesToNats:
            - ALL_IP_RANGES
      rules:
        - ruleNumber: 100
          description: nat rules example
          match: inIpRange(destination.ip, '1.1.0.0/16') || inIpRange(destination.ip, '2.2.0.0/16')
          action:
            sourceNatActiveIps:
              - ${addr2.selfLink}
              - ${addr3.selfLink}
      enableEndpointIndependentMapping: false

With MANUAL_ONLY allocation, you create Address resources and reference them in natIps. The rules array defines traffic matching conditions using the match expression (checking destination IP ranges) and specifies which IPs to use via sourceNatActiveIps. The subnetworks block limits NAT to specific subnets rather than all subnets in the network. Note that Address resources used by RouterNat should include create_before_destroy lifecycle rules to avoid resourceInUseByAnotherResource errors when modifying IP assignments.

Enable private NAT for inter-VPC communication

Organizations with hub-and-spoke network topologies need private NAT to translate addresses between VPCs without exposing traffic to the public internet.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const net = new gcp.compute.Network("net", {name: "my-network"});
const subnet = new gcp.compute.Subnetwork("subnet", {
    name: "my-subnetwork",
    network: net.id,
    ipCidrRange: "10.0.0.0/16",
    region: "us-central1",
    purpose: "PRIVATE_NAT",
});
const router = new gcp.compute.Router("router", {
    name: "my-router",
    region: subnet.region,
    network: net.id,
});
const hub = new gcp.networkconnectivity.Hub("hub", {
    name: "my-hub",
    description: "vpc hub for inter vpc nat",
});
const spoke = new gcp.networkconnectivity.Spoke("spoke", {
    name: "my-spoke",
    location: "global",
    description: "vpc spoke for inter vpc nat",
    hub: hub.id,
    linkedVpcNetwork: {
        excludeExportRanges: [
            "198.51.100.0/24",
            "10.10.0.0/16",
        ],
        uri: net.selfLink,
    },
});
const natType = new gcp.compute.RouterNat("nat_type", {
    name: "my-router-nat",
    router: router.name,
    region: router.region,
    sourceSubnetworkIpRangesToNat: "LIST_OF_SUBNETWORKS",
    enableDynamicPortAllocation: false,
    enableEndpointIndependentMapping: false,
    minPortsPerVm: 32,
    type: "PRIVATE",
    subnetworks: [{
        name: subnet.id,
        sourceIpRangesToNats: ["ALL_IP_RANGES"],
    }],
    rules: [{
        ruleNumber: 100,
        description: "rule for private nat",
        match: "nexthop.hub == \"//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub\"",
        action: {
            sourceNatActiveRanges: [subnet.selfLink],
        },
    }],
});
import pulumi
import pulumi_gcp as gcp

net = gcp.compute.Network("net", name="my-network")
subnet = gcp.compute.Subnetwork("subnet",
    name="my-subnetwork",
    network=net.id,
    ip_cidr_range="10.0.0.0/16",
    region="us-central1",
    purpose="PRIVATE_NAT")
router = gcp.compute.Router("router",
    name="my-router",
    region=subnet.region,
    network=net.id)
hub = gcp.networkconnectivity.Hub("hub",
    name="my-hub",
    description="vpc hub for inter vpc nat")
spoke = gcp.networkconnectivity.Spoke("spoke",
    name="my-spoke",
    location="global",
    description="vpc spoke for inter vpc nat",
    hub=hub.id,
    linked_vpc_network={
        "exclude_export_ranges": [
            "198.51.100.0/24",
            "10.10.0.0/16",
        ],
        "uri": net.self_link,
    })
nat_type = gcp.compute.RouterNat("nat_type",
    name="my-router-nat",
    router=router.name,
    region=router.region,
    source_subnetwork_ip_ranges_to_nat="LIST_OF_SUBNETWORKS",
    enable_dynamic_port_allocation=False,
    enable_endpoint_independent_mapping=False,
    min_ports_per_vm=32,
    type="PRIVATE",
    subnetworks=[{
        "name": subnet.id,
        "source_ip_ranges_to_nats": ["ALL_IP_RANGES"],
    }],
    rules=[{
        "rule_number": 100,
        "description": "rule for private nat",
        "match": "nexthop.hub == \"//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub\"",
        "action": {
            "source_nat_active_ranges": [subnet.self_link],
        },
    }])
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/networkconnectivity"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		net, err := compute.NewNetwork(ctx, "net", &compute.NetworkArgs{
			Name: pulumi.String("my-network"),
		})
		if err != nil {
			return err
		}
		subnet, err := compute.NewSubnetwork(ctx, "subnet", &compute.SubnetworkArgs{
			Name:        pulumi.String("my-subnetwork"),
			Network:     net.ID(),
			IpCidrRange: pulumi.String("10.0.0.0/16"),
			Region:      pulumi.String("us-central1"),
			Purpose:     pulumi.String("PRIVATE_NAT"),
		})
		if err != nil {
			return err
		}
		router, err := compute.NewRouter(ctx, "router", &compute.RouterArgs{
			Name:    pulumi.String("my-router"),
			Region:  subnet.Region,
			Network: net.ID(),
		})
		if err != nil {
			return err
		}
		hub, err := networkconnectivity.NewHub(ctx, "hub", &networkconnectivity.HubArgs{
			Name:        pulumi.String("my-hub"),
			Description: pulumi.String("vpc hub for inter vpc nat"),
		})
		if err != nil {
			return err
		}
		_, err = networkconnectivity.NewSpoke(ctx, "spoke", &networkconnectivity.SpokeArgs{
			Name:        pulumi.String("my-spoke"),
			Location:    pulumi.String("global"),
			Description: pulumi.String("vpc spoke for inter vpc nat"),
			Hub:         hub.ID(),
			LinkedVpcNetwork: &networkconnectivity.SpokeLinkedVpcNetworkArgs{
				ExcludeExportRanges: pulumi.StringArray{
					pulumi.String("198.51.100.0/24"),
					pulumi.String("10.10.0.0/16"),
				},
				Uri: net.SelfLink,
			},
		})
		if err != nil {
			return err
		}
		_, err = compute.NewRouterNat(ctx, "nat_type", &compute.RouterNatArgs{
			Name:                             pulumi.String("my-router-nat"),
			Router:                           router.Name,
			Region:                           router.Region,
			SourceSubnetworkIpRangesToNat:    pulumi.String("LIST_OF_SUBNETWORKS"),
			EnableDynamicPortAllocation:      pulumi.Bool(false),
			EnableEndpointIndependentMapping: pulumi.Bool(false),
			MinPortsPerVm:                    pulumi.Int(32),
			Type:                             pulumi.String("PRIVATE"),
			Subnetworks: compute.RouterNatSubnetworkArray{
				&compute.RouterNatSubnetworkArgs{
					Name: subnet.ID(),
					SourceIpRangesToNats: pulumi.StringArray{
						pulumi.String("ALL_IP_RANGES"),
					},
				},
			},
			Rules: compute.RouterNatRuleArray{
				&compute.RouterNatRuleArgs{
					RuleNumber:  pulumi.Int(100),
					Description: pulumi.String("rule for private nat"),
					Match:       pulumi.String("nexthop.hub == \"//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub\""),
					Action: &compute.RouterNatRuleActionArgs{
						SourceNatActiveRanges: pulumi.StringArray{
							subnet.SelfLink,
						},
					},
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var net = new Gcp.Compute.Network("net", new()
    {
        Name = "my-network",
    });

    var subnet = new Gcp.Compute.Subnetwork("subnet", new()
    {
        Name = "my-subnetwork",
        Network = net.Id,
        IpCidrRange = "10.0.0.0/16",
        Region = "us-central1",
        Purpose = "PRIVATE_NAT",
    });

    var router = new Gcp.Compute.Router("router", new()
    {
        Name = "my-router",
        Region = subnet.Region,
        Network = net.Id,
    });

    var hub = new Gcp.NetworkConnectivity.Hub("hub", new()
    {
        Name = "my-hub",
        Description = "vpc hub for inter vpc nat",
    });

    var spoke = new Gcp.NetworkConnectivity.Spoke("spoke", new()
    {
        Name = "my-spoke",
        Location = "global",
        Description = "vpc spoke for inter vpc nat",
        Hub = hub.Id,
        LinkedVpcNetwork = new Gcp.NetworkConnectivity.Inputs.SpokeLinkedVpcNetworkArgs
        {
            ExcludeExportRanges = new[]
            {
                "198.51.100.0/24",
                "10.10.0.0/16",
            },
            Uri = net.SelfLink,
        },
    });

    var natType = new Gcp.Compute.RouterNat("nat_type", new()
    {
        Name = "my-router-nat",
        Router = router.Name,
        Region = router.Region,
        SourceSubnetworkIpRangesToNat = "LIST_OF_SUBNETWORKS",
        EnableDynamicPortAllocation = false,
        EnableEndpointIndependentMapping = false,
        MinPortsPerVm = 32,
        Type = "PRIVATE",
        Subnetworks = new[]
        {
            new Gcp.Compute.Inputs.RouterNatSubnetworkArgs
            {
                Name = subnet.Id,
                SourceIpRangesToNats = new[]
                {
                    "ALL_IP_RANGES",
                },
            },
        },
        Rules = new[]
        {
            new Gcp.Compute.Inputs.RouterNatRuleArgs
            {
                RuleNumber = 100,
                Description = "rule for private nat",
                Match = "nexthop.hub == \"//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub\"",
                Action = new Gcp.Compute.Inputs.RouterNatRuleActionArgs
                {
                    SourceNatActiveRanges = new[]
                    {
                        subnet.SelfLink,
                    },
                },
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.compute.Network;
import com.pulumi.gcp.compute.NetworkArgs;
import com.pulumi.gcp.compute.Subnetwork;
import com.pulumi.gcp.compute.SubnetworkArgs;
import com.pulumi.gcp.compute.Router;
import com.pulumi.gcp.compute.RouterArgs;
import com.pulumi.gcp.networkconnectivity.Hub;
import com.pulumi.gcp.networkconnectivity.HubArgs;
import com.pulumi.gcp.networkconnectivity.Spoke;
import com.pulumi.gcp.networkconnectivity.SpokeArgs;
import com.pulumi.gcp.networkconnectivity.inputs.SpokeLinkedVpcNetworkArgs;
import com.pulumi.gcp.compute.RouterNat;
import com.pulumi.gcp.compute.RouterNatArgs;
import com.pulumi.gcp.compute.inputs.RouterNatSubnetworkArgs;
import com.pulumi.gcp.compute.inputs.RouterNatRuleArgs;
import com.pulumi.gcp.compute.inputs.RouterNatRuleActionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var net = new Network("net", NetworkArgs.builder()
            .name("my-network")
            .build());

        var subnet = new Subnetwork("subnet", SubnetworkArgs.builder()
            .name("my-subnetwork")
            .network(net.id())
            .ipCidrRange("10.0.0.0/16")
            .region("us-central1")
            .purpose("PRIVATE_NAT")
            .build());

        var router = new Router("router", RouterArgs.builder()
            .name("my-router")
            .region(subnet.region())
            .network(net.id())
            .build());

        var hub = new Hub("hub", HubArgs.builder()
            .name("my-hub")
            .description("vpc hub for inter vpc nat")
            .build());

        var spoke = new Spoke("spoke", SpokeArgs.builder()
            .name("my-spoke")
            .location("global")
            .description("vpc spoke for inter vpc nat")
            .hub(hub.id())
            .linkedVpcNetwork(SpokeLinkedVpcNetworkArgs.builder()
                .excludeExportRanges(                
                    "198.51.100.0/24",
                    "10.10.0.0/16")
                .uri(net.selfLink())
                .build())
            .build());

        var natType = new RouterNat("natType", RouterNatArgs.builder()
            .name("my-router-nat")
            .router(router.name())
            .region(router.region())
            .sourceSubnetworkIpRangesToNat("LIST_OF_SUBNETWORKS")
            .enableDynamicPortAllocation(false)
            .enableEndpointIndependentMapping(false)
            .minPortsPerVm(32)
            .type("PRIVATE")
            .subnetworks(RouterNatSubnetworkArgs.builder()
                .name(subnet.id())
                .sourceIpRangesToNats("ALL_IP_RANGES")
                .build())
            .rules(RouterNatRuleArgs.builder()
                .ruleNumber(100)
                .description("rule for private nat")
                .match("nexthop.hub == \"//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub\"")
                .action(RouterNatRuleActionArgs.builder()
                    .sourceNatActiveRanges(subnet.selfLink())
                    .build())
                .build())
            .build());

    }
}
resources:
  net:
    type: gcp:compute:Network
    properties:
      name: my-network
  subnet:
    type: gcp:compute:Subnetwork
    properties:
      name: my-subnetwork
      network: ${net.id}
      ipCidrRange: 10.0.0.0/16
      region: us-central1
      purpose: PRIVATE_NAT
  router:
    type: gcp:compute:Router
    properties:
      name: my-router
      region: ${subnet.region}
      network: ${net.id}
  hub:
    type: gcp:networkconnectivity:Hub
    properties:
      name: my-hub
      description: vpc hub for inter vpc nat
  spoke:
    type: gcp:networkconnectivity:Spoke
    properties:
      name: my-spoke
      location: global
      description: vpc spoke for inter vpc nat
      hub: ${hub.id}
      linkedVpcNetwork:
        excludeExportRanges:
          - 198.51.100.0/24
          - 10.10.0.0/16
        uri: ${net.selfLink}
  natType:
    type: gcp:compute:RouterNat
    name: nat_type
    properties:
      name: my-router-nat
      router: ${router.name}
      region: ${router.region}
      sourceSubnetworkIpRangesToNat: LIST_OF_SUBNETWORKS
      enableDynamicPortAllocation: false
      enableEndpointIndependentMapping: false
      minPortsPerVm: 32
      type: PRIVATE
      subnetworks:
        - name: ${subnet.id}
          sourceIpRangesToNats:
            - ALL_IP_RANGES
      rules:
        - ruleNumber: 100
          description: rule for private nat
          match: nexthop.hub == "//networkconnectivity.googleapis.com/projects/acm-test-proj-123/locations/global/hubs/my-hub"
          action:
            sourceNatActiveRanges:
              - ${subnet.selfLink}

Setting type to PRIVATE enables private NAT, which translates addresses for traffic between connected VPCs. The rules block uses a match expression that checks the nexthop.hub to identify traffic flowing through the Network Connectivity Hub. Instead of external IPs, sourceNatActiveRanges references the subnet’s CIDR range for address translation. This configuration requires a subnet with purpose set to PRIVATE_NAT and Network Connectivity Hub and Spoke resources to establish inter-VPC routing.

Beyond these examples

These snippets focus on specific Cloud NAT features: automatic and manual IP allocation, NAT rules for traffic steering, and private NAT for inter-VPC translation. They’re intentionally minimal rather than full network architectures.

The examples may reference pre-existing infrastructure such as VPC networks, subnets, and Cloud Routers, static IP addresses for manual allocation, and Network Connectivity Hub and Spoke resources for private NAT. They focus on configuring the NAT gateway rather than provisioning the entire network topology.

To keep things focused, common NAT patterns are omitted, including:

  • Connection timeout tuning (tcpEstablishedIdleTimeoutSec, udpIdleTimeoutSec)
  • Dynamic port allocation (enableDynamicPortAllocation, minPortsPerVm, maxPortsPerVm)
  • Endpoint-independent mapping (enableEndpointIndependentMapping)
  • NAT64 for IPv6 translation (nat64Subnetworks, sourceSubnetworkIpRangesToNat64)

These omissions are intentional: the goal is to illustrate how each NAT feature is wired, not provide drop-in networking modules. See the RouterNat resource reference for all available configuration options.

Let's configure GCP Cloud Router NAT

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Common Errors & Pitfalls
Why am I getting resourceInUseByAnotherResource when recreating NAT IP addresses?
Recreating a gcp.compute.Address that’s being used by RouterNat will trigger this error. Use lifecycle.create_before_destroy = true on the address resource to avoid it.
Can I use both enableDynamicPortAllocation and enableEndpointIndependentMapping?
No, these properties are mutually exclusive. You must choose one or the other based on your requirements.
Why can't I create multiple RouterNat resources with ALL_SUBNETWORKS options?
When sourceSubnetworkIpRangesToNat is set to ALL_SUBNETWORKS_ALL_IP_RANGES or ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES, there should not be any other RouterNat section in any Router for this network in this region.
IP Allocation & Addressing
What's the difference between AUTO_ONLY and MANUAL_ONLY IP allocation?
AUTO_ONLY lets Google Cloud Platform automatically allocate NAT IPs, while MANUAL_ONLY requires you to provide user-allocated NAT IP addresses via the natIps property.
When should I use initialNatIps instead of natIps?
Use initialNatIps when creating a RouterNat alongside a RouterNatAddress resource. It conflicts with natIps and drainNatIps, and is only valid when natIpAllocateOption is set to MANUAL_ONLY.
Subnetwork Configuration
What are the options for sourceSubnetworkIpRangesToNat?
You have three options: ALL_SUBNETWORKS_ALL_IP_RANGES (all IP ranges in every subnetwork), ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES (only primary IP ranges), or LIST_OF_SUBNETWORKS (specify subnetworks in the subnetworks field).
How do I configure NAT for specific subnetworks?
Set sourceSubnetworkIpRangesToNat to LIST_OF_SUBNETWORKS and provide the subnetworks field with your subnetwork configurations.
Port Allocation
What are the port allocation requirements when using dynamic port allocation?
When enableDynamicPortAllocation is enabled, minPortsPerVm must be a power of two greater than or equal to 32, and maxPortsPerVm must be a power of two greater than minPortsPerVm.
What are the default port allocation values?
minPortsPerVm defaults to 64 for static port allocation and 32 for dynamic port allocation. If maxPortsPerVm is not set with dynamic allocation, a maximum of 65,536 ports will be allocated.
NAT Types & Advanced Features
What's the difference between PUBLIC and PRIVATE NAT?
PUBLIC NAT (the default) is used for public IP translation, while PRIVATE NAT is used for private IP translation. The type property is immutable after creation.
Can I enable NAT64 on multiple RouterNat resources in the same region?
No, if sourceSubnetworkIpRangesToNat64 contains ALL_IPV6_SUBNETWORKS, no other Router.Nat section in this region can also enable NAT64 for any subnetworks in this network. Other Router.Nat sections can still enable NAT44 only.
Timeouts & Connection Limits
What are the default timeout values for different connection types?
ICMP connections default to 30 seconds, UDP connections to 30 seconds, TCP transitory connections to 30 seconds, TCP established connections to 1,200 seconds (20 minutes), and TCP TIME_WAIT connections to 120 seconds (2 minutes).

Using a different cloud?

Explore networking guides for other cloud providers: