Introduction

In this post, we’ll create a simple, custom VPC Network using AWS CDK. With this stack, we’ll set up a network with three subnets each for different purposes.

Since these posts are focused on practicality, I won’t talk about best practices, etc. as you’ll find better resources about these subjects and details in AWS Documentation. See Useful Resources at the end of the page.

Let’s start with opening a directory and creating our CDK application.

# Let's create a folder for our CDK application.
mkdir -p practical-cdk/custom-vpc && cd $_

# Initialize the CDK application.
npx aws-cdk@2.x init app --language typescript

It’s totally optional, but let’s commit this initial state.

git add .
git commit -m 'Initial commit for custom-vpc-stack'

Now we can start writing our constructor.

VPC Network

As usual, let’s import the necessary libraries in ./lib/custom-vpc-stack.ts and set the class attributes.

// ...

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';

export class CustomVpcStack extends Stack {
  customVPC: ec2.IVpc;

  customVPCPublicSubnetIds: string[] = [];
  customVPCPrivateSubnetIds: string[] = [];
  customVPCIsolatedSubnetIds: string[] = [];

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
  }
}

Constructors

Following constructors as a whole should be in the CustomVpcStack.

////////////////////////////////////////////////////////////////////////////////////////////////////
//
// VPC Network
//
// Notes: For CIDR block, the recommended private IPv4 address ranges
//        are specified in RFC 1918.
//
//        One thing to be careful about is that if you plan to peer
//        this VPC with "another" VPC, the said VPCs' CIDR block ranges
//        should not overlap with each other.
//
//        Also, the subnets in a particular VPC, cannot have overlapping
//        CIDR block ranges with each other.
//
this.customVPC = new ec2.Vpc(this, `custom-vpc`, {
  vpcName: `custom-vpc`,
  cidr: '10.1.0.0/16',
  natGateways: 1,
  maxAzs: 3,
  subnetConfiguration: [
    {
      name: 'public-subnet-1',
      subnetType: ec2.SubnetType.PUBLIC,
      // 4096 IP Addresses
      cidrMask: 20
    },
    {
      name: 'private-subnet-1',
      subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
      // 4096 IP Addresses
      cidrMask: 20
    },
    {
      name: 'isolated-subnet-1',
      subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      // 256 IP Addresses
      cidrMask: 24
    }
  ]
});

////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Subnet Tagging and Exports
//
// Notes: We are changing the names of our subnets to have them in a
//        somewhat convenient convention. We're also exporting our
//        subnet ids for each category of subnets to fetch them easily
//        over SSM when we need them; e.g. in a serverless.yaml file.
//
//        The names will result in the format of:
//          custom-vpc-public-subnet-1-us-west-2a
//          custom-vpc-private-subnet-1-us-west-2a
//          custom-vpc-isolated-subnet-1-us-west-2a
//
for (const subnet of this.customVPC.publicSubnets) {
  cdk.Aspects.of(subnet).add(
    new cdk.Tag(
      'Name',
      `${this.customVPC.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${subnet.availabilityZone}`
    )
  );

  this.customVPCPublicSubnetIds.push(subnet.subnetId);
};
new ssm.StringListParameter(this, `custom-ssm-public-subnets`, {
  parameterName: `/practical-cdk/custom-vpc/public.subnet.ids`,
  stringListValue: this.customVPCPublicSubnetIds,
});

for (const subnet of this.customVPC.privateSubnets) {
  cdk.Aspects.of(subnet).add(
    new cdk.Tag(
      'Name',
      `${this.customVPC.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${subnet.availabilityZone}`
    )
  );

  this.customVPCPrivateSubnetIds.push(subnet.subnetId);
};
new ssm.StringListParameter(this, `custom-ssm-private-subnets`, {
  parameterName: `/practical-cdk/custom-vpc/private.subnet.ids`,
  stringListValue: this.customVPCPrivateSubnetIds,
});

for (const subnet of this.customVPC.isolatedSubnets) {
  cdk.Aspects.of(subnet).add(
    new cdk.Tag(
      'Name',
      `${this.customVPC.node.id}-${subnet.node.id.replace(/Subnet[0-9]$/, '')}-${subnet.availabilityZone}`
    )
  );

  this.customVPCIsolatedSubnetIds.push(subnet.subnetId);
};
new ssm.StringListParameter(this, `custom-ssm-isolated-subnets`, {
  parameterName: `/practical-cdk/custom-vpc/isolated.subnet.ids`,
  stringListValue: this.customVPCIsolatedSubnetIds,
});

As a final step, we should uncomment the env prop in the CustomVpcStack definition. Open ./bin/custom-vpc.ts and change the stack constructor with the following code.

new CustomVpcStack(app, 'CustomVpcStack', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Deployment

Let’s check if we missed something by running the synthesizer.

npm run cdk synth

If the CloudFormation template is created without any problem, we are ready to apply our stack.

npm run cdk bootstrap   # If it's the first time you are deploying a CDK application
npm run cdk deploy

Check the changes and approve them if it looks OK. Once it’s finished, you should be ready to use and deploy resources into your private network.

VPC Peering

VPC Peering, as the name suggests, is connecting two VPCs and leting the resources within these networks access each other as if they are in the same network. This is quite useful in many scenarios. One of the big advantages is that it’s possible to peer with a network that’s in another AWS account. The main rule is to make sure that these networks don’t have overlapping CIDR blocks.

The AWS documentation on this subject is really thorough, so I’ll cut my comments short. However, since this is a blog post related to AWS CDK, it would be amiss if we don’t give an example. Unfortunately, an L2 constructor for this purpose isn’t available yet. So, we’re going to use the raw CloudFormation constructor.

To peer two networks with VPC, one should make sure that the networks are created and ready beforehand. Another thing to make sure of is that you have to peer the subnets as well and it should be done both ways.

The following code is just an example and won’t be used in the application we’ve created above. Of course, you could experiment with that yourself.

////////////////////////////////////////////////////////////////////////////////
//
// VPC Peering
//
this.vpcPeering = new ec2.CfnVPCPeeringConnection(this, `peering-two-vpcs`, {
    vpcId: this.firstVPC.vpcId,
    peerVpcId: this.secondVPC.vpcId,
});

////////////////////////////////////////////////////////////////////////////////
//
// Subnet Peerings
//
//
// firstVPC to secondVPC
this.firstVPC.publicSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `public-subnets-of-first-to-second-${index}`, {
        destinationCidrBlock: this.secondVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

this.firstVPC.privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `private-subnets-of-first-to-second-${index}`, {
        destinationCidrBlock: this.secondVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

this.firstVPC.isolatedSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `isolated-subnets-of-first-to-second-${index}`, {
        destinationCidrBlock: this.secondVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

// secondVPC to firstVPC
this.secondVPC.publicSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `public-subnets-of-second-to-first-${index}`, {
        destinationCidrBlock: this.firstVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

this.secondVPC.privateSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `private-subnets-of-second-to-first-${index}`, {
        destinationCidrBlock: this.firstVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

this.secondVPC.isolatedSubnets.forEach(({ routeTable: { routeTableId } }, index) => {
    new ec2.CfnRoute(this, `isolated-subnets-of-second-to-first-${index}`, {
        destinationCidrBlock: this.firstVPC.vpcCidrBlock,
        routeTableId,
        vpcPeeringConnectionId: this.vpcPeering.ref,
    });
});

Clean Up

To clean up what we’ve done, we can simply run cdk destroy and approve. It’ll take care of everything.

npm run cdk destroy

Useful Resources