AWS CDK(v2): Linking Lambda and APIGateway

Page content

I think that Lambda and APIGateway are a combination that is often used, but is complicated to set up in the management console. It would be great to be able to write this easily. Also, if you write Lambda in TypeScript, it automatically transpiles the TypeScript and installs node modules and packages them for you, so it’s quite easy to use.

Preparation

It is assumed that the following settings have been completed.

Version

  • node: 16.13.1
  • aws-cdk: 2.2.0
  • tsc: 4.5.4

Cowsay Sample Application

Using npm cowsay, I’m going to try to create an application that returns a requested string as text spoken by a cow.

 _____________
< Let's Start >
 -------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Setup CDK

First, create a directory and do cdk init --language typescript. This will create the various typescript.

% mkdir cow_say
% cd cow_say
% cdk init --language typescript

A file for CowSayStack will be created in lib/cow_say-stack.ts, and you can write various things here in the end.

/// lib/cow_say-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class CowSayStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'CowSayQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

This time, we will create the Lambda Function first, and then write the Stask configuration.

Make Lambda Function

Install cowsay.

% npm install cowsay

You can put the Lambda Handler file anywhere you want. Let’s put it in lib/lambda/main.ts in this time. The code itself is simple. It converts the text value of the QueryString parameter of a GET request from API Gateway with cowsay and returns it as is.

/// lib/lambda/main.ts
import * as cowsay from "cowsay";

export const handler = async (event: any = {}): Promise<any> => {
  try {
    let statusCode = 200;
    const text = event.queryStringParameters?.text ?? "Hello!";
    const said = cowsay.say({ text: text });
    return { statusCode: statusCode, body: said };
  } catch (error) {
    console.log(error);
    return { statusCode: 500, body: JSON.stringify({ message: `${error}` }) };
  }
};

UnitTest

I didn’t know how to write good tests for Lambda, but since jest is included from the beginning, for example, you can run UnitTest by doing the following

/// test/lambda/main.test.ts
import * as main from "../../lib/lambda/main";

test("call handler", async () => {
  const event = {
    queryStringParameters: { text: "Hello!" },
  };
  const response = await main.handler(event);
  expect(response.statusCode).toBe(200);
  console.log(response.body);
});

You can also run it with npm run test. With VSCode, you can run a specific test with just a click.

Launch test by VSCode

The test results

Write Stack Configuration

In this case, we just need to be able to call Lambda from the APIGateway, so we can write it as follows. The point is to use NodejsFunction for NodeJS. It will take care of the packaging including TypeScript Transpile and node module.

/// lib/cow_say-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import { LambdaIntegration, RestApi } from "aws-cdk-lib/aws-apigateway";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
import * as path from "path";

export class CowSayStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // https://aws.amazon.com/jp/blogs/news/lambda-managed-by-cdk/
    // For only NodeJS, Golang and Python now
    const cowLambda = new NodejsFunction(this, "CowSay", {
      entry: path.join(__dirname, "./lambda/main.ts"),
      handler: "handler",
    });

    // setup API Gateway for aaLambda
    const api = new RestApi(this, "CowSayAPI", {
      restApiName: "CowSayAPI",
    });

    const integration = new LambdaIntegration(cowLambda);
    api.root.addMethod("GET", integration);
  }
}

Deploy the Stack

% cdk deploy

and it will be deployed in a few minutes.

Let’s run

Try to run it with curl.

% curl "https://umh9ezyai2.execute-api.ap-northeast-1.amazonaws.com/prod/?text=Hey"
{"message":"Error: ENOENT: no such file or directory, open '/var/cows/default.cow'"}

Then, I got an error. It said that the file /var/vows/default.cow was missing. Apparently, the JS file of cowsay is included in the package, but the resource file default.cow is not included, because there is no error in import.

So let’s add cowsay in the bundling.nodeModules option.

/// lib/cow_say-stack.ts
...

    const cowLambda = new NodejsFunction(this, "CowSay", {
      entry: path.join(__dirname, './lambda/main.ts'),
      handler: "handler",
      // Add This!
      bundling: {
       nodeModules: ["cowsay"],
      }
    });

...

If you want to update only the Lambda function, you can add the --hotswap option to speed up the update, but it is not guaranteed to work, so it is recommended to use it only in development environments.

cdk deploy --hotswap

Again, try to access it with curl.

% curl "https://umh9ezyai2.execute-api.ap-northeast-1.amazonaws.com/prod/?text=Hey"
 _____
< Hey >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

This time it was displayed successfully.

Lastly

I have deployed an application that uses Lambda and API Gateway. It may be a little difficult at first, but once you get used to it, you can quickly build it, and since everything is managed in code, it is easy to maintain and run as a team. We can understand a lot more by actually moving our hands.