AWS Developer Tools Blog

.NET Core 3.0 on Lambda with AWS Lambda’s Custom Runtime

.NET Core 3.0 was recently released which brings in a host of new features and improvements. This release of .NET Core is called a “Current” release by the .NET team which means it will have a short lifecycle of support after the next release of .NET Core. The Lambda team’s policy is to support Long Term Support (LTS) versions of a runtime so .NET Core 3.0 will not be natively supported on AWS Lambda.

That doesn’t mean you can’t use .NET Core 3.0 on Lambda today though. With the Amazon.Lambda.RuntimeSupport NuGet package you can use any version of .NET Core including 3.0. This is possible because one of the great features of .NET Core is the ability to package up an application as a completely self contained deployment bundle. The following blog post shows how to use Amazon.Lambda.RuntimeSupport. https://aws.amazon.com/blogs/developer/announcing-amazon-lambda-runtimesupport/

Since the first Amazon.Lambda.RuntimeSupport blog post was written, which talks about creating the project using the dotnet new command, the AWS Toolkit for Visual Studio has added the project template as well.

ASP.NET Core 3.0

One of our popular features is to run ASP.NET Core applications using .NET Core and Lambda. This is possible with the Amazon.Lambda.AspNetCoreServer NuGet package. ASP.NET Core 3.0 does contain some breaking changes that affected Amazon.Lambda.AspNetCoreServer. To use Amazon.Lambda.AspNetCoreServer with ASP.NET Core 3.0 you need to use the new 4.0.0 version of the NuGet package.

What are the breaking changes?

Most users of Amazon.Lambda.AspNetCoreServer will not notice the breaking changes. Users that needed to customize the conversion of Amazon API Gateway’s request and responses to ASP.NET Core’s request and response might be affected by the following breaking changes.

The main breaking change is Amazon.Lambda.AspNetCoreServer was incorrectly exposing the type HostingApplication.Context, an ASP.NET Core type, that was technically public but it was in an namespace called Internal. As part of ASP.NET Core 3.0 the type was made internal; breaking Amazon.Lambda.AspNetCoreServer. The 4.0 release of Amazon.Lambda.AspNetCoreServer changes the virtual methods that were used to customize the conversation to no longer expose HostingApplication.Context.

The list of breaking changes are:

  • Removed PostCreateContext. This method had HostingApplication.Context as the parameter to allow the context to be customized.
  • Added PostMarshallHttpAuthenticationFeature. Allows subclasses to customize the ClaimsPrincipal for the incoming request.
  • Added PostMarshallItemsFeatureFeature. Allows subclasses to customize what is added to the Items collection of the HttpContext for the incoming request.

Additionally the Items collection on HttpContext has been changed to return null when attempting to get a value that does not exist. This was done to match the behavior of ASP.NET Core requests coming from Kestrel.

How do you enable Lambda for an ASP.NET Core 3.0 project?

There are 6 steps to take an existing ASP.NET Core 3.0 project and get it ready to deploy to Lambda. These steps will not affect how the ASP.NET Core project runs locally so you can still easily build and debug your project like any other ASP.NET Core project. In the examples below you will see references to the namespace CustomRuntimeAspNetCore30. This is the namespace of my sample project that you should replace that with the namespace of your ASP.NET Core project. Lets take a look at the 6 steps.

1. Add NuGet packages.

There are 2 NuGet packages that need to be included. The first is Amazon.Lambda.AspNetCoreServer which provides the functionality to convert API Gateway’s request and responses to ASP.NET Core’s request and responses. The second package is Amazon.Lambda.RuntimeSupport which provides support for using custom .NET Core Lambda runtimes in Lambda.

2. Add Lambda entry point.

Add a class typically called LambdaEntryPoint that will be a subclass of the Lambda function defined in Amazon.Lambda.AspNetCoreServer.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using System.IO;

namespace CustomRuntimeAspNetCore30
{
    public class LambdaEntryPoint :
        // When using an ELB's Application Load Balancer as the event source change 
        // the base class to Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction
        Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
    {
        /// <summary>
        /// The builder has configuration, logging and Amazon API Gateway already configured. The startup class
        /// needs to be configured in this method using the UseStartup<>() method.
        /// </summary>
        /// <param name="builder"></param>
        protected override void Init(IWebHostBuilder builder)
        {
            builder
                .UseStartup<Startup>();
        }
    }
}

3. Update Main function

When you use the .NET Core 2.1 native Lambda runtime the LambdaEntryPoint is loaded by Lambda through reflection. When using Lambda’s custom runtime feature it is up to the Main function to load the LambdaEntryPoint. To make sure the ASP.NET Core project works locally using Kestrel the code below only loads LambdaEntryPoint if the AWS_LAMBDA_FUNCTION_NAME environment variable exists. The Lambda runtime will define AWS_LAMBDA_FUNCTION_NAME when it executes a Lambda function.

When the code detects it is running in Lambda it sets up the LambdaBootstrap from Amazon.Lambda.RuntimeSupport which will listen for incoming Lambda events and send them to the LambdaEntryPoint.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.Json;

namespace CustomRuntimeAspNetCore30
{
    public class Program
    {

        public static void Main(string[] args)
        {
            if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME")))
            {
                CreateHostBuilder(args).Build().Run();
            }
            else
            {
                var lambdaEntry = new LambdaEntryPoint();
                var functionHandler = (Func<APIGatewayProxyRequest, ILambdaContext, Task<APIGatewayProxyResponse>>)(lambdaEntry.FunctionHandlerAsync);
                using (var handlerWrapper = HandlerWrapper.GetHandlerWrapper(functionHandler, new JsonSerializer()))
                using (var bootstrap = new LambdaBootstrap(handlerWrapper))
                {
                    bootstrap.RunAsync().Wait();
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

4. Add CloudFormation template

To make sure the REST API from API Gateway is created when our Lambda function is deployed a CloudFormation template needs to be added to the project. Add a file called serverless.template to the project with the JSON content below. In this case the template is a single AWS::Serverless::Function resource. Notice in comparison to using the .NET Core 2.1 Lambda runtime the Runtime property is set to provided. Also the Handler field is no longer meaningful since the Main function will always be called. Handler cannot be left blank so you must put a string value in the field.


{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.",
  "Parameters": {},
  "Conditions": {},
  "Resources": {
    "AspNetCoreFunction": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "not-required",
        "Runtime": "provided",
        "CodeUri": "",
        "MemorySize": 256,
        "Timeout": 30,
        "Role": null,
        "Policies": [
          "AWSLambdaFullAccess"
        ],
        "Environment": {
          "Variables": {
            "LAMBDA_NET_SERIALIZER_DEBUG": "true"
          }
        },
        "Events": {
          "ProxyResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/{proxy+}",
              "Method": "ANY"
            }
          },
          "RootResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/",
              "Method": "ANY"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
        "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
      }
    }
  }
}

5. Add defaults file

The Visual Studio deployment wizard and the .NET Lambda command line tools (Amazon.Lambda.Tools) both look for a file called aws-lambda-tools-defaults.json for settings to use for deployments. The most important value that needs to be set in this file is the msbuild-parameters value. Notice how that setting below tells the underlying call to the dotnet publish command to publish as a self contained package for Linux. This means all of the .NET Core 3.0 runtime assemblies will be added to the package as well as our project. The setting also changes the assembly name to be bootstrap when creating the package bundle. This is required because Lambda’s custom runtimes look for an executable called bootstrap to invoke which will start the process that will handle Lambda events.


{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "default",
  "region": "",
  "configuration": "Release",
  "s3-prefix": "CustomRuntimeAspNetCore30/",
  "template": "serverless.template",
  "template-parameters": "",
  "msbuild-parameters": "--self-contained true /p:AssemblyName=bootstrap",
  "framework": "netcoreapp3.0",
  "s3-bucket": "",
  "stack-name": "CustomRuntimeAspNetCore30"
}

6. Turn on Visual Studio deployment wizard.

If you are using the AWS Toolkit for Visual Studio the project file has to be edited to enable Lambda deployment. Edit the project file to include the AWSProjectType with a value of Lambda in the PropertyGroup collection.


<PropertyGroup>
  <TargetFramework>netcoreapp3.0</TargetFramework>
  <AWSProjectType>Lambda</AWSProjectType>
</PropertyGroup>

Once you have completed these 6 steps you are ready to deploy your ASP.NET Core project to Lambda by either:

Right clicking the project in Visual Studio

Or by executing the deploy-serverless command from Amazon.Lambda.Tools


dotnet lambda deploy-serverless

Cold starts with .NET Core 3.0

Cold starts can be affected when using .NET Core 3.0 because of the larger package bundle that includes the .NET Core 3.0 runtime. .NET Core 3.0 come with a lot of performance improvements that help offset the increase in cold starts compared to the cold start time in the native .NET Core 2.1 Lambda runtime. The most exciting of these improvements is the new “ReadyToRun” images. This is a flag you can set when publishing your application that prejits the .NET assemblies for the target deployment environment. This saves the .NET Core runtime from doing a lot of work during startup converting the assemblies to a native format. So far in my testing I have found by enabling this feature the cold starts for a .NET Core 3.0 self contained Lambda function is at or close to the same as the .NET Core 2.1 native Lambda runtime. Every function has different dependencies and requirements for initialization so your performance will probably be different. It is definitely worth investigating the use of ReadyToRun.

In order to enable ReadyToRun, edit the aws-lambda-tools-defaults.json file and add “/p:PublishReadyToRun=true” to the msbuild-parameters value.

The caveat for using ReadyToRun is you have to create the Lambda package bundle on Linux to match the Lambda environment. My normal development environment is Windows. So what I do is use aws-lambda-tools-defaults.json in my development environment and use AWS CodeBuild with a separate copy of aws-lambda-tools-defaults.json called codebuild-defaults.json when I want to do a production deployment. To use the separate config file in codebuild I set the –config-file switch to codebuild-defaults.json.


dotnet lambda package —config-file codebuild-defaults.json

Conclusion

We know the .NET community is excited about .NET Core 3.0 and so are we at AWS. With the Amazon.Lambda.RuntimeSupport package you can get started with any version of .NET Core as soon as they are released. Plus with the new ReadyToRun feature of .NET Core 3.0 the performance is greatly improved. Now is a great time to checkout .NET Core 3.0 on Lambda. As always, we welcome your feedback which you can provide for .NET Core and Lambda on our GitHub repo at https://github.com/aws/aws-lambda-dotnet.

–Norm