Creating and deploying a Lambda function with the AWS CDK

Amazon Web Services (AWS) is a leading cloud computing platform that provides a variety of products and services to developers. That includes computing, storage, networking, database, analytics, application services, deployment management, machine learning, mobile, and developer tools. The most popular are Elastic Cloud (EC2), Amazon Simple Storage Service (S3), and AWS Lambda.

In this tutorial, we will build and deploy a simple AWS Lambda function that returns a random quote. I will use TypeScript for all the code in this article.

AWS Lambda is a hosting platform that lets you run serverless functions. A serverless function is a function that you can run without creating and managing a server. It has both pros and cons.

Advantages of serverless functions:

Disadvantages of serverless functions:

What is the AWS CDK

The AWS CDK is a framework that lets you define your cloud infrastructure using one of the supported programming languages. Currently, it supports TypeScript, JavaScript, Python, Java, C# or Go.

Instead of having to set up the infrastructure manually on the AWS Management Console, you define what you want the infrastructure to look like, and the CDK will do the rest.

In this case, if you tried to do it manually, it would be like this:

That would be a lot of clicking!

If you wanted to update the function, you would have to re-upload the ZIP file every time you change the function locally.

Sounds like a really tedious task.

What would it look like if you did the same thing with the AWS CDK?

You would start by defining a stack consisting of the Lambda function and the API Gateway declaration. Then, every time you would like to update the function, you would issue the cdk deploy command and watch the function update. To make it even easier, the CDK has a command cdk watch that watches a folder for a change and automatically builds and deploys the function for you when the change is detected.

Prepare the local development environment

Create AWS Account and Install Tooling

Before going any further, I assume that you already have an AWS account and the AWS Command Line Interface (AWS CLI) installed on your computer.

If you don’t have an AWS account yet, you can get one completely for free here. In addition, AWS lets you try many of its services for free or free for the first 12 months.

Read how to install the AWS CLI here, and how to configure it here.

After you finish installing and configuring the AWS CLI, you can test if you did everything correctly by running the following command:

aws sts get-caller-identity

If everything is fine, the output should look like this:

{
    "UserId": "{userId}",
    "Account": "{accountId}",
    "Arn": "arn:aws:iam::{accountId}:user/{username}"
}

Installing Node.js and npm

Node.js is a JavaScript runtime, and npm is a package manager for installing and managing JavaScript modules. Because the AWS CLI is built using TypeScript, a derivative of JavaScript, it needs Node.js and npm to run.

Download Node.js and npm from here.

Installing AWS CDK

Once you have an AWS account and the AWS CLI ready, you can proceed with installing the AWS CDK.

Install the AWS CDK by running the following command:

npm install -g aws-cdk

Creating a new CDK project

A CDK project is a directory that contains a collection of files that make up your CDK application. Usually, the CDK application folder always has a json configuration file and files describing your cloud infrastructure. It may also include source code for serverless functions, various deployable assets such as images or Dockerfiles, and tests.

The cloud infrastructure can be described using the following languages: C#, F#, Go, Java, JavaScript, Python and TypeScript. TypeScript was the first language supported by the AWS CDK, and it is still the most popular one.

Create an empty folder:

mkdir dailyquote 
cd dailyquote

Initialize an empty project with TypeScript bare-bones using the following command:

cdk init --language=typescript

Directory structure

Let’s look at the generated folder structure and files:

$ ls -1p

README.md
bin/
cdk.json
jest.config.js
lib/
node_modules/
package-lock.json
package.json
test/
tsconfig.json

Because we chose TypeScript as the project language, the generated project structure follows the usual structure of Node.js projects. For the sake of clarity, let’s go through every item in it:

Constructs

You will encounter constructs a low while working with the AWS CDK.

What’s a construct?

Constructs are the basic building blocks, like LEGO bricks, that make up the AWS CDK application. A construct can represent a single AWS resource, or it can be a higher level abstraction consisting of multiple resources.

The AWS CDK comes with the construct library, which contains constructs for every AWS resource. In addition, you can use the Construct Hub to discover additional open-source construct libraries.

Application

A CDK application is a construct. It can have one or multiple stacks.

Inside the bin folder, you will find your application’s main entry point. The file is executed every time you issue the cdk command.

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { DailyquoteStack } from '../lib/dailyquote-stack';

const app = new cdk.App();
new DailyquoteStack(app, 'DailyquoteStack', {});

The content of this file is pretty straightforward. There are two module imports, followed by the initialization of a construct called app. Then the app construct is passed as a parameter to the initialization of the stack construct.

That’s it. The control flow then goes to the stack.

Stacks

A stack is also a construct. Application can have more than one stack. A stack is a single unit of deployment that consists of multiple AWS resources. A single unit of deployment means that all the AWS resources that are defined in the stack would be provisioned as a single unit.

Below is the content of the lib/dailyquote-stack.ts file, which is an empty stack without any resources defined yet.

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    // The code that defines your stack goes here
  }
}

Building a lambda function

What does a bare-bone lambda function look like? It looks like a regular JavaScript function—it accepts parameters, has a function body with some code and a return statement.

export async function handler(event, context) {
  const result = 'Hi ' + event['name']
  return result
};

As you can see, the lambda function accepts two parameters—event and context. The event parameter is a JavaScript object constructed from the supplied JSON payload. The context is an object containing the information about the function invocation.

Deploying manually using Lambda console

Open the Lambda console and click the Create function button. Fill in the name and runtime fields (select the Node.js 18.x as the runtime) in the next window, and click the Create function button again.

On the code editor page that appears after creating the function, paste the code from the example above, and push the Deploy button.

Open the terminal and execute the function using the AWS CLI:

$ aws lambda invoke \ 
  --function-name "TestFunction" \ 
  --cli-binary-format raw-in-base64-out \ 
  --payload '{"name":"John"}' \
  output

Let’s see the result:

$ cat output
Hi John

Invoking the function using the HTTP protocol

There are numerous ways to call a Lambda function. We just used a direct invocation, but this way wouldn’t be suitable for web use. Another way to invoke a Lambda function is to use triggers. A trigger is a resource you configure to invoke a Lambda function when certain events or conditions occur.

There is a service called API Gateway that acts as a public web server that can route requests to internal AWS services. We can create a trigger that would invoke our lambda function when a public HTTP endpoint is hit.

You can create a trigger from the function overview page by clicking the Add trigger button. In the trigger configuration window, select API Gateway as a trigger source, then choose HTTP API for API Type and set the security option to open. After clicking the Add button again, you will be redirected to the Lambda Triggers' page where you will find the URL of the newly created endpoint for your lambda function.

I copied the URL and called it using curl:

$ curl -G -d name=John https://dummy.execute-api.eu-central-1.amazonaws.com/default/TestFunction
{"message":"Internal Server Error"}

However, I didn’t get the result I expected but an error instead.

It happened because the AWS Gateway called the lambda function but received a response that it couldn’t process. Your lambda function needs to specify the HTTP status code and the response body. Without the response code, you will get the internal status error.

Let’s update the function to fulfill this requirement:

export async function handler(event, context) {
    const result = 'Hi ' + event['name']
    return {
        statusCode: 200,
        body: result,
    }
};

And then try to run it again.

$ curl -G -d name=John https://dummy.execute-api.eu-central-1.amazonaws.com/default/TestFunction
Hi undefined

Now the name input is missing. We changed the output but the input is also fed slightly different. The event parameter is not a simple key value list we supplied before. It has now become an object that contains information about the current HTTP request—a request body, a list of query string parameters, HTTP headers, and other information you would expect an HTTP request to have.

The last change to the function to read the name from the query string:

export async function handler(event, context) {
    const result = 'Hi ' + event.queryStringParameters.name
    return {
        statusCode: 200,
        body: result,
    }
};

Now when executed, the function should return the expected result.

$ curl -G -d name=John https://dummy.execute-api.eu-central-1.amazonaws.com/default/TestFunction
Hi John

Deploying the lambda function using the AWS CDK

In the previous chapter, we created and deployed a lambda function manually. In this chapter, we will do the same but using the AWS CDK.

Create a lambda.ts inside the lib folder and copy to it the following code:

import { APIGatewayProxyEventV2, APIGatewayProxyResultV2, Context } from 'aws-lambda';

const quotes = [
  {
    "quote": "Life isn’t about getting and having, it’s about giving and being.",
    "author": "Kevin Kruse"
  },
  {
    "quote": "Whatever the mind of man can conceive and believe, it can achieve.",
    "author": "Napoleon Hill"
  },
  {
    "quote": "Strive not to be a success, but rather to be of value.", "author":
      "Albert Einstein"
  },
  {
    "quote": "I attribute my success to this: I never gave or took any excuse.",
    "author": "Florence Nightingale"
  },
  {
    "quote": "You miss 100% of the shots you don’t take.",
    "author": "Wayne Gretzky"
  },
  {
    "quote": "I’ve missed more than 9000 shots in my career. I’ve lost almost 300 games. 26 times I’ve been trusted to take the game winning shot and missed. I’ve failed over and over and over again in my life. And that is why I succeed.",
    "author": "Michael Jordan"
  },
];

export async function handler(event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> {
  const randomQuote = quotes[Math.floor(Math.random()*quotes.length)];

  return {
    headers: {"Content-Type": "application/json"},
    statusCode: 200,
    body: JSON.stringify(randomQuote),
  };
}

The code is quite self-explanatory. There is an array containing quotes and a function that returns a random element of the array wrapped in the APIGatewayProxyResultV2 type.

Configuring the stack

Open the stack definition file and replace everything with the following code:

import { Stack, StackProps, CfnOutput } from "aws-cdk-lib";
import { Construct } from 'constructs';
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import * as path from "path";
import * as apigateway from "@aws-cdk/aws-apigatewayv2-alpha";
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";

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

   const dailyQuoteLambda = new NodejsFunction(this, 'DailyQuoteFunction', {
    entry: path.join(__dirname, 'lambda.ts'),
   });

   const httpApi = new apigateway.HttpApi(this, "DailyQuoteApi");

   httpApi.addRoutes({
    path: "/",
    integration: new HttpLambdaIntegration("DailyQuoteLambdaIntegration", dailyQuoteLambda),
   })
   
   new CfnOutput(this, "apiUrl", {
    value: httpApi.url || '',
   });
  }
}

What has been added since the previous version of the stack definition file?

We added a NodeJsFunction construct that takes the specified TypeScript source file, transpiles it to JavaScript, and then uploads it to the AWS.

This is followed by an HttpApi construct that contains a single route going to the dailyQuoteLambda function

And the last one is the CfnOutput construct that prints the given value of the API URL to the terminal. You might wonder why we need to use the CfnOutput when the console.log could do the job? To put it simply, the stack definition code is executed before deploying the stack itself. In other words, TypeScript code is used to generate CloudFormation templates, not to deploy the stack to AWS.

Deploying the stack

It’s time to deploy the stack of the cloud. This can be done with the following command:

$ cdk deploy

...

Outputs:
DailyQuoteStack.apiUrl = https://dummy.execute-api.eu-central-1.amazonaws.com/

It might take a while to run for the first time. However, all subsequent deployments will be faster. Also, there is the cdk watch command that not only monitors whether your code has changed and then pushes the changes to the cloud, but it also speeds up the deployment process substantially.

Let’s test the function using the HTTP API:

$ curl https://dummy.execute-api.eu-central-1.amazonaws.com/
{"quote":"You miss 100% of the shots you don’t take.","author":"Wayne Gretzky"}

Cleaning up

When you are done and don’t need the stack anymore, you can delete it with the command below:

$ cdk destroy

Conclusions

In this post, we created a simple serverless function and learned two different ways to deploy it in AWS.

If you need a single function, it can be easier to set up it manually. However, creating and managing multiple cloud resources can get complicated very quickly—that’s where the AWS CDK comes in handy.