Create and Configure AWS VPCs

The aws:ec2/vpc:Vpc resource, part of the Pulumi AWS provider, provisions the VPC network container itself: its IPv4 address space, tenancy rules, and DNS behavior. This guide focuses on three capabilities: IPv4 CIDR allocation (explicit and IPAM-managed), instance tenancy configuration, and organizational tagging.

VPCs are the foundation for AWS networking. Subnets, route tables, security groups, and internet gateways are configured through companion resources. The examples are intentionally small. Combine them with your own subnet layout and routing configuration.

Create a VPC with a CIDR block

Most AWS deployments begin by defining a VPC with an IPv4 CIDR block that determines the private IP address range available for resources.

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

const main = new aws.ec2.Vpc("main", {cidrBlock: "10.0.0.0/16"});
import pulumi
import pulumi_aws as aws

main = aws.ec2.Vpc("main", cidr_block="10.0.0.0/16")
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := ec2.NewVpc(ctx, "main", &ec2.VpcArgs{
			CidrBlock: pulumi.String("10.0.0.0/16"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var main = new Aws.Ec2.Vpc("main", new()
    {
        CidrBlock = "10.0.0.0/16",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.Vpc;
import com.pulumi.aws.ec2.VpcArgs;
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 main = new Vpc("main", VpcArgs.builder()
            .cidrBlock("10.0.0.0/16")
            .build());

    }
}
resources:
  main:
    type: aws:ec2:Vpc
    properties:
      cidrBlock: 10.0.0.0/16

The cidrBlock property sets the IPv4 address range using CIDR notation. This example uses 10.0.0.0/16, which provides 65,536 private IP addresses. Choose a CIDR block that doesn’t overlap with other networks you’ll connect to via VPN or peering.

Add instance tenancy and organizational tags

Teams often need to control where EC2 instances run and apply tags for cost tracking and resource organization.

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

const main = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    instanceTenancy: "default",
    tags: {
        Name: "main",
    },
});
import pulumi
import pulumi_aws as aws

main = aws.ec2.Vpc("main",
    cidr_block="10.0.0.0/16",
    instance_tenancy="default",
    tags={
        "Name": "main",
    })
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := ec2.NewVpc(ctx, "main", &ec2.VpcArgs{
			CidrBlock:       pulumi.String("10.0.0.0/16"),
			InstanceTenancy: pulumi.String("default"),
			Tags: pulumi.StringMap{
				"Name": pulumi.String("main"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var main = new Aws.Ec2.Vpc("main", new()
    {
        CidrBlock = "10.0.0.0/16",
        InstanceTenancy = "default",
        Tags = 
        {
            { "Name", "main" },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.Vpc;
import com.pulumi.aws.ec2.VpcArgs;
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 main = new Vpc("main", VpcArgs.builder()
            .cidrBlock("10.0.0.0/16")
            .instanceTenancy("default")
            .tags(Map.of("Name", "main"))
            .build());

    }
}
resources:
  main:
    type: aws:ec2:Vpc
    properties:
      cidrBlock: 10.0.0.0/16
      instanceTenancy: default
      tags:
        Name: main

The instanceTenancy property controls hardware isolation. Setting it to “default” allows instances to use their own tenancy attribute; “dedicated” forces all instances onto dedicated hardware (incurs additional per-hour fees). The tags property adds key-value metadata for organization and cost allocation.

Allocate CIDR from AWS IPAM pools

Organizations managing IP addresses across multiple accounts and regions use IPAM to centralize allocation and prevent overlapping ranges.

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

const current = aws.getRegion({});
const test = new aws.ec2.VpcIpam("test", {operatingRegions: [{
    regionName: current.then(current => current.region),
}]});
const testVpcIpamPool = new aws.ec2.VpcIpamPool("test", {
    addressFamily: "ipv4",
    ipamScopeId: test.privateDefaultScopeId,
    locale: current.then(current => current.region),
});
const testVpcIpamPoolCidr = new aws.ec2.VpcIpamPoolCidr("test", {
    ipamPoolId: testVpcIpamPool.id,
    cidr: "172.20.0.0/16",
});
const testVpc = new aws.ec2.Vpc("test", {
    ipv4IpamPoolId: testVpcIpamPool.id,
    ipv4NetmaskLength: 28,
}, {
    dependsOn: [testVpcIpamPoolCidr],
});
import pulumi
import pulumi_aws as aws

current = aws.get_region()
test = aws.ec2.VpcIpam("test", operating_regions=[{
    "region_name": current.region,
}])
test_vpc_ipam_pool = aws.ec2.VpcIpamPool("test",
    address_family="ipv4",
    ipam_scope_id=test.private_default_scope_id,
    locale=current.region)
test_vpc_ipam_pool_cidr = aws.ec2.VpcIpamPoolCidr("test",
    ipam_pool_id=test_vpc_ipam_pool.id,
    cidr="172.20.0.0/16")
test_vpc = aws.ec2.Vpc("test",
    ipv4_ipam_pool_id=test_vpc_ipam_pool.id,
    ipv4_netmask_length=28,
    opts = pulumi.ResourceOptions(depends_on=[test_vpc_ipam_pool_cidr]))
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		current, err := aws.GetRegion(ctx, &aws.GetRegionArgs{}, nil)
		if err != nil {
			return err
		}
		test, err := ec2.NewVpcIpam(ctx, "test", &ec2.VpcIpamArgs{
			OperatingRegions: ec2.VpcIpamOperatingRegionArray{
				&ec2.VpcIpamOperatingRegionArgs{
					RegionName: pulumi.String(current.Region),
				},
			},
		})
		if err != nil {
			return err
		}
		testVpcIpamPool, err := ec2.NewVpcIpamPool(ctx, "test", &ec2.VpcIpamPoolArgs{
			AddressFamily: pulumi.String("ipv4"),
			IpamScopeId:   test.PrivateDefaultScopeId,
			Locale:        pulumi.String(current.Region),
		})
		if err != nil {
			return err
		}
		testVpcIpamPoolCidr, err := ec2.NewVpcIpamPoolCidr(ctx, "test", &ec2.VpcIpamPoolCidrArgs{
			IpamPoolId: testVpcIpamPool.ID(),
			Cidr:       pulumi.String("172.20.0.0/16"),
		})
		if err != nil {
			return err
		}
		_, err = ec2.NewVpc(ctx, "test", &ec2.VpcArgs{
			Ipv4IpamPoolId:    testVpcIpamPool.ID(),
			Ipv4NetmaskLength: pulumi.Int(28),
		}, pulumi.DependsOn([]pulumi.Resource{
			testVpcIpamPoolCidr,
		}))
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var current = Aws.GetRegion.Invoke();

    var test = new Aws.Ec2.VpcIpam("test", new()
    {
        OperatingRegions = new[]
        {
            new Aws.Ec2.Inputs.VpcIpamOperatingRegionArgs
            {
                RegionName = current.Apply(getRegionResult => getRegionResult.Region),
            },
        },
    });

    var testVpcIpamPool = new Aws.Ec2.VpcIpamPool("test", new()
    {
        AddressFamily = "ipv4",
        IpamScopeId = test.PrivateDefaultScopeId,
        Locale = current.Apply(getRegionResult => getRegionResult.Region),
    });

    var testVpcIpamPoolCidr = new Aws.Ec2.VpcIpamPoolCidr("test", new()
    {
        IpamPoolId = testVpcIpamPool.Id,
        Cidr = "172.20.0.0/16",
    });

    var testVpc = new Aws.Ec2.Vpc("test", new()
    {
        Ipv4IpamPoolId = testVpcIpamPool.Id,
        Ipv4NetmaskLength = 28,
    }, new CustomResourceOptions
    {
        DependsOn =
        {
            testVpcIpamPoolCidr,
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.AwsFunctions;
import com.pulumi.aws.inputs.GetRegionArgs;
import com.pulumi.aws.ec2.VpcIpam;
import com.pulumi.aws.ec2.VpcIpamArgs;
import com.pulumi.aws.ec2.inputs.VpcIpamOperatingRegionArgs;
import com.pulumi.aws.ec2.VpcIpamPool;
import com.pulumi.aws.ec2.VpcIpamPoolArgs;
import com.pulumi.aws.ec2.VpcIpamPoolCidr;
import com.pulumi.aws.ec2.VpcIpamPoolCidrArgs;
import com.pulumi.aws.ec2.Vpc;
import com.pulumi.aws.ec2.VpcArgs;
import com.pulumi.resources.CustomResourceOptions;
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) {
        final var current = AwsFunctions.getRegion(GetRegionArgs.builder()
            .build());

        var test = new VpcIpam("test", VpcIpamArgs.builder()
            .operatingRegions(VpcIpamOperatingRegionArgs.builder()
                .regionName(current.region())
                .build())
            .build());

        var testVpcIpamPool = new VpcIpamPool("testVpcIpamPool", VpcIpamPoolArgs.builder()
            .addressFamily("ipv4")
            .ipamScopeId(test.privateDefaultScopeId())
            .locale(current.region())
            .build());

        var testVpcIpamPoolCidr = new VpcIpamPoolCidr("testVpcIpamPoolCidr", VpcIpamPoolCidrArgs.builder()
            .ipamPoolId(testVpcIpamPool.id())
            .cidr("172.20.0.0/16")
            .build());

        var testVpc = new Vpc("testVpc", VpcArgs.builder()
            .ipv4IpamPoolId(testVpcIpamPool.id())
            .ipv4NetmaskLength(28)
            .build(), CustomResourceOptions.builder()
                .dependsOn(testVpcIpamPoolCidr)
                .build());

    }
}
resources:
  test:
    type: aws:ec2:VpcIpam
    properties:
      operatingRegions:
        - regionName: ${current.region}
  testVpcIpamPool:
    type: aws:ec2:VpcIpamPool
    name: test
    properties:
      addressFamily: ipv4
      ipamScopeId: ${test.privateDefaultScopeId}
      locale: ${current.region}
  testVpcIpamPoolCidr:
    type: aws:ec2:VpcIpamPoolCidr
    name: test
    properties:
      ipamPoolId: ${testVpcIpamPool.id}
      cidr: 172.20.0.0/16
  testVpc:
    type: aws:ec2:Vpc
    name: test
    properties:
      ipv4IpamPoolId: ${testVpcIpamPool.id}
      ipv4NetmaskLength: 28
    options:
      dependsOn:
        - ${testVpcIpamPoolCidr}
variables:
  current:
    fn::invoke:
      function: aws:getRegion
      arguments: {}

Instead of specifying cidrBlock directly, this configuration uses ipv4IpamPoolId to reference an IPAM pool and ipv4NetmaskLength to request a /28 subnet from that pool. IPAM automatically assigns a non-overlapping CIDR block. The dependsOn ensures the pool CIDR exists before VPC creation.

Beyond these examples

These snippets focus on specific VPC-level features: IPv4 CIDR allocation (explicit and IPAM-managed) and instance tenancy and tagging. They’re intentionally minimal rather than full network architectures.

The IPAM example requires pre-existing infrastructure such as AWS IPAM configuration with operating regions and pool CIDRs. The examples focus on VPC configuration rather than provisioning subnets, route tables, or gateways.

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

  • DNS resolution settings (enableDnsSupport, enableDnsHostnames)
  • IPv6 CIDR blocks and dual-stack configuration
  • Network address usage metrics (enableNetworkAddressUsageMetrics)

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

Let's create and Configure AWS VPCs

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

Try Pulumi Cloud for FREE

Frequently Asked Questions

CIDR & IP Address Management
What VPC properties can't be changed after creation?
The cidrBlock, ipv4IpamPoolId, and ipv4NetmaskLength properties are immutable and require VPC replacement if changed.
How do I allocate a VPC CIDR from AWS IPAM?
Set ipv4IpamPoolId to your IPAM pool ID and specify ipv4NetmaskLength for the desired CIDR size. Add dependsOn to ensure the IPAM pool CIDR is allocated before VPC creation.
Can I specify the CIDR block explicitly or derive it from IPAM?
Yes, you can either set cidrBlock explicitly (e.g., “10.0.0.0/16”) or derive it from IPAM using ipv4IpamPoolId with ipv4NetmaskLength.
IPv6 Configuration
What are my options for enabling IPv6 on a VPC?
You have two options: use assignGeneratedIpv6CidrBlock for an Amazon-provided /56 CIDR block, or use ipv6IpamPoolId for IPAM-managed allocation. These options conflict and cannot be used together.
Why am I getting conflicts with IPv6 configuration?
assignGeneratedIpv6CidrBlock conflicts with ipv6IpamPoolId, and ipv6NetmaskLength conflicts with ipv6CidrBlock. Choose one approach per conflict pair.
What's the valid range for IPv6 netmask length?
Valid values for ipv6NetmaskLength are 44 to 60 in increments of 4 (44, 48, 52, 56, 60).
DNS & Instance Configuration
Why aren't my EC2 instances getting DNS hostnames?
DNS hostnames are disabled by default. Set enableDnsHostnames to true to enable hostname resolution for instances in the VPC.
What DNS settings should I configure for my VPC?
enableDnsSupport (defaults to true) enables DNS resolution, while enableDnsHostnames (defaults to false) enables DNS hostnames for instances. Enable both for full DNS functionality.
What's the cost of using dedicated instance tenancy?
Setting instanceTenancy to dedicated incurs a $2 per hour per region fee, plus an hourly per-instance usage fee. Use default tenancy unless dedicated instances are required.
Default Resources & Management
What default resources are created with a VPC?
AWS automatically creates a default network ACL (defaultNetworkAclId), route table (defaultRouteTableId), and security group (defaultSecurityGroupId) when you create a VPC.
Can I change the main route table for my VPC?
Yes, use aws.ec2.MainRouteTableAssociation to associate a different route table as the VPC’s main route table.

Using a different cloud?

Explore vpc guides for other cloud providers: