Kilhoffer
Kilhoffer

Reputation: 32914

Can we deploy a .Net Core 3 service to AWS Lambda using the Serverless Framework?

Now that .Net Core 3 has dropped and AWS announced Custom Runtimes, I'm looking to take advantage of some of the new .Net Core 3 features. Unfortunately, I'm coming up short when trying to find information on how to do this using the Serverless Framework. Has anyone out there done this yet? And if so, is there a good online resource on how to do so?

Upvotes: 0

Views: 1180

Answers (1)

Andrew Rands
Andrew Rands

Reputation: 154

The short answer is 'yes'.

Amazon state:

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/

We have successfully deployed a .Net Core 3.0 Web Api to AWS Serverless.

Here's how I did it:

  1. Added NuGet packages:
 Amazon.Lambda.AspNetCoreServer 
 Amazon.Lambda.RuntimeSupport
  1. Added Lambda entry point
    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>();
        }
    }
  1. Updated Main function
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>();
                });
    }
}
  1. Added serverless.template file:
{
  "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/"
      }
    }
  }
}
  1. Added aws-lambda-tools-defaults.json
{
  "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"
}
  1. Turned on Visual Studio deployment wizard.

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>

You can then deploy the ASP.NET Core project to Lambda by right-clicking the project in Visual Studio and selecting 'Publish to AWS Lambda...'

More info here:

https://aws.amazon.com/blogs/developer/net-core-3-0-on-lambda-with-aws-lambdas-custom-runtime/

Note that this process may error if the webapi project you are publishing has references to other assemblies. This is because in the deployment (step 5), it attempts to rename all assemblies to 'bootstrap'. then you will need to:

Rename your project's assembly name in the csproj file to 'bootstrap' (without quotes). Modify aws-lambda-tools-defaults.json so that the line:

"msbuild-parameters": "--self-contained true /p:AssemblyName=bootstrap",

becomes

"msbuild-parameters"  : "--self-contained true",

Note also that logging seems to have changed from .Net 2.1.

I tried to implement logging as per .Net Core 2.1. That is:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
       Host.CreateDefaultBuilder(args)
         .ConfigureLogging(logging =>
         {
            logging.AddAWSProvider();
            logging.SetMinimumLevel(LogLevel.Debug);
         })
         .ConfigureWebHostDefaults(webBuilder =>
         {
            webBuilder.UseStartup<startup>();
         });

(and similar code in LambdaEntryPoint.cs)

However, this doesn't work. Looking at the code, it is trying to find the IConfiguration ImplementationInstance and this is null.

According to this https://github.com/aspnet/AspNetCore/issues/14400, this breaking change is by design.

To get logging working in .Net Core 3, you need to add code to Startup.Configure():

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            var loggingConfigSection = Configuration.GetAWSLoggingConfigSection();
            var awsLoggerProvider = new AWSLoggerProvider(loggingConfigSection);
            loggerFactory.AddProvider(awsLoggerProvider);

            // rest of code
        }

Upvotes: 3

Related Questions