AWS Developer Tools Blog

.NET 5 AWS Lambda Support with Container Images

Yesterday, the AWS Lambda team announced support for publishing Lambda functions as container images. As part of that release, we updated the AWS Lambda .NET tooling to support building Lambda functions as container images for .NET Core 2.1 and 3.1, as well as full support for .NET 5.

.NET 5, which was released last month, is a major release towards the vision of a single .NET experience for .NET Core, .NET Framework, and Xamarin developers. .NET 5 is a “Current” release and is not a long term supported (LTS) version of .NET. The next LTS version will be .NET 6, which is currently scheduled to be released in November 2021. .NET 5 will be supported for 3 months after that date, which means that .NET 5 will be supported for about 15 months in total. In comparison, .NET 6 will have 3 years of support. Even though Lambda’s policy has always been to support LTS versions of language runtimes for managed runtimes, the new container image support makes .NET 5 a first class platform for Lambda functions.

With Lambda container image support, you can build .NET 5 Lambda functions using our AWS Lambda .NET 5 base image. Our AWS .NET Lambda tooling allows you to start building .NET 5 Lambda functions with very little knowledge of containers. If you’ve previously built .NET Core 2.1 or 3.1 Lambda functions, you will find the experience very similar. The only additional requirement is that the Docker CLI must be installed.

While using our AWS .NET Lambda tooling, you still have access to the Dockerfile and the Docker CLI, so you can customize your Lambda running environment. Also you can add various tools, machine learning models, or additional native libraries as discussed in this post’s “System.Drawing support with container images” section.

Getting started with Visual Studio

Version 1.20.0.0 of the AWS Toolkit for Visual Studio has been released with support for container image based Lambda functions. The toolkit can be installed from the Visual Studio Marketplace.

After the toolkit is installed and a credentials profile is set up, create a new project in Visual Studio using the AWS Lambda project template for either C# or F#. After naming the project, the AWS Lambda blueprint screen will be displayed with the new .NET 5 (Container Image) blueprint option:

Once the project is created, it will have a Dockefile ready to build the container image. The aws-lambda-tools-defaults.json file will be prefilled with most of the settings that are required to deploy the Lambda function as a container.

All you need to do is write the logic to solve your use case, taking advantage of .NET 5’s features. This “hello world” sample code is demonstrating the use of C# 9’s new record type being used as the return for a Lambda function:


[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace HelloWorldFromContainers
{
    public class Function
    {
        public Casing FunctionHandler(string input, ILambdaContext context)
        {
            return new Casing(input?.ToLower(), input?.ToUpper());
        }
    }

    public record Casing(string Lower, string Upper);
}

To deploy the Lambda function, right click on the Lambda project in the Solution Explorer and select Publish to AWS Lambda… to launch the deployment wizard. With the settings in aws-lambda-tools-defaults.json written by the blueprint, the only settings you need to modify in the wizard are the function name on the first page and IAM Role on the second page.

The first page of the wizard has changed slightly compared to deploying as a zip file. It now has a new Package Type property, whose value is set to image. This property toggles fields that need to be set on the rest of the page. For a package type of image, the Image Command property works similar to the function handler property for zip packages. The value tells the Lambda runtime which .NET function to call when a Lambda event needs to be processed. The format of the string is <assembly-name>::<full-type-name>::<function-name>. The image command could also be specified directly in the Dockerfile using the CMD command. The other important properties set here are Image Repo and Image Tag , as they identify where to push the container image to Elastic Container Registry (ECR).

The second page in the wizard hasn’t changed compared to packaging functions as a zip file. Here you need to set the Role Name field to give the Lambda function AWS credentials to access AWS services.

Once you have updated the settings, push the Upload button to initiate the deployment process. The deployment wizard takes care of the following tasks as part of the deployment:

  • Building the .NET Lambda project if the docker-host-build-output-dir parameter is set in the aws-lambda-tools-defaults.json file.
  • Building the container image using the Docker CLI.
  • Creating the ECR repository if it doesn’t exist.
  • Logging into ECR with the Docker CLI.
  • Pushing the Docker image to the ECR repository.
  • Creating the Lambda function with the Lambda service pointing to the ECR repository as the code source for the Lambda function.

In this short clip that shows the deployment process, you can see that the entire image is being uploaded to ECR. The initial push will take longer than zip file deployments because this is the first time an image is being uploaded to the repository. Subsequent uploads will be faster because container images are broken up into layers, and only the layers that have changed will be pushed. Typically in future re-deployments the only layer that will be pushed is the layer that contains just your code.

To handle the larger size of container images compared zip files, Lambda applies smart caching that significantly improves the performance of subsequent invokes.

AWS CloudFormation deployment support

In the toolkit you can also deploy Lambda functions using CloudFormation by creating a project using the AWS Serverless Application project template for either C# or F#. When you pick the blueprint for the serverless application, you will have a basic .NET 5 Container Image blueprint and an ASP.NET Core 5 container image blueprint both available.

You can use the Dockerfile to customize the Lambda environment your .NET Lambda function is running in, but besides that the experience hasn’t changed much from zip based Lambda functions. There are a couple changes to be aware of in the CloudFormation template, specifically with the AWS::Serverless::Function resource. In a zip based Lambda function the CodeUri property is used as the relative path to the .NET project. If this property is not set, then the directory of the CloudFormation template is used. For image based Lambda functions the ImageUri property serves that same purpose. The other change is that the Handler and Runtime properties are no longer used. To identify the .NET code to run for an event, set the ImageConfig.Command property that will be translated into a CMD command for the container image.

Getting started from the .NET CLI

.NET developers who don’t use Visual Studio can have a similar experience using the .NET CLI. The Amazon.Lambda.Templates NuGet package provides the Lambda project templates to the dotnet new command. To deploy Lambda functions the .NET CLI tool, Amazon.Lambda.Tools provides the same deployment features that are offered in the AWS Toolkit for Visual Studio. Amazon.Lambda.Tools is also very useful when transitioning from deploying in Visual Studio to an automated CI/CD workflow.

For container image support, Amazon.Lambda.Templates has been updated with the same templates you saw in Visual Studio. You can install the AWS templates using the following command:


dotnet new -i Amazon.Lambda.Templates

Now when you run dotnet new, the same templates will be available at the command line:


Templates           Short Name                        Language Tags
------------------- -------------------------------------------- ------------ ----------------------
...
Lambda Empty Fun... lambda.EmptyFunction              [C#], F# AWS/Lambda/Function
Lambda Empty Fun... lambda.image.EmptyFunction        [C#], F# AWS/Lambda/Function
Lex Book Trip Sa... lambda.LexBookTripSample          [C#] AWS/Lambda/Function
Lambda Simple S3... lambda.S3                         [C#], F# AWS/Lambda/Function
...
Lambda Empty Ser... serverless.image.EmptyServerless  [C#], F# AWS/Lambda/Serverless
Lambda ASP.NET C... serverless.image.AspNetCoreWebAPI [C#] AWS/Lambda/Serverless
Lambda ASP.NET C... serverless.AspNetCoreWebApp       [C#] AWS/Lambda/Serverless
Serverless WebSo... serverless.WebSocketAPI           [C#] AWS/Lambda/Serverless
...
Console Application console                           [C#], F#, VB Common/Console
Class library classlib                                [C#], F#, VB Common/Library
...

With the template package installed, you can create a project using the short names listed above.


dotnet new lambda.image.EmptyFunction --output <project-name> --region <aws-region>

To deploy the project, install Amazon.Lambda.Tools using the following command:


dotnet tool install -g Amazon.Lambda.Tools

If the tool is already installed, be sure to update to the latest version using the following command:


dotnet tool update -g Amazon.Lambda.Tools

If you created a Lambda project like the lambda.image.EmptyFunction above, run the dotnet lambda deploy-function command in the project directory to deploy the project to Lambda. If you created a serverless project like serverless.image.EmptyServerless, run the dotnet lambda deploy-serverless command to deploy your project through CloudFormation.

You can use the --help switch to see the list of all of the possible command line arguments. The following new command line arguments were added to the deploy-function command to configure the Lambda function for containers:


--package-type                 The deployment package type for Lambda function. Valid values: image, zip
--image-entrypoint             Overrides the image's ENTRYPOINT when package type is set "--image-command                Overrides the image's CMD when package type is set "image".
--image-working-directory      Overrides the image's working directory when package type is set "image".
--image-tag                    Name and optionally a tag in the 'name:tag' format.

In addition, the following command line arguments were added to configure how the container image is built:


--dockerfile                   The docker file used build image. Default value is "Dockerfile".
--docker-build-options         Additional options passed to the "docker build" command.
--docker-build-working-dir     The directory to execute the "docker build" command from.
--docker-host-build-output-dir If set a "dotnet publish" command is executed on the host machine before executing "docker build". The output can be copied into image being built.

All command line arguments can be specified in the aws-lambda-tools-defaults.json file to avoid having to specify them on the command line. The project templates from either Visual Studio or the dotnet new command will have a few arguments prefilled to give an easy getting started experience. They can be customized based on your needs.

System.Drawing support with container images

So far we have focused on using container images just as a packaging feature, but they also provide powerful capabilities, allowing you to customize the Lambda environment your .NET code is running in.

For example, one of the most highly requested features by Lambda developers who use .NET is to use the System.Drawing libraries of .NET in Lambda. This is often needed for simple image manipulation or adding watermarks to images. To get System.Drawing to work in a Linux environment, you need to install the libgdiplus native dependency. This dependency is not installed in the Lambda managed runtime environment. When packaging a .NET Lambda function as a container image, the Dockerfile can be modified to include those dependencies in the container.

Add the last 3 RUN commands as shown below to the Dockerfile that is included in the .NET project templates . That will make sure libgdiplus, the required native dependency for System.Drawing, is included in the container.


FROM ecr.aws/lambda/dotnet:5.0

WORKDIR /var/task

COPY "bin/Release/net5.0/linux-x64/publish"  .

RUN yum install -y amazon-linux-extras 
RUN amazon-linux-extras install epel -y
RUN yum install -y libgdiplus  

Adding System.Drawing support is just one example of customization. With the 10 GB container image support in Lambda, you can now embed large machine learning models to use with ML.NET, including additional tools you need to shell out to from your .NET code, or whatever else you can think of.

Testing container image Lambda functions

The best way to test your Lambda functions is to write automated tests. The AWS .NET Lambda project template provides an option to create a .NET test project. You also have a few options for adhoc testing.

The AWS Mock .NET Lambda Test Tool has been updated to support .NET 5. The latest version of the AWS Toolkit for Visual Studio will take care of configuring the test tool for .NET 5 based Lambda functions. That includes installing the test tool from NuGet using the new Amazon.Lambda.TestTool-5.0 NuGet package. Check out the test tool’s README file for more information.

AWS provided base image comes with a local Lambda server baked in, a very useful feature that allows you start your Lambda function locally. Below is an example of starting the Lambda function locally and exposing port 9000 to send events into the function.


docker run -p 9000:8080 commandlinedemo:latest <function-handler-string>

Note that the last parameter is the function handler string that identifies the piece of .NET code to call. It is the value you specify for the Image Command when deploying the function. The format of the string is <assembly-name>::<full-type-name>::<function-name>. If you don’t want to specify the value on the command line when running locally or when deploying your function, you can also specify the value inside the Dockerfile like this:


CMD ["<assembly-name>::<full-type-name>::<function-name>"]

To send events into the local running Lambda function, you can send the JSON payload into the function using any HTTP tool. Here is an example using curl:


curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '\"hello world\"'

Conclusion

There are a lot of possibilities with the new container image support for Lambda using .NET Core 2.1, 3.1 and .NET 5. It’s easy to get started either from Visual Studio with the AWS Toolkit for Visual Studio or the .NET CLI using Amazon.Lambda.Tools. If you are having issues or want to tell us what you think about the new container support experience, reach out to us in our GitHub repositories:

–Norm