AWS VPC Subnet Groups

The L2 VPC cdk construct accepts a list of Subnet Groups. This post explains how subnet groups work, and the reason for some defaults.

AWS VPC Subnet Groups

The L2 VPC cdk construct accepts a list of Subnet Groups in the subnetConfiguration property. Subnet Groups only seem to be documented in the context of an Elasticache cluster, so I'll provide a quick breakdown of how they work in the context of VPCs:

  1. Subnets are assigned CIDR blocks in the order they are defined
  2. Subnet groups are deployed to every AZ
  3. You can "Reserve" subnet blocks without deploying a subnet resource

For reference, a Subnet Group (configured using a SubnetConfiguration instance) has the following configurable properties:

Property Description
name string
subnet_type valid values: PRIVATE_ISOLATED, PRIVATE_WITH_NAT, PUBLIC
cidr_mask valid values: 16-28
map_public_ip_on_launch true by default for public subnets
reserved false by default. more details below

Subnets are assigned CIDR blocks in the order they are defined

Every Subnet Group entry in the subnetConfiguration list is assigned a CIDR block based on the VPC cidr property and the subnet group cidrMask property.

Let's say you have a VPC (cidr:10.0.0.0/16) with a single AZ with 3 entries (cidrMask:24) in the subnetConfiguration list:

const vpc = new Vpc(this, 'lambda-vpc', {
    'cidr': "10.0.0.0/16",
    'maxAzs': 1,
    'subnetConfiguration': [{
        cidrMask: 24,
        name: 'the-shy-one',
        subnetType: SubnetType.PRIVATE_ISOLATED,
    },
    {
        cidrMask: 24,
        name: 'the-cute-one',
        subnetType: SubnetType.PRIVATE_WITH_NAT
    },
    {
        cidrMask: 24,
        name: 'the-rebel',
        subnetType: SubnetType.PUBLIC
    }
    ],
    'vpcName': 'generic-boy-band'
})

This VPC will have 3 subnets with the following blocks: 10.0.0.0/24, 10.0.0.1/24 and 10.0.0.2/24.

If the VPC has 2 AZs instead, there will be 2 blocks per subnet group - 10.0.0.0/24 and 10.0.0.1/24 for the first subnet group, 10.0.0.2/24 and 10.0.0.3/24 for the second subnet group and so on.

Trying to add a new subnet group entry in the middle of the configuration list after the initial deployment will not work. For example, if we modify the example above:

const vpc = new Vpc(this, 'lambda-vpc', {
    'cidr': "10.0.0.0/16",
    'maxAzs': 1,
    'subnetConfiguration': [{
        cidrMask: 24,
        name: 'the-shy-one',
        subnetType: SubnetType.PRIVATE_ISOLATED,
    },
    {
        cidrMask: 24,
        name: 'the-cute-one',
        subnetType: SubnetType.PRIVATE_WITH_NAT
    },
    // NEW ENTRY
    {
        cidrMask: 24,
        name: 'the-copy-cat',
        subnetType: SubnetType.PRIVATE_WITH_NAT
    },
    {
        cidrMask: 24,
        name: 'the-rebel',
        subnetType: SubnetType.PUBLIC
    }
    ],
    'vpcName': 'generic-boy-band'
})
updated VPC config

It will fail with the error:

Resource handler returned message: "The CIDR [..] conflicts with another subnet"

Subnet groups are deployed to every AZ

Every Subnet Group entry in the subnetConfiguration list creates a subnet per AZ in the VPC. There is no way to specify different AZs for different subnet groups, nor can you limit a subnet to a single AZ. For example, say your VPC is configured with 2 AZs. You can't have SUBNET-GROUP-1 subnets deployed in us-west-2a and us-west-2b, and SUBNET-GROUP-2 deployed in us-west-2b only, at least with the L2 VPC construct.

This seemed odd to me, so I posted a question about it on the AWS subreddit. See the discussion in that thread for more details, but here's why I am convinced this is a good default:

AZ-aware AWS resources allow specifying the number of AZs to deploy resources into. For example, you can configure an EC2 auto-scaling group to only deploy 2 instances, even if you have 3 AZs available. If I'm creating a VPC with multiple AZs, it means I anticipate needing higher availability guarantees, even if I don't need it immediately for all my applications. Thus, the default subnet group configuration ensures I have that reserved address space whenever I choose to start using the additional AZs (and it does not cost anything).

This also ensures all subnets in a subnet group form one contiguous block of CIDR address ranges. This simplifies rules for similar subnets. This page has some examples that show how contiguous blocks can be useful.

"Reserved" subnet blocks

Subnet Group configurations also provide a reserved boolean property. Read a detailed description here, but this property essentially allows you to block certain CIDR blocks without actually creating the subnet resource.

Example: I have a VPC configured with a single AZ. There are 2 kinds of subnets in this VPC, "application" and "database" subnets. All resources in the "application" subnet will have similar access, which will differ from the access granted to resources in the "database" subnet. I expect to eventually need 5 "application" subnets and 2 "database" subnets, but I only need 2 "application" subnets and a single "database" subnet today.

Note: Remember CIDR blocks are allocated in the order you define subnet groups, and you cannot add new groups in the middle of the configuration list.

Option 1: Define 3 subnet groups corresponding to the 3 subnets I need today. Add new subnet group entries as needed.

const vpc = new Vpc(this, 'lambda-vpc', {
    'cidr': "10.0.0.0/16",
    'maxAzs': 1,
    'subnetConfiguration': [{
        cidrMask: 24,
        name: 'application-1',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
    },
    {
        cidrMask: 24,
        name: 'application-2',
        subnetType: SubnetType.PRIVATE_WITH_NAT
    },
    {
        cidrMask: 24,
        name: 'database-1',
        subnetType: SubnetType.PRIVATE
    }],
    'vpcName': 'fake-org'
})

Behavior: Each new subnet group will block a new CIDR block. "Application" and "database" subnets will be interspersed, making rules and configurations based on IP address ranges difficult to manage.

Option 2: Define 5 "application" subnet groups and 2 "database" subnet groups in order today. The 3 subnet groups I need today will have the reserved property set to false (default). The others will have it set to true.

const vpc = new Vpc(this, 'lambda-vpc', {
    'cidr': "10.0.0.0/16",
    'maxAzs': 1,
    'subnetConfiguration': [{
        cidrMask: 24,
        name: 'application-1',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
    },
    {
        cidrMask: 24,
        name: 'application-2',
        subnetType: SubnetType.PRIVATE_WITH_NAT
    },
    {
        cidrMask: 24,
        name: 'application-3',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
        reserved: true
    },
    {
        cidrMask: 24,
        name: 'application-4',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
        reserved: true
    },
    {
        cidrMask: 24,
        name: 'application-5',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
        reserved: true
    },
    {
        cidrMask: 24,
        name: 'database-1',
        subnetType: SubnetType.PRIVATE
    },
    {
        cidrMask: 24,
        name: 'database-2',
        subnetType: SubnetType.PRIVATE,
        reserved: true
    }],
    'vpcName': 'fake-org'
})

Behavior: "Application" and "database" subnets will have their own IP address ranges. There are 4 subnet CIDR ranges that have been reserved, but have no associated resources. Over time, the 4 reserved subnets can be deployed as subnet resources.


That's it for VPC subnet groups. Look for a post soon on how to deploy a VPC with security best practices using CDK.

As always, you can reach me @rohchak