VPC Example in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Tue May 04 20217 min read

Creating a VPC in AWS CDK #

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

In this article 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.

For the example in this article 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.

In order 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/aws-ec2'; import * as cdk from '@aws-cdk/core'; 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', { cidr: '10.0.0.0/16', natGateways: 1, maxAzs: 3, subnetConfiguration: [ { name: 'private-subnet-1', subnetType: ec2.SubnetType.PRIVATE, cidrMask: 24, }, { name: 'public-subnet-1', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, { name: 'isolated-subnet-1', subnetType: ec2.SubnetType.ISOLATED, cidrMask: 28, }, ], }); } }

In the code snippet we used the Vpc class to create a VPC resource.

The configuration props we passed to the construct are:

  • cidr - 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, that means no isolated subnets are provisioned by default.

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

  • PRIVATE 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 by able to access it on ports 80 and 443.

  • ISOLATED subnet - resources provisioned in 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 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 a public, private and isolated subnet groups, which makes up the 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 to 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 to 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 to 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 to 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 to the resources.

The easiest way to change the tags associated to 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/aws-ec2'; import * as cdk from '@aws-cdk/core'; 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 }`, ), ); } } }

In the code snippet 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, for 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 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, execute the destroy command:

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

Further Reading #

Add me on LinkedIn

I'm a Web Developer with TypeScript, React.js, Node.js and AWS experience.

Let's connect on LinkedIn

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee