Create Your First AWS CDK App to Understand its Power
Why it’s the most developer-friendly way to write serverless applications
In this age of cloud development and serverless architecture, the ability to write infrastructure as code is really powerful.
There are many options to choose from. Let’s explore them!
Options to consider
AWS has several tools to support infrastructure as code. Among them, **Cloudformation**
is the most powerful one but it’s very verbose.
Another option is **AWS SAM**
which is an extension of Cloudformation with reduced syntax. But unlike **Cloudformation**
we have to use JSON
or YAML
syntax for this.
Then comes the AWS CDK
What is AWS CDK?
The AWS Cloud Development Kit (AWS CDK) is an open-source software development framework to define your cloud application resources using familiar programming languages.
Provisioning cloud applications can be a challenging process that requires you to perform manual actions, write custom scripts, maintain templates, or learn domain-specific languages. AWS CDK uses the familiarity and expressive power of programming languages for modeling your applications. It provides high-level components called constructs that preconfigure cloud resources with proven defaults, so you can build cloud applications with ease. AWS CDK provisions your resources in a safe, repeatable manner through AWS CloudFormation. It also allows you to compose and share your own custom constructs incorporating your organization's requirements, helping you expedite new projects.
According to AWS documentation.
The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to define your cloud application resources using familiar programming languages
In simple terms, we can use our typescript
or python
code to define resources in the cloud.
This is powerful for us developers because we don’t have to use some fancy YAML syntax to write infrastructure code anymore!
What we will build today?
To understand the power of AWS CDK we will build a simple hello world application. it will have a Lambda
function and an ApiGateway
resource and can also attach sns
and dynamo DB
database
We will invoke the API to get a response from the lambda.
Let’s begin.
Initialize the project
Go to your terminal and run the following commands mkdir learn-aws-cdkcd learn-aws-cdkcdk init app --language typescript
we will create AWS CDK stack to build and deploy lambda
lets initialize package.json for this project
{
"name": "infra",
"version": "0.1.0",
"bin": {
"infra": "bin/infra.js"
},
"scripts": {
"build": "tsc",
"clean": "rimraf cdk.out && rimraf \"{bin,lib,stacks}/**/*.js\"",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
"devDependencies": {
"@types/node": "10.17.27",
"aws-cdk": "2.12.0",
"aws-cdk-local": "^2.15.0",
"ts-node": "^9.0.0",
"typescript": "~3.9.7"
},
"dependencies": {
"aws-cdk-lib": "2.12.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.16"
}
}
// Native.
import * as path from "path";
// Package.
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
// Internal.
import { APIApiStack } from "../stacks/stack";
import { APIDDBLocalStack } from "../stacks/ddb-local-stack";
// Code.
const app = new cdk.App();
const stage = process.env.STAGE || "development";
const env: cdk.Environment = {
account: process.env.AWS_ACCOUNT_ID,
region: "eu-central-1",
};
const projectName = path.basename(path.join(__dirname, "..", ".."));
// API Proxy to lambda
const isLocal = stage === "local";
new ApiStack(app, `api-stack-${stage}`, {
stage,
apiLambdaPath: isLocal
? path.resolve(`/tmp/${projectName}`)
: path.resolve(__dirname, "..", "..", `${projectName}.zip`),
apiLambdaHandler: "build/api/lambda.handler",
lambdaDescription: gitDescription,
env,
tags: { domain: "service", service: "API" },
stackName: `api-${stage}`,
description: `API stack backed by a lambda to manage API - ${gitDescription}`,
terminationProtection: false,
});
Now lets build Stack for same, we can have
- lambda
- api gateway
- dynamo DB tables
- sns with lambda
// Native.
import * as fs from "fs";
import * as path from "path";
// Package.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
// Internal.
import { Config } from "../lib/config";
import { buildDatabaseInstance } from "../lib/ddb/buildDatabaseInstace";
// Code.
export interface CdkApiStackProps extends cdk.StackProps {
stage: string;
apiLambdaPath: string;
snsListenerLambdaPath: string;
apiLambdaHandler: string;
lambdaDescription?: string;
snsListenerLambdaHandler: string;
}
export class CdkApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: CdkApiStackProps) {
super(scope, id, props);
const {
stage,
apiLambdaPath,
apiLambdaHandler,
lambdaDescription,
} = props;
const isProd = stage === "production";
// Database.
const tables: cdk.aws_dynamodb.Table[] = buildDatabaseInstance({
scope: this,
stage,
});
if (!fs.existsSync(path.resolve(apiLambdaPath))) {
throw new Error("Lambda code zip file does not exist");
}
// adding all dynamo tables to add resource access
const dynamoTableResources: string[] = [];
tables.forEach((table: cdk.aws_dynamodb.Table) => {
dynamoTableResources.push(table.tableArn);
dynamoTableResources.push(
`arn:aws:dynamodb:*:*:table/${table.tableName}`
);
dynamoTableResources.push(
`arn:aws:dynamodb:*:*:table/${table.tableName}/index/*`
);
});
// Topic for external services to push message to Cdk
const sns = new cdk.aws_sns.Topic(this, `cdk-events-${stage}`, {
topicName: `cdk-events-${stage}`,
displayName: `cdk-events-${stage}`,
});
// Lambda.
const apiLambda = new cdk.aws_lambda.Function(
this,
`cdk-api-lambda-${stage}`,
{
functionName: `cdk-api-${stage}`,
code: cdk.aws_lambda.Code.fromAsset(apiLambdaPath),
handler: apiLambdaHandler,
runtime: cdk.aws_lambda.Runtime.NODEJS_14_X,
memorySize: 512,
logRetention: cdk.aws_logs.RetentionDays.FIVE_DAYS,
description: lambdaDescription,
environment: {
STAGE: stage,
DEBUG: Config.debug,
NODE_ENV: "production",
LOG_LEVEL: Config.logLevel,
},
timeout: cdk.Duration.seconds(10),
initialPolicy: [
new cdk.aws_iam.PolicyStatement({
effect: cdk.aws_iam.Effect.ALLOW,
actions: ["secretsmanager:*"],
resources: ["*"],
}),
new cdk.aws_iam.PolicyStatement({
effect: cdk.aws_iam.Effect.ALLOW,
actions: ["dynamodb:*"],
resources: dynamoTableResources,
}),
],
}
);
sns.addSubscription(
new cdk.aws_sns_subscriptions.LambdaSubscription(snsListenerLambda)
);
// API GW
const apiGw = new cdk.aws_apigateway.LambdaRestApi(this, `cdk-api-gw`, {
handler: apiLambda,
deploy: true,
proxy: true,
binaryMediaTypes: ["*/*"],
deployOptions: {
stageName: stage,
},
});
// Output
new cdk.CfnOutput(this, "cdk-api-lambda-arn", {
exportName: "cdk-api-lambda-arn",
value: apiLambda.functionArn,
});
new cdk.CfnOutput(this, "cdk-sns-listener-lambda-arn", {
exportName: "cdk-sns-listener-lambda-arn",
value: snsListenerLambda.functionArn,
});
new cdk.CfnOutput(this, "cdk-api-gw-id", {
exportName: "cdk-api-gw-id",
value: apiGw.restApiName,
});
new cdk.CfnOutput(this, "cdk-events-sns-topic-arn", {
exportName: "cdk-events-sns-topic-arn",
value: sns.topicArn,
});
}
}
Lets decode this whole codebase or sample code our lambda stack
const apiLambda = new cdk.aws_lambda.Function(
this,
`cdk-api-lambda-${stage}`,
{
functionName: `cdk-api-${stage}`,
code: cdk.aws_lambda.Code.fromAsset(apiLambdaPath),
handler: apiLambdaHandler,
runtime: cdk.aws_lambda.Runtime.NODEJS_14_X,
memorySize: 512,
logRetention: cdk.aws_logs.RetentionDays.FIVE_DAYS,
description: lambdaDescription,
environment: {
STAGE: stage,
DEBUG: Config.debug,
NODE_ENV: "production",
LOG_LEVEL: Config.logLevel,
},
timeout: cdk.Duration.seconds(10),
initialPolicy: [
new cdk.aws_iam.PolicyStatement({
effect: cdk.aws_iam.Effect.ALLOW,
actions: ["secretsmanager:*"],
resources: ["*"],
}),
new cdk.aws_iam.PolicyStatement({
effect: cdk.aws_iam.Effect.ALLOW,
actions: ["dynamodb:*"],
resources: dynamoTableResources,
}),
],
}
);
This lambda can access dynamo and access secret manager we can also ad ssns subscription to this lambda using cdk only
// Topic for external services to push message to Cdk
const sns = new cdk.aws_sns.Topic(this, `cdk-events-${stage}`, {
topicName: `cdk-events-${stage}`,
displayName: `cdk-events-${stage}`,
});
sns.addSubscription(
new cdk.aws_sns_subscriptions.LambdaSubscription(snsListenerLambda)
);
// API GW
const apiGw = new cdk.aws_apigateway.LambdaRestApi(this, `cdk-api-gw`, {
handler: apiLambda,
deploy: true,
proxy: true,
binaryMediaTypes: ["*/*"],
deployOptions: {
stageName: stage,
},
});
Lets come to the deployment after writing your lambda
we need aws env to pass in CDK env and before deploying lambda we need zip code of lambda which can be passed to deploy using CDK
const env: cdk.Environment = {
account: process.env.AWS_ACCOUNT_ID,
region: "eu-central-1",
};
new ApiStack(app, `api-stack-${stage}`, {
stage,
apiLambdaPath: isLocal
? path.resolve(`/tmp/${projectName}`)
: path.resolve(__dirname, "..", "..", `${projectName}.zip`),
apiLambdaHandler: "build/api/lambda.handler",
lambdaDescription: gitDescription,
env,
tags: { domain: "service", service: "API" },
stackName: `api-${stage}`,
description: `API stack backed by a lambda to manage API - ${gitDescription}`,
terminationProtection: false,
});
We need to deploy stack api-stack-${env_name}
Now we need to prepare the application as well. To do that run the following command
cdk bootstrap cdk bootstrap aws://YOUR_ACCOUNT_ID/us-east-1
If that is successful now you can run the deploy command to deploy the application to the cloud.
npx cdk deploy cdk-api-stack-$DEV
And you will be greeted with an URL that you can use to call the lambda API.
Comments