How to use Lambda Layers in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Wed Apr 21 20215 min read

banner

Photo by Matt Whitacre

Updated on Wed Apr 21 2021

In order to provision Lambda Layers in CDK we have to use the LayerVersion construct

Using Lambda Layers in AWS CDK #

To provision Lambda Layers in AWS CDK we have to use the LayerVersion construct.

In this article we are going to provision a Lambda function that has 2 layers:

  • a layer with a 3rd party library
  • a layer with some helper functions, that we have written ourselves
The code for this article is available on GitHub

Project setup #

  1. Clone the github repository

  2. Install the dependencies:

shell
npm install && npm install --prefix src/layers/yup-utils/nodejs
  1. Provision the CDK stack
shell
npx cdk deploy

Provisioning Lambda Layers with CDK #

The lambda function, we have provisioned uses two layers:

  • the yup library
  • our own calc layer, which exports a simple helper function

This is the code, that provisions the 2 Lambda layers:

lib/cdk-starter-stack.ts
import * as lambda from '@aws-cdk/aws-lambda';
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
import * as cdk from '@aws-cdk/core';
import * as path from 'path';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ๐Ÿ‘‡ layer we've written
    const calcLayer = new lambda.LayerVersion(this, 'calc-layer', {
      compatibleRuntimes: [
        lambda.Runtime.NODEJS_12_X,
        lambda.Runtime.NODEJS_14_X,
      ],
      code: lambda.Code.fromAsset('src/layers/calc'),
      description: 'multiplies a number by 2',
    });

    // ๐Ÿ‘‡ 3rd party library layer
    const yupLayer = new lambda.LayerVersion(this, 'yup-layer', {
      compatibleRuntimes: [
        lambda.Runtime.NODEJS_12_X,
        lambda.Runtime.NODEJS_14_X,
      ],
      code: lambda.Code.fromAsset('src/layers/yup-utils'),
      description: 'Uses a 3rd party library called yup',
    });
  }
}

Let's go over the code snippet:

  1. We've defined 2 layers using the LayerVersion construct
  2. The layers take 3 parameters:
  • compatibleRuntimes - the runtimes the layer supports. The Lambda function's runtime must be one of the Layer's compatibleRuntimes, otherwise an error is thrown.
  • code - the content of the layer. We've called the Code.fromAsset method with a path that leads to the layer directory, which has a nodejs directory. The nodejs part is different for every Lambda runtime.
  • description - a short description of the layer

The code that provisions the Lambda function, which uses layers is:

lib/cdk-starter-stack.ts
new NodejsFunction(this, 'my-function', {
  memorySize: 1024,
  timeout: cdk.Duration.seconds(5),
  runtime: lambda.Runtime.NODEJS_14_X,
  handler: 'main',
  entry: path.join(__dirname, `/../src/my-lambda/index.ts`),
  bundling: {
    minify: false,
    // ๐Ÿ‘‡ don't bundle `yup` layer
    // layers are already available in the lambda env
    externalModules: ['aws-sdk', 'yup'],
  },
  layers: [calcLayer, yupLayer],
});

Let's go over the snippet:

  1. We use the NodejsFunction construct to provision a Lambda function. This construct allows us to transpile and bundle our lambda code automatically, regardless if we use JavaScript or TypeScript.
  2. The lambda runtime is one of the layer compatibleRuntimes
  3. We've added the bundling.externalModules prop - it allows us to specify packages that shouldn't be bundled with the lambda code. Since layers and the aws-sdk are already available in the lambda environment, we don't want to bundle layers. In our case we've excluded the yup and aws-sdk libraries from being bundled with our lambda code.

File structure of Lambda Layers in CDK Projects #

The folder structure of our local src directory looks like:

src/
  my-lambda/
    index.ts
  layers/
    calc/
      nodejs/
        calc.ts
    yup-utils
      nodejs/
        node_modules/
        package-lock.json
        package.json
        yup-utils.ts

However when the layers get ported inside the lambda function they are placed in the /opt directory. The folder structure inside the lambda will look like:

/opt
  nodejs/
    calc.js
    yup-utils.js

So the way we would import a helper method from the calc layer looks like:

import {double} from '/opt/nodejs/calc';

Code for the Lambda Layers and the Lambda Function #

Let's take a quick look at the code for our layer functions, starting with the calc layer:

src/layers/calc/nodejs/calc.ts
export function double(a: number): number {
  return a * 2;
}

And the code for the yup layer:

src/layers/yup-utils/nodejs/yup-utils.ts
export * from 'yup';

Notice that we can just write typescript without having to worry about managing any webpack configuration and so on, esbuild handles all the heavy lifting.

For a more real world project, we would define more helper methods that we would reuse in our lambda functions.

Let's look at the code of our Lambda function at src/my-lambda/index.ts:

src/my-lambda/index.ts
import {APIGatewayProxyEventV2, APIGatewayProxyResultV2} from 'aws-lambda';
/* eslint-disable import/extensions, import/no-absolute-path */
import {double} from '/opt/nodejs/calc';
/* eslint-disable import/extensions, import/no-absolute-path */
import {number, object, string} from '/opt/nodejs/yup-utils';

// ๐Ÿ‘‡ using yup layer
const schema = object().shape({
  name: string().required(),
  age: number().required(),
});

export async function main(
  event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> {
  console.log(event);

  await schema.isValid({name: 'Tom', age: 24});

  return {
    // ๐Ÿ‘‡ using calc layer
    body: JSON.stringify({num: double(15)}),
    statusCode: 200,
  };
}

We've made use of both of our layers in the lambda code.

Notice how we import the layers using an absolute path:

src/my-lambda/index.ts
/* eslint-disable import/extensions, import/no-absolute-path */
import {double} from '/opt/nodejs/calc';
/* eslint-disable import/extensions, import/no-absolute-path */
import {Asserts, number, object, string} from '/opt/nodejs/yup-utils';

Since the layers are not located under /opt/nodejs on our local path, we end up getting a lot of linting and typescript warnings.

It's important to mention, that in order to please typescript we have to specify the local paths to the layers in our tsconfig.json file:

tsconfig.json
{
  "baseUrl": "./",
  "paths": {
    "/opt/nodejs/yup-utils": ["src/layers/yup-utils/nodejs/yup-utils"],
    "/opt/nodejs/calc": ["src/layers/calc/nodejs/calc"]
  }
}

Testing the Lambda Function and Layers #

Let's test the lambda function and layers with the Lambda console.

After running a test with an empty event object I got the following response:

lambda layers test

The test shows that the lambda function successfully invoked our calc layer to double the integer 15 and it also validated an object using the 3rd party yup library.

The bundle size of the lambda function is only 943 bytes, because we've specified the 3rd party library yup in the externalModules array when defining our NodejsFunction.

lambda bundle size

Cleanup #

To delete the stack you can run the following command:

shell
npx cdk destroy

Discussion #

Being able to write all our lambda / layer / infrastructure code using TypeScript is quite nice.

With zero configuration we can leverage esbuild to transpile and bundle our code, minify our lambda code, and exclude the layer code, by specifying the layer package as an external module.

The layers are already available in the Lambda runtime so we don't have to bundle them alongside the lambda code.

An unfortunate thing is the way we have to import the lambda layers by specifying an absolute path, which leads to many eslint warnings that the layers are not under the same path on the local file system.

We also have to explicitly install the 3rd party libraries our layers use before a deployment.

If you find a better way to do it, hit me up and I'll update the article.

The code for this article is available on GitHub

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee