AWS Lambda Layers in .NET

AWS Lambda Layers in .NET

Lambda layers provide a convenient way to package libraries and other dependencies that you can use with your Lambda functions. Using layers reduces the size of uploaded deployment archives and makes it faster to deploy your code.

Moving the concept above to the .NET ecosystem, Lambda Layers allow us to put dependencies (assemblies) outside of the deployment package, which reduces the size of the .zip file that has to be uploaded whenever we deploy a function. This approach is useful when working with large dependencies or when multiple functions share the same dependencies. Sounds easy, but how to take advantage of this feature in .NET? Runtime package store is the answer to that question:

Starting with .NET Core 2.0, it's possible to package and deploy apps against a known set of packages that exist in the target environment

Therefore, the steps to use Lamba Layers in .NET are:

  • Create the Lambda Layer with all the dependencies we want.

  • In our Lambda Function, add a reference to the Lambda Layer.

  • Deploy our Lambda Function without including the dependencies included in the Lambda Layer.

Prerequisites

  • An IAM User with programmatic access.

  • Install AWS CLI.

  • Install the Amazon Lambda Templates (dotnet new -i Amazon.Lambda.Templates).

  • Install the Amazon Lambda Tools (dotnet tool install -g Amazon.Lambda.Tools).

  • An s3 bucket (aws s3api create-bucket --bucket <MY_BUCKET_NAME> --region <MY_REGION> --create-bucket-configuration LocationConstraint=<MY_REGION>).

Creating a Lambda Function

Disclaimer: .NET Core 3.1 will be used in this post until we know why it does not work with .NET 6.

Run the following commands to create a Lambda Function:

dotnet new lambda.EmptyFunction -n HelloWorldApi -o .
dotnet add src/HelloWorldApi package Amazon.Lambda.APIGatewayEvents

Update the HelloWorldApi.csproj file as follow:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Nullable>enable</Nullable>
   <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AWSProjectType>Lambda</AWSProjectType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.6.0" />
    <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.0" />
  </ItemGroup>
</Project>

Update the Function.cs file as follows:

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

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

namespace HelloWorldApi
{
    public class Function
    {

        public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
        {
            return new APIGatewayHttpApiV2ProxyResponse
            {
                Body = @"{""Message"":""Hello world!""}",
                StatusCode = 200,
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };
        }
    }
}

Create a template.xml file (AWS SAM) with the following content:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Lambda Layers

Resources:     
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Runtime: dotnetcore3.1
      Architectures:
        - x86_64    
      Handler: HelloWorldApi::HelloWorldApi.Function::FunctionHandler
      CodeUri: ./src/HelloWorldApi/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /
            Method: get

Let's deploy the Lambda Function to see the size of the package:

dotnet lambda deploy-serverless --region <MY_REGION> -t template.yaml -sb <MY_BUCKET_NAME>

The package size, without Lambda Layers, is around 50 Kb.

Creating a Lambda Layer

Create a file named packages.csproj as follows:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AWSProjectType>Lambda</AWSProjectType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.6.0" />
    <PackageReference Include="Amazon.Lambda.Core" Version="2.1.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.0" />
  </ItemGroup>
</Project>

Run the following command:

dotnet lambda publish-layer MyDotNetLambdaLayer31 -sb <MY_BUCKET_NAME> --layer-type runtime-package-store --package-manifest packages.csproj --framework netcoreapp3.1 --region <MY_REGION>

To inspect the Lambda Layer, run the command: dotnet lambda get-layer-version --arn <MY_LAMBA_LAYER_ARN>. The result will be something like this:

Layer ARN:               <MY_LAMBA_LAYER_ARN>
Version Number:          1
Created:                 3/11/2023 3:45 PM
License Info:
Compatible Runtimes:     dotnetcore3.1
Layer Type:              .NET Runtime Package Store

.NET Runtime Package Store Details:
Manifest Location:       s3://<MY_BUCKET_NAME>/MyDotNetLambdaLayer31-638141643366026806/artifact.xml
Packages Optimized:      False
Packages Directory:      /opt/dotnetcore/store

Manifest Contents
-----------------------
<StoreArtifacts>
  <Package Id="Amazon.Lambda.APIGatewayEvents" Version="2.6.0" />
  <Package Id="Amazon.Lambda.Core" Version="2.1.0" />
  <Package Id="Amazon.Lambda.Serialization.SystemTextJson" Version="2.3.0" />
</StoreArtifacts>

As we are using the Runtime package store in the background, the artifact.xml file contains the list of all the packages in our store. The package.zip has all the assemblies and, unzipped, represents the content of the Lambda Layer.

Using the Lambda Layer

Modify the template.xml as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Lambda Layers

Resources:     
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Runtime: dotnetcore3.1
      Architectures:
        - x86_64    
      Handler: HelloWorldApi::HelloWorldApi.Function::FunctionHandler
      CodeUri: ./src/HelloWorldApi/
      Layers : 
        - <MY_LAMBA_LAYER_ARN>
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /
            Method: get

Run dotnet lambda deploy-serverless --region <MY_REGION> -t template.yaml -sb <MY_BUCKET_NAME> to update the Lambda Function:

Now, the package size is around 10 Kb. But what exactly is going on here?

  • At the beginning of the deployment, the Lambda Layer was inspected and the artifact.xml file was downloaded.

  • The artifact.xml file was used in the dotnet publish command, which told it not to include packages listed there.

  • The DOTNET_SHARED_STORE environment variable was added to the Lambda Function with the value /opt/dotnetcore/store/.

  • When your Lambda Function runs, the Lambda Layer will be downloaded and made available to your Lambda Function at runtime, allowing you to use its dependencies.

For more information about Lambda Layers in .NET, follow this link. The code is available here. Thank you, and happy coding.