How to create and deploy a dockerized Lambda using TypeScript CDK

diagram of AWS stack with lambda scheduled by eventbridge rule

An application running periodically based on a schedule with no interface to interact with it directly (often called background worker) is quite common in today’s architecture design.

In this article, we’ll explore how we can create a Lambda function using an AWS template, define an EventBridge scheduler to run it automatically and deploy everything to AWS using a Cloud Development Kit (CDK) stack written in TypeScript.

Even though we are using dockerized C# lambda function, any supported language would work just as fine, with CDK code being almost identical.

Some background

.NET Lambda app can be deployed to AWS as a .NET deployment package (.zip file archive) or as a Docker image. The latter option requires some initial work but provides more flexibility in the future since we can control the underlying image.

Of course, the image needs to be stored somewhere, so there is a small additional cost of using Elastic Container Registry (ECR):

There is no additional charge for packaging and deploying functions as container images. When a function deployed as a container image is invoked, you pay for invocation requests and execution duration. You do incur charges related to storing your container images in Amazon ECR.

Deploying Lambda functions

It probably makes sense to start with a zip archive model and switch to docker later if needed.

We will be working with the containerized model in this article. If you want to follow along make sure you have Docker and .NET 8 installed.

Create a new Lambda app from the template

Let’s start with creating a root folder for our project. In our case, we’ll call it DockerizedLambdaExample.

To create a new lambda function we are going to execute these two commands from a terminal opened in the folder we’ve just created:

dotnet new install Amazon.Lambda.Templates
dotnet new lambda.image.EmptyFunction

And we should end up with this:

terminal with commands to create lambda function from a template

There are two new folders created in our root folders, src and test, with a new .NET project in each one. Readme.md located in src folder is a good starting point.

Inspecting the Dockerfile

By default, you need to execute dotnet build command before you can build the image because the default Dockerfile just copies the pre-built binaries:

The default configuration for the project and the Dockerfile is to build the .NET project on the host machine and then execute the docker build command which copies the .NET build artifacts from the host machine into the Docker image.

Readme.md

I prefer to build the lambda inside the Dockerfile and Readme.md conveniently mentions how we can achieve this:

Alternatively the Docker file could be written to use multi-stage builds and have the .NET project built inside the container.

Readme.md

So let’s change the generated Dockerfile with the alternative one provided in Readme.md:

FROM public.ecr.aws/lambda/dotnet:8 AS base

FROM mcr.microsoft.com/dotnet/sdk:8.0 as build
WORKDIR /src
COPY ["DockerizedLambdaExample.csproj", "DockerizedLambdaExample/"]
RUN dotnet restore "DockerizedLambdaExample/DockerizedLambdaExample.csproj"

WORKDIR "/src/DockerizedLambdaExample"
COPY . .
RUN dotnet build "DockerizedLambdaExample.csproj" --configuration Release --output /app/build

FROM build AS publish
RUN dotnet publish "DockerizedLambdaExample.csproj" \
            --configuration Release \
            --runtime linux-x64 \
            --self-contained false \
            --output /app/publish \
            -p:PublishReadyToRun=true

FROM base AS final
WORKDIR /var/task
COPY --from=publish /app/publish .

Inspecting the code

Let’s inspect the function’s code in Function.cs:

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace DockerizedLambdaExample;

public class Function
{

    /// <summary>
    /// A simple function that takes a string and returns both the upper and lower case version of the string.
    /// </summary>
    /// <param name="input">The event for the Lambda function handler to process.</param>
    /// <param name="context">The ILambdaContext that provides methods for logging and describing the Lambda environment.</param>
    /// <returns></returns>
    public Casing FunctionHandler(string input, ILambdaContext context)
    {
        return new Casing(input.ToLower(), input.ToUpper());
    }
}

public record Casing(string Lower, string Upper);

As we can see, it takes a string and returns a lower- and upper-cased version of the input. Since we are implementing a background job-like lambda, let’s remove the parameter and return “Hello world from Lambda function!” string instead:

public class Function
{
    public string FunctionHandler(ILambdaContext context)
    {
        return "Hellow World from Lambda Function!";
    }
}

Now we need to adjust the unit test, let’s replace the FunctionTest.cs class in the test folder with this:

public class FunctionTest
{
    [Fact]
    public void TestToUpperFunction()
    {
        var function = new Function();
        var context = new TestLambdaContext();
        var result = function.FunctionHandler(context);

        Assert.Equal("Hellow World from Lambda Function!", result);
    }
}

We can verify the test passes by running dotnet test from the folder with the test project:

dotnet unit test passing in terminal

Creating CDK stack

We have created and tested our Lambda function, so now it’s time to define our IaC stack.

Serverless applications usually comprise a combination of Lambda functions and other managed AWS services working together to perform a particular business task. AWS SAM and AWS CDK simplify building and deploying Lambda functions with other AWS services at scale.

Build and deploy C# Lambda functions

Let’s create a new folder called cdk in the root folder and execute the following command to create a new CDK app inside this folder:

cdk init app --language typescript

It will create a new bootstraped CDK app and we want to replace lib/cdk-stack.ts content with:

import * as cdk from "aws-cdk-lib";
import { DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";

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

    const lambdaFn = new DockerImageFunction(this, "DockerizedLambdaExample", {
      code: DockerImageCode.fromImageAsset("../src/DockerizedLambdaExample", {
        cmd: [
          "DockerizedLambdaExample::DockerizedLambdaExample.Function::FunctionHandler",
        ],
      }),
      timeout: cdk.Duration.seconds(600),
    });

    // Run the eventbridge every minute
    const rule = new events.Rule(this, "Rule", {
      schedule: events.Schedule.expression("cron(* * ? * * *)"),
    });

    // Add the lambda function as a target to the eventbridge
    rule.addTarget(new targets.LambdaFunction(lambdaFn));
  }
}

In the “code” section of DockerImageFunction we point our asset to the folder with the Dockerfile - ../src/DockerizedLambdaExample, we also provide the CMD option which we can take from our generated aws-lambda-tools-defaults.json.

The next thing we need to define is our EventBridge rule, which uses a cron expression, in our case, it will run the function each minute.

The final step is to add our lambda as the target of the rule.

Let’s verify our CDK stack is correct by running cdk synth in cdk folder, it should produce YAML file in the terminal and new cdk.out folder with JSON templates.

You can now deploy your app using cdk deploy command.

The source code is available here.

comments powered by Disqus