CDK Local Lambdas
Local Development and Testing of AWS CDK Lambdas
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
npm i -g aws-cdk
Init Project
To initialize a new project using SAM and CDK run the following command:
mkdir my-project
cd my-project
cdk init app --language typescript
npm install @aws-cdk/aws-lambda
This will generate the following file structure:
my-project
|- .npmignore
|- jest.config.js
|- cdk.json
|- README.md
|- .gitignore
|- package.json
|- tsconfig.json
|- bin
|- my-project.ts
|- lib
|- my-project-stack.ts
|- test
|- 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
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { MyProjectStack } from '../lib/my-project-stack';
const app = new cdk.App();
new 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
import { APIGatewayProxyHandler } from "aws-lambda";
export const handler: APIGatewayProxyHandler = async (event) => {
console.log("request:", JSON.stringify(event, undefined, 2))
const res = {
hello: 'world'
}
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(res)
}
}
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
import * as cdk from '@aws-cdk/core';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'
export class MyProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// this defines a Nodejs function handler
const hello = new aws_lambda_nodejs_1.NodejsFunction(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_12_X,
// code located in lambdas directory
entry: 'lambdas/hello.ts',
// use the 'hello' file's 'handler' export
handler: 'handler'
});
}
}
If we want, we can alternatively use the lower-level cdk.Function
class to define the handler like so:
const hello = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_12_X,
// define directory for code to be used
code: lambda.Code.fromAsset('./lambdas'),
// define the name of the file and handler function
handler: 'hello.handler'
})
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
:
let api = new apiGateway.LambdaRestApi(this, 'Endpoint', {
handler: hello
})
So our Stack
now looks something like this:
lib/my-project-stack.ts
import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda'
import * as apiGateway from '@aws-cdk/aws-apigateway'
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs'
export class MyProjectStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// define the `hello` lambda
const hello = new NodejsFunction(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_12_X,
// code located in lambdas directory
entry: 'lambdas/hello.ts',
// use the 'hello' file's 'handler' export
handler: 'handler'
})
// our main api
let api = new apiGateway.LambdaRestApi(this, 'Endpoint', {
handler: hello
})
}
}
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:
cdk 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:
sam 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:
{
"hello": "world"
}
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
# Note: You can use any Debian/Ubuntu based image you want.
FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu
# [Option] Install zsh
ARG INSTALL_ZSH="true"
# [Option] Upgrade OS packages to their latest versions
ARG UPGRADE_PACKAGES="false"
# [Option] Enable non-root Docker access in container
ARG ENABLE_NONROOT_DOCKER="true"
# [Option] Use the OSS Moby CLI instead of the licensed Docker CLI
ARG USE_MOBY="true"
# Install needed packages and setup non-root user. Use a separate RUN statement to add your
# own dependencies. A user of "automatic" attempts to reuse an user ID if one already exists.
ARG USERNAME=automatic
ARG USER_UID=1000
ARG USER_GID=$USER_UID
COPY library-scripts/*.sh /tmp/library-scripts/
RUN apt-get update \
&& /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \
# Use Docker script from script library to set things up
&& /bin/bash /tmp/library-scripts/docker-debian.sh "${ENABLE_NONROOT_DOCKER}" "/var/run/docker-host.sock" "/var/run/docker.sock" "${USERNAME}" "${USE_MOBY}" \
# Clean up
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts/
# install python and pip
RUN apt-get update && apt-get install -y \
python3.4 \
python3-pip
# install nodejs
RUN apt-get -y install curl gnupg
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get -y install nodejs
# install cdk
RUN npm install -g aws-cdk
# install SAM
RUN pip3 install aws-sam-cli==1.12.0
# Setting the ENTRYPOINT to docker-init.sh will configure non-root access to
# the Docker socket if "overrideCommand": false is set in devcontainer.json.
# The script will also execute CMD if you need to alter startup behaviors.
ENTRYPOINT [ "/usr/local/share/docker-init.sh" ]
CMD [ "sleep", "infinity" ]
.devcontainer/devcontainer.json
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/docker-from-docker
{
"name": "Docker from Docker",
"dockerFile": "Dockerfile",
"runArgs": [
"--init"
],
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind"
],
"overrideCommand": false,
// Use this environment variable if you need to bind mount your local source code into a new container.
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
},
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-azuretools.vscode-docker"
],
"workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind",
"workspaceFolder": "${localWorkspaceFolder}",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "npm install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
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