AWS CDK Part 1: How to create a custom VPC

No Comments

Introduction

During a performance improvement project for one of our clients, we recently encountered ourselves with the task to code a completely new architectural setup based on an existing architecture. Terraform and Serverless framework are well-known tools for such a task. Yet we decided to tackle the architectural setup with AWS CDK as it provides the possibility to code your infrastructure within your TypeScript codebase. In this series of blog posts, we want to document our journey with AWS CDK, the problems we encountered and what eventually lead to resolving our struggle.

First of all, some details on our existing architecture: we have a few Lambdas in a default VPC that receive event triggers via an S3 endpoint, which then manipulate and store the event-related data into the RDS instance.

Our new architectural setup includes the same S3 endpoint combined with a step function orchestrating three lambdas, an RDS instance and a custom VPC with isolated subnets in only two Availability Zones.

AWS CDK VPC-Architecture

In this blog series, we will provide you with a step-by-step setup guide for the architecture shown in the figure above using TypeScript (>= 2.7). The topics are thematically divided as follows:

  • How to create a custom VPC
  • How to create S3 buckets
  • How to create an RDS instance
  • How to create lambdas
  • How to create a step function.
  • How to create a least privileged IAM CloudFormation user

We’ll start with the first topic on how to create a custom VPC.

Getting Started with AWS CDK:

(From this point forward we assume you are using a UNIX OS.)
This step-by-step approach will guide you through the steps required to setup your first custom VPC. First of all, install the AWS CDK using the following command:

npm install -g aws-cdk

Run the following command to see the version number of the AWS CDK (for this guide we used 1.7.0)

cdk --version

Now run the following commands to set up your sample app in TypeScript.

mkdir sampleCDK && cd sampleCDK && cdk init --language typescript

Let’s build our first stack by adapting/creating two files called ./bin/sample_cdk.ts

//sample_cdk.ts
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import {VpcStack} from "../lib/vpc-stack";

const app = new cdk.App();
new VpcStack(app, 'VpcStack')
app.synth();

and ./lib/vpc-stack.ts (feel free to use the generated stack sample_cdk_stack.ts if wanted):

//vpc-stack.ts
import {App, Stack, StackProps} from '@aws-cdk/core';
import {Peer, Port, SecurityGroup, SubnetType, Vpc} from '@aws-cdk/aws-ec2'

export class VpcStack extends Stack {
    readonly vpc: Vpc;
    readonly ingressSecurityGroup: SecurityGroup;
    readonly egressSecurityGroup: SecurityGroup;

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

        //Place resource definitions here.
    }
}

(To install the EC2 package run the command npm i @aws-cdk/aws-ec2)

The code in the file app.ts allows us to synthesize multiple stacks to one app, which will come in handy later on.

Stacks are essentially the units of deployment in CDK. All resources associated with that stack are eventually provisioned as a single unit. It is possible to add any number of stacks to a CDK app. For now we only assign our VPC stack to the app.
From this point on we will limit our focus on the code in file vpc-stack.ts. It currently contains the class skeleton for a CDK stack. In the following we will populate the three class fields vpc, ingressSecurityGroup and egressSecurityGroup with CDK constructs. Constructs are the basic building blocks of a CDK app and essentially represent a component at different levels inside AWS CloudFormation.

Let’s add our first resource to the stack. We want to create a VPC with an isolated subnet into which we can place our RDS instance later on. We chose to use an isolated subnet instead of a private one because it saves us the expense of a NAT-Gateway inside our VPC. Insert the following code into the constructor:

this.vpc = new Vpc(this, 'CustomVPC', {
    cidr: '10.0.0.0/16',
    maxAzs: 2,
    subnetConfiguration: [{
        cidrMask: 26,
        name: 'isolatedSubnet',
        subnetType: SubnetType.ISOLATED,
    }],
    natGateways: 0
});

The VPC resource constructor takes three parameters: the stack it should be added to (this), the ID for the resource and a collection of properties defined in the interface VpcProps. In case of our aimed for architecture it is important to define the above properties. Additionally, if you want to launch an RDS instance into your VPC, make sure to set the property maxAzs to >=2 since RDS instances require at least two subnets each in a different AZ.

We can now build our app for the first time by running the following commands from the project’s root directory sampleCdk.

npm run build && cdk synth

Before we show how to actually deploy this resource to your AWS account let us add two security groups to the VPC to secure our isolated subnet.

In order to create our first two security groups, add the following code snippet to the file vpc-stack.ts

this.ingressSecurityGroup = new SecurityGroup(this, 'ingress-security-group', {
    vpc: this.vpc,
    allowAllOutbound: false,
    securityGroupName: 'IngressSecurityGroup',
});
this.ingressSecurityGroup.addIngressRule(Peer.ipv4('10.0.0.0/16'), Port.tcp(3306));

this.egressSecurityGroup = new SecurityGroup(this, 'egress-security-group', {
    vpc: this.vpc,
    allowAllOutbound: false,
    securityGroupName: 'EgressSecurityGroup',
});
this.egressSecurityGroup.addEgressRule(Peer.anyIpv4(), Port.tcp(80));

Again the resource constructor SecurityGroup takes three arguments: the first being the stack, the second being the ID, and as a third parameter the SecurityGroupProps. In the case of ingress we want to assign the security group to our VPC and block all outbound traffic by default. We add an ingress rule allowing only local traffic from our VPC’s CIDR IPv4 range directed at our RDS instance port. In case of egress we allow all outbound TCP traffic on default port 80.

Final Build and Deploy

We can check our new resource additions for functionality by running our build step again

npm run build && cdk synth

If everything looks good we can actually start a deployment to our AWS environment. In order to do so, we need to add our AWS programmatic access credentials to our local AWS config file. Run

cd ~/.aws && nano config

and append the AWS profile of your choice to the file, looking as follows

[profile sample]
aws_access_key_id= SAMPLE_ACCESS_KEY_ID
aws_secret_access_key= SAMPLE_SECRET_ACCESS_KEY
region=eu-west-1

Remark: Some people might notice the difference to AWS CLI here, which accesses access credentials from the file ~c/.aws/credentials. We struggled quite a bit to get the –profile flag to work and the solution was to place the profile into the config file.

Once done, we can deploy our CDK stack by simply running the command

cdk deploy --profile sample

This command triggers the deployment of the generated CloudFormation template and executes it within the AWS Cloud. For now we are assuming that the executing IAM user linked to the access key ID has all the necessary rights to do so. We will further elaborate on how to configure a least privileged IAM user in Part 6: How to create a least privileged IAM CloudFormation user of this series. If you log in to your AWS console and navigate to the service CloudFormation, you should see your stack being built. You also have insight into events taking place during the build process when entering the stack details in the CloudFormation service.
Once completed, navigate to the service VPC within the region you chose during the config setup, and you will find the created custom VPC ready to be used for the harbouring of additional resources.

To sum things up, after playing around with a few settings inside the VPC’s configuration properties, we ended up with a custom VPC upon which we can build our desired architecture. The documentation on constructing a VPC from AWS is quite comprehensive, although it needs to be mentioned that we needed to do some back tracing until we got to our final architecture setup (there are some interdependencies in settings of properties which are not obvious at first). Some pitfalls like the amount of AZs or the size of the CIDR range in the VPC was something we fixed along the way. Another small issue was the realisation that profiles in AWS CDK work differently than they do in AWS CLI: they need to be defined in the config file instead of credentials file. Hopefully you managed to create your own custom VPC and can’t wait to get started on launching some resources into your new VPC. In the following article of this series on AWS CDK, we will elaborate on how to create S3 buckets and create an endpoint into your VPC.

Maik Kingma

Maik is a full stack developer, with a focus on backend and the Java / Spring environment. He also has experience in DevOps and is currently aspiring to gather knowledge as a software developer / software architect in the AWS Cloud.

Comment

Your email address will not be published. Required fields are marked *