CDK Local Lambdas
Local Development and Testing of AWS CDK Lambdas
Updated: 03 September 2023
Introduction
The AWS CDK enables us to define application infrastructure using a programming language instead of markup, which is then transformed by the CDK to CloudFormation templates for the management of cloud infrustructure services
The CDK supports TypeScript, JavaScript, Python, Java, and C#
Prerequisites
AWS Lamda development requires SAM to be installed, depending on your OS you can use the installation instructions here
In addition to SAM you will also require Docker
I’m using
aws-sam-cli@1.12.0
to avoid certain compat issues from the current version
And lastly, you will need to install cdk
1npm i -g aws-cdk
Init Project
To initialize a new project using SAM and CDK run the following command:
1mkdir my-project2cd my-project3cdk init app --language typescript4npm install @aws-cdk/aws-lambda
This will generate the following file structure:
1my-project2 |- .npmignore3 |- jest.config.js4 |- cdk.json5 |- README.md6 |- .gitignore7 |- package.json8 |- tsconfig.json9 |- bin10 |- my-project.ts11 |- lib12 |- my-project-stack.ts13 |- test14 |- my-project.test.ts
In the generated files we can see the bin/my-project.ts
file which creates an instance of the Stack
that we expose from lib/my-project-stack.ts
bin/my-project.ts
1#!/usr/bin/env node2import 'source-map-support/register'3import * as cdk from '@aws-cdk/core'4import { MyProjectStack } from '../lib/my-project-stack'5
6const app = new cdk.App()7new MyProjectStack(app, 'MyProjectStack', {})
Create a Handler
Next, we can create a handler for our file, we’ll use the Typescript handler but the concept applies to any handler we may want to use
First, we’ll export a handler function from our code, I’ve named this handler
but this can be anything and we will configure CDK
as to what function to look for. We’ll do this in the lambdas/hello.ts
file as seen below. Note the use of the APIGatewayProxyHandler
type imported from aws-lambda
, this helps inform us if our event
and return
types are what AWS expects
lambdas/hello.ts
1import { APIGatewayProxyHandler } from 'aws-lambda'2
3export const handler: APIGatewayProxyHandler = async (event) => {4 console.log('request:', JSON.stringify(event, undefined, 2))5
6 const res = {7 hello: 'world',8 }9
10 return {11 statusCode: 200,12 headers: { 'Content-Type': 'application/json' },13 body: JSON.stringify(res),14 }15}
Define Stack
Next, in order to define our application stack we will need to use CDK, we can do this in the lib/my-project-stack.ts
file utilizing @aws-cdk/aws-lambda-nodejs
to define our Nodejs handler:
lib/my-project-stack.ts
1import * as cdk from '@aws-cdk/core'2import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'3
4export class MyProjectStack extends cdk.Stack {5 constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {6 super(scope, id, props)7
8 // this defines a Nodejs function handler9 const hello = new aws_lambda_nodejs_1.NodejsFunction(this, 'HelloHandler', {10 runtime: lambda.Runtime.NODEJS_12_X,11 // code located in lambdas directory12 entry: 'lambdas/hello.ts',13 // use the 'hello' file's 'handler' export14 handler: 'handler',15 })16 }17}
If we want, we can alternatively use the lower-level cdk.Function
class to define the handler like so:
1const hello = new lambda.Function(this, 'HelloHandler', {2 runtime: lambda.Runtime.NODEJS_12_X,3 // define directory for code to be used4 code: lambda.Code.fromAsset('./lambdas'),5 // define the name of the file and handler function6 handler: 'hello.handler',7})
Note, avoid running the above command using
npm run sdk ...
as it will lead to thetemplate.yaml
file including thenpm
log which is not what we want
Create API
Next, we need to add our created lambda to an API Gateway instance so that we can route traffic to it, we can do this using the @aws-cdk/aws-apigateway
package
To setup the API we use something like this in the Stack
:
1let api = new apiGateway.LambdaRestApi(this, 'Endpoint', {2 handler: hello,3})
So our Stack
now looks something like this:
lib/my-project-stack.ts
1import * as cdk from '@aws-cdk/core'2import * as lambda from '@aws-cdk/aws-lambda'3import * as apiGateway from '@aws-cdk/aws-apigateway'4import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'5
6export class MyProjectStack extends cdk.Stack {7 constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {8 super(scope, id, props)9
10 // define the `hello` lambda11 const hello = new NodejsFunction(this, 'HelloHandler', {12 runtime: lambda.Runtime.NODEJS_12_X,13 // code located in lambdas directory14 entry: 'lambdas/hello.ts',15 // use the 'hello' file's 'handler' export16 handler: 'handler',17 })18
19 // our main api20 let api = new apiGateway.LambdaRestApi(this, 'Endpoint', {21 handler: hello,22 })23 }24}
Generate Template
Now that we have some API up, we can look at the process for making it requestable. The first step in the process for running this locally is generating a template.yaml
file which the sam
CLI will look for in order to setup the stack
We can build a Cloud Formation template using the cdk synth
command:
1cdk synth --no-staging > template.yaml
You can take a look at the generated file to see the CloudFormation config that CDK has generated, note that creating the template this way is only required for local
sam
testing and isn’t the way this would be done during an actual deployment kind of level
Run the Application
Once we’ve got the template.yaml
file it’s just a matter of using sam
to run our API. To start our API Gateway application locally we can do the following:
1sam local start-api
This will allow you to make requests to the lambda at http://localhost:3000
. A GET
request to the above URL should result in the following:
1{2 "hello": "world"3}
Use a DevContainer
I’ve also written a Dev container Docker setup file for use with CDK and SAM, It’s based on the Remote Containers: Add Development Container Configuration Files > Docker from Docker
and has the following config:
Dockerfile
1# Note: You can use any Debian/Ubuntu based image you want.2FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu3
4# [Option] Install zsh5ARG INSTALL_ZSH="true"6# [Option] Upgrade OS packages to their latest versions7ARG UPGRADE_PACKAGES="false"8# [Option] Enable non-root Docker access in container9ARG ENABLE_NONROOT_DOCKER="true"10# [Option] Use the OSS Moby CLI instead of the licensed Docker CLI11ARG USE_MOBY="true"12
13# Install needed packages and setup non-root user. Use a separate RUN statement to add your14# own dependencies. A user of "automatic" attempts to reuse an user ID if one already exists.15ARG USERNAME=automatic16ARG USER_UID=100017ARG USER_GID=$USER_UID18COPY library-scripts/*.sh /tmp/library-scripts/19RUN apt-get update \20 && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \21 # Use Docker script from script library to set things up22 && /bin/bash /tmp/library-scripts/docker-debian.sh "${ENABLE_NONROOT_DOCKER}" "/var/run/docker-host.sock" "/var/run/docker.sock" "${USERNAME}" "${USE_MOBY}" \23 # Clean up24 && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts/25
26# install python and pip27RUN apt-get update && apt-get install -y \28 python3.4 \29 python3-pip30
31# install nodejs32RUN apt-get -y install curl gnupg33RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -34RUN apt-get -y install nodejs35
36# install cdk37RUN npm install -g aws-cdk38
39# install SAM40RUN pip3 install aws-sam-cli==1.12.041
42# Setting the ENTRYPOINT to docker-init.sh will configure non-root access to43# the Docker socket if "overrideCommand": false is set in devcontainer.json.44# The script will also execute CMD if you need to alter startup behaviors.45ENTRYPOINT [ "/usr/local/share/docker-init.sh" ]46CMD [ "sleep", "infinity" ]
.devcontainer/devcontainer.json
1// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:2// https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/docker-from-docker3{4 "name": "Docker from Docker",5 "dockerFile": "Dockerfile",6 "runArgs": ["--init"],7 "mounts": [8 "source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind"9 ],10 "overrideCommand": false,11 // Use this environment variable if you need to bind mount your local source code into a new container.12 "remoteEnv": {13 "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"14 },15 // Set *default* container specific settings.json values on container create.16 "settings": {17 "terminal.integrated.shell.linux": "/bin/bash"18 },19 // Add the IDs of extensions you want installed when the container is created.20 "extensions": ["ms-azuretools.vscode-docker"],21 "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind",22 "workspaceFolder": "${localWorkspaceFolder}",23 // Use 'forwardPorts' to make a list of ports inside the container available locally.24 // "forwardPorts": [],25 // Use 'postCreateCommand' to run commands after the container is created.26 // "postCreateCommand": "npm install",27 // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.28 "remoteUser": "vscode"29}
Especially note the workspaceMount
and `workspaceFolderz sections as these ensure the directory structure maps correctly between your local folder structure and container volume so that the CDK and SAM builds are able to find and create their assets in the correct locations