Deploying Lambda functions with CDK and Typescript

Lebogang Mabala / Dec 06, 2021

5 min read
Deploying Lambda functions with CDK and Typescript

AWS CDK has definitely made deploying infrastructure simpler than it used to be. Gone are the days where you have to manage your infrastructure with long Cloudformation templates. AWS SAM was an initial attempt to simplify some of the complicated ways of defining resources in AWS. Like I have realised with many new AWS releases, the documentation isn't always up to par with everyday-use cases the community tends to have.

This might have been an issue with my own expectations (just another developer being lazy) when I expected @aws-cdk/aws-lambda to be a no-build setup. After being able to successfully deploy, it was a bit of a surprise when I had issues at runtime. I will now recount the problems I faced and how I solved them.

Outline#

Prerequisites#

This might not be the best place to get started if you are a beginner. Some prior knowledge and setup is required for the technologies listed below:

Getting Started#

Install CDK globally

$ npm install -g aws-cdk

Inside an empty folder of your choice, run the command to initiate a new app.

$ cdk init app --language typescript

Reference project architecture#

The initial default project structure that CDK creates is not something I tend to use as-is. However, if you prefer the defaults, you can continue without making any of the changes I will be making.

Change the app field in your cdk.json

cdk.json
{
  "app": "npx ts-node bin/app.ts",
  ...
}

Your project should have a similar structure to the one below:

.
├── bin
|   ├── app.ts
├── lib
│   ├── LambdaStack.ts
├── src
|   ├── lambda.ts
└── tsconfig.json
└── cdk.json.json

Getting our hands dirty-ish#

Creating our stack.

lib/LambdaStack.ts
import { Stack, Construct } from '@aws-cdk/core';
import { Code, Function as Handler, Runtime } from '@aws-cdk/aws-lambda';

export class LambdaStack extends Stack {
  constructor(scope: Construct) {
    super(scope, 'lambda-stack');
    const code = Code.fromAsset('./src');
    const handler = new Handler(this, 'Handler', {
      runtime: Runtime.NODEJS_14_X,
      handler: 'lambda.handler',
      functionName: 'handler',
      code,
    });
  }
}

Creating and echo function as our handler.

src/lambda.ts
/**
 * Echo handler
 *
 * @param event
 * @returns
 */
export async function handler(event: any){
  return event
}
$ yarn cdk deploy

Once deployed, let's try and execute our newly deployed function.

$ aws lambda invoke \
  --cli-binary-format raw-in-base64-out \
  --function-name handler \
  --payload '{ "name": "Bob" }' \
  response.json
response.json
{
  "errorType": "Runtime.ImportModuleError",
  "errorMessage": "Error: Cannot find module 'lambda'\nRequire stack:\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js",
  "trace": [
    "Runtime.ImportModuleError: Error: Cannot find module 'lambda'",
    "Require stack:",
    "- /var/runtime/UserFunction.js",
    "- /var/runtime/index.js",
    "    at _loadUserApp (/var/runtime/UserFunction.js:202:13)",
    "    at Object.module.exports.load (/var/runtime/UserFunction.js:242:17)",
    "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
    "    at Module._compile (internal/modules/cjs/loader.js:1085:14)",
    "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)",
    "    at Module.load (internal/modules/cjs/loader.js:950:32)",
    "    at Function.Module._load (internal/modules/cjs/loader.js:790:12)",
    "    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)",
    "    at internal/main/run_main_module.js:17:47"
  ]
}

There is clearly something wrong with our function. The invocation error shouldn't come as a surprise because our typescript file doesn't seem to have been compiled when looking at the lambda console.

AWS Lambda code console

This meant I needed to set up my own build to compile my typescript files to javascript. Like most of my fellow developers, I wasted time on the internet looking for a solution that would give me my no-build solution instead of using what I already knew. I came across Serverless Stack (excellent tool and easy to setup) but I wasn't keen on creating a new project from scratch, so I kept looking. @aws-cdk/aws-lambda-nodejs to the rescue.

Changing our LambdaStack to look like the code below allows us to have auto-compiled typescript:

lib/LambdaStack.ts
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
...
const handler = new NodejsFunction(this, 'Handler', {
  runtime: Runtime.NODEJS_14_X,
  handler: 'handler',
  functionName: 'handler',
  entry: './src/lambda.ts',
});

As mentioned in the documentation, to build this locally you need to have docker running or add esbuild to your project. Even though I have docker on my computer, I prefer to add esbuild to the project.

$ yarn add --dev esbuild@0

Deploying once more

$ yarn cdk deploy

Invoking our lambda once more

$ aws lambda invoke \
  --cli-binary-format raw-in-base64-out \
  --function-name handler \
  --payload '{ "name": "Bob" }' \
  response.json

We have the expected response.

response.json
{
  "name": "Bob"
}
This basic setup is needed to deploy your Lambda function with CDK and TypeScript. You might need to adapt this solution to your use cases for a more complete solution. Hopefully, this is sufficient enough to get you started.

Resources#

Subscribe

Get emails whenever I release a new post.