Create and Configure AWS VPCs

The aws:ec2/vpc:Vpc resource, part of the Pulumi AWS provider, provisions the VPC network container itself: its CIDR block, DNS settings, and instance tenancy. This guide focuses on two capabilities: CIDR block allocation (explicit and IPAM-managed) and tagging with instance tenancy configuration.

VPCs are foundational network containers. Subnets, route tables, security groups, and other networking resources are created separately and reference the VPC. The examples are intentionally small. Combine them with your own subnet, routing, and security group 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 private IPv4 address range using CIDR notation. Common choices include 10.0.0.0/16 (65,536 addresses) or 172.16.0.0/12 (1,048,576 addresses). This range determines how many subnets and instances you can create within the VPC.

Add tags and instance tenancy settings

Production VPCs typically include organizational tags for cost tracking and resource identification, along with tenancy settings that control EC2 instance placement.

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 tags property adds key-value metadata for organization and cost allocation. The instanceTenancy property controls whether EC2 instances run on shared hardware (default) or dedicated hardware (dedicated). Dedicated tenancy incurs a $2 per hour regional fee plus per-instance charges, regardless of instance-level tenancy settings.

Allocate CIDR blocks from AWS IPAM

Organizations managing IP addresses across multiple accounts and regions use IPAM (IP Address Manager) 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, you reference an IPAM pool via ipv4IpamPoolId and request a netmask length via ipv4NetmaskLength. IPAM allocates a CIDR block from the pool automatically, ensuring no conflicts across your organization. The dependsOn ensures the pool has allocated CIDR ranges before VPC creation.

Beyond these examples

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

The IPAM example references pre-existing infrastructure such as IPAM pools with allocated CIDR ranges. It focuses on configuring the VPC rather than provisioning the complete networking stack.

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

  • DNS settings (enableDnsSupport, enableDnsHostnames)
  • IPv6 configuration (assignGeneratedIpv6CidrBlock, ipv6IpamPoolId)
  • 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 properties can't I change after creating a VPC?
The cidrBlock, ipv4IpamPoolId, and ipv4NetmaskLength properties are immutable and cannot be modified after VPC creation.
How do I use IPAM to allocate my VPC's CIDR block?
Set ipv4IpamPoolId and ipv4NetmaskLength, then add a dependsOn for the aws.ec2.VpcIpamPoolCidr resource as shown in the IPAM example.
What are my options for enabling IPv6 on a VPC?
You have three options: use assignGeneratedIpv6CidrBlock for an Amazon-provided /56 block, ipv6IpamPoolId with ipv6NetmaskLength for IPAM allocation, or ipv6CidrBlock for an explicit CIDR. Note that assignGeneratedIpv6CidrBlock conflicts with ipv6IpamPoolId, and ipv6NetmaskLength conflicts with ipv6CidrBlock.
What's the valid range for ipv6NetmaskLength?
Valid values are 44 to 60 in increments of 4 (44, 48, 52, 56, 60).
Instance Tenancy & Costs
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.
What's the difference between default and dedicated instance tenancy?
With default tenancy, instances use the tenancy attribute specified at launch. With dedicated tenancy, all instances launched in the VPC run on dedicated hardware regardless of the launch-time attribute.
DNS Configuration
What are the DNS defaults for a VPC?
DNS support (enableDnsSupport) defaults to true, while DNS hostnames (enableDnsHostnames) defaults to false.
Advanced Configuration
How do I restrict IPv6 address advertisement to Local Zones?
Set ipv6CidrBlockNetworkBorderGroup to specify a Network Border Group such as a Local Zone. By default, it’s set to the VPC’s region.
What default resources are created with a VPC?
AWS automatically creates a default network ACL (defaultNetworkAclId), default route table (defaultRouteTableId), and default security group (defaultSecurityGroupId) when you create a VPC.

Using a different cloud?

Explore vpc guides for other cloud providers: