Setting up an AWS VPC using CDKTF

Stevosjt
6 min readDec 29, 2023

--

This article is to demonstrate a method of setting up an AWS VPC with proper subnets and routing. By the end of this, you should be able to deploy an AWS recommended VPC setup including subnets, routing and NAT gateways. This is an opinionated approach to setting this up. If you are new to CDKTF and need help getting started, you can visit my other article which will get you through how to get started with the CDKTF here. https://medium.com/@stevosjt88/getting-started-with-cdktf-and-aws-4372aaaa9bf8

This is the rough architecture of what we are trying to deploy.

To start we need to ensure we have the CDKTF installed. You can do that by running “npm install -g cdktf-cli”. From there you just need to run “cdktf init” and follow the prompts to build an out of the box cdktf application. In this demo, we will be using typescript.

We will start by creating a new typescript file called network.ts. This is where we will build everything that is related to the networking. We will then update the main.ts to load the AWS provider and call the network class that we will create in the network.ts file.

Our main.ts file should look like this.

import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Network } from "./network";

export interface Creds {
accessKey: string;
secretKey: string;
region: string;
}

const creds: Creds = {
accessKey: '',
secretKey: '',
region: 'us-east-1',
}

const app = new App();
new Network(app, creds);
app.synth();

And our network.ts file should look like this.

import { Construct } from "constructs";

export class Network extends TerraformStack {
public readonly vpcId: string;
public readonly publicSubnetIds: string[];
public readonly privateSubnetIds: string[];

constructor(app: App, creds: Creds) {
super(app, 'network');

new AwsProvider(this, 'aws', creds);
}
}

It is important to note that this is creating a new stack called “network” that will be its own state file. Also note that we are creating public readonly values. This will make those values accessible to other stacks for future use.

Now time to put some code in our network.ts file. We first need to decide the subnets that we will use to deploy this. I find it best to put these at the top.

const vpcCidr = '10.0.0.0/20'
const publicSubnets = ['10.0.0.0/25', '10.0.0.128/25', '10.0.1.0/25']
const privateSubnets = ['10.0.2.0/23', '10.0.4.0/23', '10.0.6.0/23']

From here, we can build the VPC

const vpc = new Vpc(this, 'vpc', {
cidrBlock: vpcCidr
});
this.vpcId = vpc.id

Next we need to build the subnets. For this we are going to create a function. We need to ensure that each subnet is assigned to a different availability zone in AWS. The code below takes an input of vpcId, the subnet cidr blocks, and the subnet type (public or private). From there, it will build all the subnets and return back the Ids that are needed.

  private buildSubnets(vpcId: string, subnets: string[], subnetType: string): string[] {
const subnetIds: string[] = [];

let count = 0

subnets.forEach((subnet) => {
count++;
const s = new Subnet(this, `${subnetType}${count}`, {
vpcId: vpcId,
cidrBlock: subnet,
availabilityZone: `us-east-1${(count + 9).toString(36)}`,
tags: {
Name: `${subnetType}${count}`,
}
});
subnetIds.push(s.id);
});

return subnetIds;
}

Now we can call this from the constructor in the network.ts file by doing the following.

this.privateSubnetIds = this.buildSubnets(vpc.id, privateSubnets, 'private');
this.publicSubnetIds = this.buildSubnets(vpc.id, publicSubnets, 'public');

Now this will build a vpc, 3 public subnets, and 3 private subnets. Now we need to hook up all the routing, create the NAT gateways and create the internet gateway.

The internet gateway allows resources deployed to public subnets to reach the internet (if they are given a public IP). The NAT gateways allow resources deployed in private subnets to reach.

To build this, I decided to create another function as we will need access to both public and private subnets.

private buildGatewaysAndRoutes(publicSubnetIds: string[], privateSubnetIds: string[], vpcId: string) {
const gateway = new InternetGateway(this, 'internet-gateway', {
vpcId: vpcId,
});

const publicRt = new RouteTable(this, 'route-table-public', {
vpcId: vpcId,
route: [
{
cidrBlock: '0.0.0.0/0',
gatewayId: gateway.id,
},
],
});

let count = 0;
publicSubnetIds.forEach((id) => {
count++;
const eip = new Eip(this, `eip-${count}`, {
vpc: true,
});

const gw = new NatGateway(this, `nat-gateway-${count}`, {
allocationId: eip.allocationId,
subnetId: id,
});

new RouteTableAssociation(this, `route-table-association-public-${count}`, {
routeTableId: publicRt.id,
subnetId: id,
});

// Private Route Table
const rt = new RouteTable(this, `route-table-private-${count}`, {
vpcId: vpcId,
route: [
{
cidrBlock: '0.0.0.0/0',
natGatewayId: gw.id,
},
],
});


new RouteTableAssociation(this, `route-table-association-private-${count}`, {
routeTableId: rt.id,
subnetId: privateSubnetIds[count - 1],
});
});
}

Just to review what is happening above, we first build the Internet Gateway. We then build a single public route that routes all traffic to the Internet Gateway we just created. We then loop through all the public subnet ids that were provided and create the following

  1. Elastic IP
  2. NatGateway which uses the elastic ip we created
  3. Route Table Association which maps the public subnet id to the public route.
  4. A new route table for the private subnet that maps to the NAT gateway we just built.
  5. Route Table Association which maps thte private subnet id to the newely created private route.

Then we should be able to call this function.

this.buildGatewaysAndRoutes(publicSubnetIds, privateSubnetIds, vpc.id);

So just to recap, the network.ts file looks like this.

import { Subnet } from "@cdktf/provider-aws/lib/subnet";
import { App, TerraformStack } from "cdktf";
import { Creds } from "./main";
import { AwsProvider } from "@cdktf/provider-aws/lib/provider";
import { Vpc } from "@cdktf/provider-aws/lib/vpc";
import { InternetGateway } from "@cdktf/provider-aws/lib/internet-gateway";
import { RouteTable } from "@cdktf/provider-aws/lib/route-table";
import { Eip } from "@cdktf/provider-aws/lib/eip";
import { NatGateway } from "@cdktf/provider-aws/lib/nat-gateway";
import { RouteTableAssociation } from "@cdktf/provider-aws/lib/route-table-association";

export class Network extends TerraformStack {

public readonly vpcId: string;
public readonly publicSubnetIds: string[];
public readonly privateSubnetIds: string[];

constructor(app: App, creds: Creds) {
super(app, "network");

new AwsProvider(this, "aws", creds);

const vpcCidr = "10.0.0.0/20";
const publicSubnets = ["10.0.0.0/25", "10.0.0.128/25", "10.0.1.0/25"];
const privateSubnets = ["10.0.2.0/23", "10.0.4.0/23", "10.0.6.0/23"];

const vpc = new Vpc(this, "vpc", {
cidrBlock: vpcCidr,
});

this.vpcId = vpc.id;
this.privateSubnetIds = this.buildSubnets(
vpc.id,
privateSubnets,
"private"
);
this.publicSubnetIds = this.buildSubnets(vpc.id, publicSubnets, "public");
this.buildGatewaysAndRoutes(this.publicSubnetIds, this.privateSubnetIds, vpc.id);
}

private buildSubnets(
vpcId: string,
subnets: string[],
subnetType: string
): string[] {
const subnetIds: string[] = [];

let count = 0;

subnets.forEach((subnet) => {
count++;
const s = new Subnet(this, `${subnetType}${count}`, {
vpcId: vpcId,
cidrBlock: subnet,
availabilityZone: `us-east-1${(count + 9).toString(36)}`,
tags: {
Name: `${subnetType}${count}`,
},
});
subnetIds.push(s.id);
});

return subnetIds;
}

private buildGatewaysAndRoutes(
publicSubnetIds: string[],
privateSubnetIds: string[],
vpcId: string
) {
const gateway = new InternetGateway(this, "internet-gateway", {
vpcId: vpcId,
});

const publicRt = new RouteTable(this, "route-table-public", {
vpcId: vpcId,
route: [
{
cidrBlock: "0.0.0.0/0",
gatewayId: gateway.id,
},
],
});

let count = 0;
publicSubnetIds.forEach((id) => {
count++;
const eip = new Eip(this, `eip-${count}`, {
vpc: true,
});

const gw = new NatGateway(this, `nat-gateway-${count}`, {
allocationId: eip.allocationId,
subnetId: id,
});

new RouteTableAssociation(
this,
`route-table-association-public-${count}`,
{
routeTableId: publicRt.id,
subnetId: id,
}
);

// Private Route Table
const rt = new RouteTable(this, `route-table-private-${count}`, {
vpcId: vpcId,
route: [
{
cidrBlock: "0.0.0.0/0",
natGatewayId: gw.id,
},
],
});

new RouteTableAssociation(
this,
`route-table-association-private-${count}`,
{
routeTableId: rt.id,
subnetId: privateSubnetIds[count - 1],
}
);
});
}
}

And that is it. You can now run “cdktf deploy” (given that you provided your own credentials for your AWS account) and it will create all the networking needed to start running services on a network in AWS.

Conclusion

In this tutorial we explored a way to create the networking in AWS using CDKTF. This is just one opinionated way to set up the environment. Please explore and figure out what works best for you when setting up your environments when running in AWS. If interested in EKS and how to set it up using CDKTF, please see my next article here. https://medium.com/@stevosjt88/creating-an-eks-cluster-using-cdktf-ed6cf28599c9

--

--