AWS CDK for deploying lambda AWS serverless

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