Upgrade Your AWS Infrastructure Using AWS CDK

Today, describing infrastructure as code is more important than ever to control all the cloud resources your systems are using. Typical tools like AWS CloudFormation or Terraform can help you deploying such resources. However, these tools have their limitations because they are using descriptive languages like JSON, YAML, or flavours of it. The AWS Cloud Development Kit (CDK) tries to solve these disadvantages by letting engineers write their infrastructure using programming languages like TypeScript, Python, Java, or C#. This blog post explains how you can upgrade your AWS infrastructure using AWS CDK and get started with some examples.

Infrastructure as Code

Using cloud providers like AWS becomes more and more popular these days. With a huge collection of services, they offer any kind of organization to publish their solutions to millions of people worldwide. Of course, the main advantage is that organizations don’t have to reinvent the wheel and build up on AWS’ solutions. However, as soon as you start using these services, managing them becomes crucial for each team and organization. Teams and organizations strive to adopt services quickly and reliably while considering costs, security, best practices, and other factors.

Infrastructure as Code (IaC) tools like AWS CloudFormation or Terraform reduce this overhead of resource management a lot. They provide a syntax that can be used in template files to describe your system’s cloud resources. For example, you create a template file and define that you want to use a database. Then, these tools interpret your template file and create the infrastructure as defined. With this approach, you can automate almost everything in your cloud infrastructure as long as it’s supported by an API. This means, you can not only provision databases but also start compute services, configure permissions, or similar.

The following code shows an example CloudFormation template. It’s using a Lambda function and a DynamoDB table which are typical resources for a Serverless infrastructure stack in AWS:

AWSTemplateFormatVersion:'2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Deploys a simple AWS Lambda function and DynamoDB table using SAM.

Resources:
SampleFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
console.log('event: ', JSON.stringify(event));
return {};
};
Policies:
- AWSLambdaBasicExecutionRole

SampleTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: sample-table
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-AttributeName: Id
AttributeType: S
KeySchema:
-AttributeName: Id
KeyType: HASH

 

The template is using a commonly used flavour of CloudFormation, i.e. the AWS Serverless Application Model (SAM). It helps writing serverless functions by using a more convenient syntax. In the end, it still is compiled to a regular CloudFormation template.

In order to deploy these resources to your AWS account, you only need to run the following command using the AWS CLI:

aws cloudformation deploy --template template.yaml --stack-name cdk
-blog-post --capabilities CAPABILITY_IAM

Make sure to install & setup the AWS CLI before executing the commands. For an even better developer experience of deploying Serverless functions with CloudFormation, I recommend using the AWS SAM CLI instead. Also, if you use more complex Serverless functions, you need to run aws cloudformation package first which will handle the upload of function artifacts. You can find similar code examples in the repository aws-cloudformation-examples.

The deployment of these two resources using CloudFormation usually takes less than a minute. If a human did this by hand using the AWS Console, it would certainly take more time than that. Also, since a human usually makes more errors than a machine, it might even cost more time and money. However, a typical infrastructure stack includes more than just two resources. Therefore, automating these steps using IaC tools is a necessity which certainly increases the productivity. Moreover, organizations can also provide templates with best practices applied. This is often used to keep costs under control, reduce the number of services and improve the infrastructure security.

Due to these advantages, IaC tools have evolved and improved over the past years. In the AWS world, tools like AWS CloudFormation or Terraform are the standard tools for this task. For Serverless development, tools like AWS SAM or Serverless Framework are certainly popular as well. One of the disadvantages of all these tools is that they require you to write the templates using JSON, YAML, or similar. This makes it harder to automate certain steps, apply best practices and share common configurations across an organization. In addition to that, developers have to learn another syntax again which can not only be frustrating to them but also decrease productivity.

Cloud Development Kit (CDK)

These disadvantages were a few of the reasons why the AWS Cloud Development Kit (CDK) was created. The AWS CDK lets you write the infrastructure code in your programming language of choice. At the moment these are JavaScript/TypeScript, Python, Java, and C# (Go will be available soon). In the end, the CDK still creates a CloudFormation template and deploys it to your AWS account. However, the process often feels much more natural to engineers because they are used to writing code in these languages.

Let’s have a look at some example code:

import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';

export class ExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// The code that defines your stack goes here
}
}

const app = new cdk.App();
new ExampleStack(app, 'ExampleStack', {
stackName: 'example-stack',
});

As you can see, CDK uses the terms App and Stack. A Stack represents a typical CloudFormation stack containing resources whereas an App contains one or more Stacks belonging to one app or system. In your Stack class, you can define your resources like buckets, DynamoDB tables, etc. To further customize your stack, you can provide stack properties like a stack name or provide your own properties.

If we take the previous example of a DynamoDB table and a Lambda function, it can look like this using CDK:

import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';

// required imports for the DynamoDB and Lambda resources below
import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb';
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';

export class ExampleStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// DynamoDB table
new Table(this, 'sample-table', {
tableName: 'sample-table',
billingMode: BillingMode.PAY_PER_REQUEST,
partitionKey: {
name: 'Id',
type: AttributeType.STRING
},
});

// Lambda function
new Function(this, 'sample-function', {
runtime: Runtime.NODEJS_12_X,
handler: 'index.handler',
code: Code.fromInline(`exports.handler = async (event) => {
console.log('event: ', JSON.stringify(event));
return {};
};`)
})
}
}

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


This example code will produce a similar CloudFormation template compared to the one you’ve seen in the previous section. (Note: CDK will not produce the exact same CloudFormation template code since it is adding some additional metadata and other things to it) If you take a closer look at the example code, you might have noticed that the resource properties are not exactly matching the properties from CloudFormation. The Table class expects a partitionKey property to create a DynamoDB table. Even though this property is not part of the regular CloudFormation resource of AWS::DynamoDB::Table, the CDK is able to create a valid CloudFormation template from it. As you can guess, the partitionKey represents the hash key in a DynamoDB table. Using pure CloudFormation code, you would have to set the appropriate AttributeDefinitions and KeySchema properties to define a hash key. The Table class is hiding this complexity from us. It lets us set a partitionKey and translates it into AttributeDefinitions and KeySchema in a CloudFormation template like this:

AttributeDefinitions:
- AttributeName: Id
AttributeType: S
KeySchema:
- AttributeName: Id
KeyType: HASH


This abstraction and translation process is available for various CDK resources and classes. It makes using the CDK really fascinating because you can save a lot of code. The concept behind it is called a CDK Construct and one of the main reasons why you should consider upgrading your AWS infrastructure using AWS CDK.

CDK Constructs

CDK constructs allow you to hide complexity and combine best practices into high-level components. The CDK defines three levels of constructs:

  • L1 constructs are low level constructs that can be directly translated to CloudFormation resources. For example, CfnBucket (Cfn is short for CloudFormation) represents the AWS::S3::Bucket resource in CloudFormation. It’s like a one-to-one mapping to its CloudFormation resource type.
  • L2 constructs offer common functionalities and sensible defaults on top of the existing L1 constructs. For example, Bucket is an L2 construct for an S3 Bucket resource and provides helper methods like addLifecycleRule() to simplify its usage.
  • L3 constructs are also called patterns and combine a number of different resources and constructs. For example, a StaticWebsite construct could combine CloudFront, S3, Route53 and Certificate Manager to serve a static website. Such a construct could create all the necessary resources, take your static files, upload them to S3 and invalidate the CloudFront cache after each deployment.

Here’s an example for a static website which is doing this. It is using the construct @cloudcomponents/cdk-static-website which has been contributed by the CDK community:

import { StaticWebsite } from '@cloudcomponents/cdk-static-website';

// ...

new StaticWebsite(this, 'static-site-test', {
bucketConfiguration: {
source: 'path/to/static/files'
}
});



There are more examples than this which help you with all kinds of AWS services. For example, if you develop Serverless functions for the Node.js runtime, you should have a look at NodejsFunction from @aws-cdk/aws-lambda-nodejs. This construct is automatically bundling your code and assets using esbuild. However, there are even more ways to bundle your Lambda function code in CDK. Other interesting constructs and tools for Serverless development are cdk-watch or SST Serverless Stack to decrease the time between writing your Lambda function code and testing it in the cloud.

As you can see, CDK constructs offer a powerful way to further increase the productivity of engineers and a whole organization. The CDK already provides a huge standard construct library for common use cases of their services. In fact, there are also a lot of community-driven CDK constructs available. Therefore, if you want to share common configuration settings or best practices with your organization, the best way is to write your own CDK constructs which bundle this knowledge. This is one of the biggest advantages of the CDK!

💡 All the official AWS CDK constructs but also many community-driven CDK constructs are available in multiple programming languages. CDK uses its own tool called jsii to compile their constructs from TypeScript to other languages like Python, Java, or C#. If you are curious how to use it in a CDK construct, then take a look at this article about publishing a CDK construct to multiple repositories like npm, Maven, PyPi, and NuGet.

Deploying Your Infrastructure With CDK

In order to finally upgrade your AWS infrastructure using AWS CDK, you need to build your own CDK app. The CDK CLI provides various helpful commands to get started and deploy your code.

CDK Init

If you start a new project or want to create an app’s infrastructure using the CDK, you’ll usually start with the cdk init command:

# view all init options
cdk init

# initialize an app using TypeScript, as shown above
cdk init app --language=typescript

# use Java instead of TypeScript
cdk init app --language=java

The cdk init app command will generate a basic folder structure including a common project setup depending on the chosen programming language. For example, in case of TypeScript the CDK assumes your app definition stays in the bin folder and the lib folder contains your stack classes. (Note: in the example code above I’ve used an inline class and hence, in this case everything stays in one file in bin - but for bigger projects, it makes sense to separate this into multiple files) The init command also creates a file cdk.json that contains certain settings for CDK.

💡 You can also start building your own CDK construct. Simply use cdk init lib --language=typescript instead.

CDK Synth

After adding the first resources to your stack (take the example above using a DynamoDB table and Lambda function for a quick test), you can call cdk synth. It will produce a CloudFormation template and prints it to the console as well as stores it under cdk.out. This is helpful to understand what your code is producing under the hood. On top of this, if you are migrating from CloudFormation to CDK, it lets you verify if you are on the right track.

Environments

Before you deploy, you need to understand that CDK deploys your app infrastructure into an Environment. An environment is a combination of AWS Account ID and Region. You can either define this in your command line using CDK_DEFAULT_ACCOUNT and CDK_DEFAULT_REGION or you explicitly set these details in your app code. For example:

// ...
new ExampleStack(app, 'ExampleStack', {
env: {
account: '123456789',
region: 'us-east-1'
}
});



Similarly, in certain cases you need to run cdk bootstrap once if you want to deploy to an environment the first time. The bootstrap command will provision a few resources that the CDK needs for a deployment, like an S3 bucket to store (temporary) files which are required by stack resources.

For more background information, consider reading this article about CDK environments because not explicitly setting the environment has a few implications.

CDK Deploy

After preparing your environment and adding a few resources to your stack, you can deploy your app using cdk deploy. The CDK CLI will print a summary of the important IAM changes and asks you to review them before starting the deployment. That’s it already! The process doesn’t take much longer compared to pure CloudFormation since it is just CloudFormation code that you’re deploying.

Testing Your CDK Code

Another big advantage of the CDK is the possibility to test your code like you’re used to with unit tests. The CDK provides its own @aws-cdk/assert package which lets you expect and assert that your classes behave as expected and correctly translate to CloudFormation code. Let’s take a look at an example:

import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as CdkInit from '../lib/cdk-init-stack';

test('Empty Stack', () => {
const app = new cdk.App();
// WHEN
const stack = new CdkInit.CdkInitStack(app, 'MyTestStack');
// THEN
expectCDK(stack).to(matchTemplate({
"Resources": {}
}, MatchStyle.EXACT))
});

 

The test is simulating a stack creation and verifies the result using expectCDK from @aws-cdk/assert. By using matchTemplate, the provided arguments are matched against the generated CloudFormation code. This code is from the example test file that is created with cdk init app --language=typescript. You can run the test using npm run test.

Since writing down the whole CloudFormation code for a test can become quite hard to maintain, you can also just verify parts of the template or certain resources. For example, the following code can be used to verify that the stack contains a resource of type AWS::SNS::Topic with a topic name of example-topic:

expectCDK(stack).to(haveResourceLike('AWS::SNS::Topic', {
TopicName: 'example-topic'
}));


There are even more ways to verify the correct behavior of your CDK code. I can recommend taking a look at the @aws-cdk/assert package for more information and ideas to verify your code.

Next Steps

As you have probably recognized, upgrading your AWS infrastructure using the AWS CDK and writing your code using a programming language provides many advantages:

If you are looking for a deep dive into all the topics covered above, then head over to cdkworkshop.com. It’s the best resource to get started with CDK and learn about all the great features in detail by building a CDK app step-by-step. Alternatively, take a look into the official AWS CDK documentation. Besides that, I can highly recommend looking into the awesome-cdk list and joining the CDK developer Slack community to ask questions around CDK.

 


Apr 2021 - 28 min read

Sebastian Hesse

Sebastian Hesse

Sebastian is a freelance software engineer helping clients to successfully develop and run serverless systems on AWS. He is constantly developing new serverless applications, improving existing ones or helping teams to migrate. In his daily work, infrastructure as code using CloudFormation, SAM, Serverless Framework or CDK is a crucial part to develop reliable systems. He enjoys sharing his knowledge on meetups, conferences and his blog on www.sebastianhesse.de.

Ready to get started?

Your request is free. We will call you back within one hour and find a suitable expert within 48 hours.

Already a member? Login here

Woman with curls