AWS CDK for Platform Engineers -CDK as a second language

Artem Volkov
6 min readApr 5, 2024

--

Author: Artem Volkov

The second article is in an “AWS CDK for Platform Engineers ” series:

The Mindset
CDK as a second language

Language learning

Have you heard that learning another programming language is easy if you already know one? It’s true because of patterns that are similar in each language. Likely, IaC is an area where patterns have been the same for many years and they are straightforward.

I’ll explain the main building blocks assuming that your first language is Terraform HCL. If you don’t see the full picture — don’t worry, we will build it article by article.

Constructs

In Terraform we can build infrastructure using low-level components (resources) and high-level abstractions (modules). In CDK we have the same concept — Constructs.

There are three types of Constructs:

L1 Constructs — resources

Low-level alternative to Terraform resources. Represents CloudFormation API objects.

# Terraform
resource "aws_s3_bucket" "s3_bucket" {
bucket = "mybucketname"
}
// CDK TypeScript
const bucket = new CfnBucket(this, 's3Bucket', {
bucketName: 'mybucketname'
});

They have similar problems.

  1. If you add some strange bucket names — you will understand the mistake only by an AWS API response. So we have no validation on a compilation time.
  2. We always want to deploy some policies and other low-level resources together with a bucket. If we go only with L1 constructs — it’s too verbose and you have to copy-paste a lot of code.

L2 Constructs — modules

Alternative to Terraform modules. Combines a lot of low-level resources, input validations, conditions, etc.

#Terraform

module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "mybucketname"
block_public_policy = true
versioning = {
enabled = true
}
}
// CDK TypeScript
import * as s3 from 'aws-cdk-lib/aws-s3';

new s3.Bucket(scope, 'Bucket', {
bucketName: "mybucketname"
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
});

Anton Babenko created such terraform modules for AWS. In the case of L2 Construct — the AWS team creates them as a part of a standard CDK Library.

You can create your L2 constructs based on L1 elements or contribute to existing open-source constructs.

L3 Constructs — patterns

Certain organizational patterns become apparent in a large corporation when employing Terraform. These can include unique configurations, security protocols, or naming standards. Typically, this leads to the creation of an additional layer of abstraction — a composite module that encapsulates these modules.

#Terraform 

module "sheduled_fargate_Task" {
source = "link-to-private-registry"
cluster_id = aws_ecs_cluster.default.id
service_name = "crawler"
image = "amazon/amazon-ecs-sample"
ram = 512
schedule = "minute"
}
// CDK TypeScript

import * as escPaterns from 'aws-cdk-lib/aws_ecs_patterns';

const scheduledFargateTask = new ecsPatterns.ScheduledFargateTask(this, 'ScheduledFargateTask', {
cluster,
scheduledFargateTaskImageOptions: {
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
memoryLimitMiB: 512,
},

schedule: appscaling.Schedule.expression('rate(1 minute)'),
});

In this example, I am comparing some self-made Terraform module that creates Fargate Task scheduled by AWS CloudWatch and the same L3 Construct from the CDK Standard Library.

The decision tree is similar to the Terraform experience:

  1. Find L2 Construct for your needs and read the documentation.
  2. In case L2 Construct is not supporting some feature — you can magically override L1 Constructs inside L2 Construct. You don’t need to reinvent the wheel like in Terraform and create your module.

In this example, the L2 Construct representing the Aurora Cluster does not support the snapshot parameter. So I am checking if the parameter snapshotIdentifierArn specified, we have to find L1 CfnCluster (that supports this parameter) inside the L2 DatabaseCluster and override the parameter. As a result CloudFormation template — we will have an instruction to create (or recreate DB from snapshot).

You can read about it more in Escape Hatches

3. Create your L3 Constructs to wrap L2 Constructs if you see some organization pattern.

Stack

Stacks contain constructs and just a CDK representation for the CloudFromation Stacks.

It could be compared logically to Terraform Root Modules, a configuration that represents our Infrastructure and has a single State. But there are a few visible differences:

  • CDK Stack could manage resources in one Region and AWS Account.
  • CloudFormation handles the IaC state. All the pre-requisites you can set just with one command cdk bootstrap

provider "aws" {
region = "us-west-2"
}

module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "my-tf-test-bucket"
}

module "lambda_function" {
source = "terraform-aws-modules/lambda/aws"
function_name = "my-tf-test-lambda"
handler = "exports.handler"
runtime = "nodejs12.x"
environment_variables = {
BUCKET = module.s3_bucket.s3_bucket_id
}
}
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';

const bucket = new s3.Bucket(app, 'Bucket', {
bucketName: "mybucketname",
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
});

new lambda.Function(app, 'Function', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
BUCKET_NAME: bucket.bucketName,
},
});

Notice how I passed the bucket name to a Lambda env variable in both examples.

I am using TypeScript and Visual Studio Code, so it gives me a lot of benefits during development. Like TypeScript Enums for autocompletion after “.” or deprecation warnings

This Stack example will:

  • Build my NodeJS lambda function, located in a “lambda” folder.
  • Publish assets to a separate S3 bucket, created by cdk bootstrap command.
  • Deploy S3 bucket and Lambda function to AWS.

Stage

CDK Stage has no alternative in vanilla Terraform (but there are some similar abstractions in Terragrunt). It’s a purely documented abstraction that allows us to combine multiple stacks and deploy them together with different parameters.

This tool is particularly beneficial for managing large CDK projects and for deployment across various environments. We will take a look at it later in a series.

Custom Resource

Sometimes, if the Terraform provider is not supporting something we use a “fantastic workaround” null_resource or new terraform_data.

In CDK there is a similar concept that looks different AWSCustomResource.

  const getParametersCustomResource = new AwsCustomResource(
scope,
`GetParam${parameterName}`,
{
onCreate: {
service: "SSM",
action: "GetParameter",
parameters: {
Name: Fn.join("/", ["/", project, environment, parameterName]),
},
region: "eu-east-1",
physicalResourceId: PhysicalResourceId.of(`GetParam${parameterName}`),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
}),
}
);

In this example, I am reading the SSM Parameter from another AWS region, which is not possible with the default constructs because of the single-region nature of CloudFormation. Another common use case is to run Database Migrations before your Lambda starts.

Technically, CDK just runs a Lambda function that executes a call to AWS API. You can also run your custom Lambda code inside this AWSCustomResource. It gives us a lot of flexibility, but on another hand, we could create unmaintainable code full of such workarounds.

It allows us to use any AWS SDK (not CDK) for JavaScript calls inside our Construct with the mapping to CRUD triggers (onCreate, onDelete, etc).

Data Source

CDK's philosophy is pushing us to avoid dynamic infrastructure lookups. But sometimes we have to use them.


resource "aws_vpc" "main" {
// parameters
name = "myvpc"
}

data "aws_vpc" "selected" {
filter {
name = "tag:Name"
values = ["myvpc"]
}
}

// Create VPC
const vpc = new ec2.Vpc(this, 'vpc', {
vpcName: 'myvpc',
// Some VPC parameter
});

// Lookup existing vpc by name
const importedVpc = ec2.Vpc.fromLookup(this, 'ImportedVpc', {
vpcName: 'myvpc',
});

CLI Commands

In terms of CLI command, everything is even simpler.

  • cdk diff == terraform plan
  • cdk deploy == terraform apply
  • cdk init ~= terraform init

Conclusion

Terraform and AWS CDK share many common ideas and foundations. This demonstrates that software developers can learn HCL and infrastructure engineers can learn CDK with the same level of ease.

Additional elements such as variables, types, and conditions also exist, though they tend to be more specific to particular programming languages. We’ll explore these in future articles once we’ve decided on a programming language.

Go Deeper

--

--