VPC Example in AWS CDK - Complete Guide

avatar
Borislav Hadzhiev

Last updated: Jan 26, 2024
7 min

banner

# Creating a VPC in AWS CDK

A VPC is a virtual private network that is isolated from other AWS customers.

We are going to cover how to create and configure a VPC in CDK, what the defaults are, and general things you should be aware of when provisioning VPCs in CDK.

We are going to provision 1 NAT Gateway. NAT Gateways have an hourly billing rate of about $0.045 in the us-east-1 region.

To create a VPC in AWS CDK, we have to instantiate and configure the Vpc class.

The code for this article is available on GitHub

Let's look at an example of creating a VPC in CDK:

lib/cdk-starter-stack.ts
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, 'my-cdk-vpc', { ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), natGateways: 1, maxAzs: 3, subnetConfiguration: [ { name: 'private-subnet-1', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24, }, { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, { name: 'isolated-subnet-1', subnetType: ec2.SubnetType.PRIVATE_ISOLATED, cidrMask: 28, }, ], }); } }

We used the Vpc class to create a VPC resource.

The configuration props we passed to the construct are:

  • ipAddresses - the CIDR block of the VPC. Must be between /16 (65536 IP Addresses) and /28 (16 IP addresses). The default value for a CIDR range is 10.0.0.0/16.

  • natGateways - how many NAT gateways should be created for the VPC. By default, one NAT Gateway is created for each availability zone. When testing you should lower the number of natGateways to 1 to avoid burning money.

    To use NAT instances instead of NAT gateways, we can set the natGatewayProvider prop to ec2.NatProvider.instance:

nat-instance-example
const vpc = new ec2.Vpc(this, 'my-cdk-vpc', { natGatewayProvider: ec2.NatProvider.instance({ instanceType: new ec2.InstanceType('t2.micro'), }) })
  • maxAzs - the maximum number of availability zones to use in the region. By default 3 availability zones are used for stacks, where we explicitly set the environment (account and region) and 2 availability zones are used for environment-agnostic stacks.

    There are regions that currently have only 2 availability zones, so the safe bet for CDK is to cap us at 2 AZs for environment-agnostic stacks.

  • subnetConfiguration - specifies the subnets that will be created in each availability zone. In our case, we have provisioned 3 subnet groups - 1 private, 1 public, 1 isolated.

    Because we have configured the VPC to use a maximum of 3 availability zones, each subnet group will create 1 subnet in each availability zone = 9 total subnets.

    The default subnetConfiguration is that the VPC CIDR block is divided between 1 public and 1 private subnet per Availability Zone, which means no isolated subnets are provisioned by default.

Let's look at the difference between the 3 types of subnets:

  • PRIVATE_WITH_EGRESS subnet - resources provisioned in PRIVATE subnets have access to the internet via a NAT Gateway, however, they can't be accessed from the internet.

    We created a private subnet group and our VPC is configured for a maximum of 3 availability zones, therefore we are going to create 3 private subnets - 1 in each availability zone.

    Because we've set the natGateways prop to 1, we will only create 1 NAT gateway, so for 2 of the private subnets, we will use a NAT gateway from another availability zone.

    Private subnets are often used for our backend servers, where we need to download patches and updates from the internet, but we don't want the instances we provision to be directly accessible from the internet.

  • PUBLIC subnet - resources provisioned in PUBLIC subnets have access to the internet via an Internet Gateway and can be accessed from the internet as long as they have a public IP address.

    Public subnets are often used for a webserver, where we need the whole world to be able to access it on ports 80 and 443.

  • PRIVATE_ISOLATED subnet - resources provisioned in PRIVATE_ISOLATED subnets don't have access to the internet and can't be accessed from the internet. They can only access and be accessed, from instances in the same VPC.

    Isolated subnets are often used for databases.

Let's deploy the resources and see what we provisioned:

shell
npx aws-cdk deploy

If we take a look at the CloudFormation management console, it shows that we provisioned 39 resources with our ~20 lines of VPC definition.

vpc cloudformation resources

Let's look at the VPC components we provisioned.

  1. The VPC:

vpc

From the screenshot we can see the naming convention for the VPC is that the stack name is also included. We are going to see how we can update the name by changing the tag of our VPC later in the article.

  1. The subnets:

vpc subnets

We used a maximum of 3 AZs for the VPC. For each AZ we created public, private and isolated subnet groups. This makes up a total of 9 subnets.

My default region is eu-central-1 and it has 3 AZs, your results will differ if your default region only has 2 AZs.

The names of the subnets are quite confusing and verbose, we are going to update them by changing the tag associated with each subnet later in the article.

  1. The Route tables:

vpc route tables

We created a total of 10 route tables - 1 route table for each of the 9 subnets, and 1 default route table.

The default routes for a route table associated with a PUBLIC subnet are configured to route traffic to the internet through an Internet Gateway:

DestinationTargetStatusPropagated
10.0.0.0/16localactiveNo
0.0.0.0/0IGW-IDactiveNo

The default routes for a route table associated with a PRIVATE subnet are configured to route traffic to the internet through a NAT Gateway:

DestinationTargetStatusPropagated
10.0.0.0/16localactiveNo
0.0.0.0/0NATGW-IDactiveNo

The default routes for a route table associated with an ISOLATED subnet are to only route traffic to resources inside the VPC:

DestinationTargetStatusPropagated
10.0.0.0/16localactiveNo
  1. The Internet gateway:

vpc internet gateway

The internet gateway we provisioned, for our public subnets, has been attached to the VPC.

  1. The NAT Gateway:

vpc nat gateway

A NAT Gateway with an elastic IP has been provisioned to route traffic to the internet from our private subnets.

  1. The Network Access Control Lists (NACLs):

vpc nacl

A default NACL has been associated with all 9 of our subnets. As expected, by default the inbound and outbound rules allow all traffic.

  1. The Security Groups:

vpc security groups

The Inbound rules for the default security group allow all traffic, that comes from resources within the same security group, on all ports:

TypeProtocolPort rangeSource
All trafficAllAllSG-ID

The Outbound rules for the default security group allow all traffic to all destinations, on all ports:

TypeProtocolPort rangeDestination
All trafficAllAll0.0.0.0/0

Next, let's look at how we can change the names of the VPC components we provisioned.

# Changing Names of VPC Components in AWS CDK

In order to change the names of VPC components in AWS CDK, we have to change the Name tag associated with the resources.

The easiest way to change the tags associated with all constructs within a given scope is to use CDK Aspects.

The code for this article is available on GitHub

Let's look at an example of how we would use aspects to change the tags of our VPC and subnets:

lib/cdk-starter-stack.ts
import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐Ÿ‘‡ update the Name tag for the VPC cdk.Aspects.of(vpc).add(new cdk.Tag('Name', 'my-cdk-vpc')); // ๐Ÿ‘‡ update the Name tag for public subnets for (const subnet of vpc.publicSubnets) { cdk.Aspects.of(subnet).add( new cdk.Tag( 'Name', `${vpc.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${ subnet.availabilityZone }`, ), ); } // ๐Ÿ‘‡ update the Name tag for private subnets for (const subnet of vpc.privateSubnets) { cdk.Aspects.of(subnet).add( new cdk.Tag( 'Name', `${vpc.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${ subnet.availabilityZone }`, ), ); } // ๐Ÿ‘‡ update the Name tag for private subnets for (const subnet of vpc.isolatedSubnets) { cdk.Aspects.of(subnet).add( new cdk.Tag( 'Name', `${vpc.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${ subnet.availabilityZone }`, ), ); } } }

We used CDK Aspects to update the Name tag of our VPC components.

To remove some of the duplication, you could extract the logic in a function.

Here is an example.

function tagSubnets(subnets: ec2.ISubnet[], tagName: string, tagValue: string) { for (const subnet of subnets) { cdk.Aspects.of(subnet).add(new cdk.Tag(tagName, tagValue)); } } tagSubnets(vpc.privateSubnets, 'Name', `your-private-subnet-name`); tagSubnets(vpc.publicSubnets, 'Name', `your-public-subnet-name`);

The expected name of the VPC after the change is my-cdk-vpc, and the expected name for a public subnet is my-cdk-vpc-public-subnet-1-us-east-1a, etc.

Let's deploy the changes:

shell
npx aws-cdk deploy

After the Name tag has been updated, the VPC name is easier to read:

vpc with name tag

The same counts for our subnets, which now include the availability zone in the subnet name:

vpc subnets with name tag

Since each subnet is associated with a route table, the subnet name tags have been applied to the route tables as well:

route tables with name tag

# Clean up

To delete the resources we have provisioned, issue the destroy command:

shell
npx aws-cdk destroy
Don't forget to destroy the stack because we provisioned a NAT Gateway which has an hourly billing rate.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev