Deploying AWS Lambda Functions with AWS SAM

Deploying AWS Lambda Functions with AWS SAM

Working with serverless technologies like Lambda Functions is great, but it is easy to lose control of what to deploy (due to the large number of resources we usually have to deploy). In a previous post, we explore a solution for that, Serverless Framework, and today we will try AWS Serverless Application Model (SAM).

The AWS Serverless Application Model (AWS SAM) is an open-source framework for building serverless applications. It provides shorthand syntax to express functions, APIs, databases, and event source mappings. With just a few lines per resource, you can define the application you want and model it using YAML. During deployment, AWS SAM transforms and expands the SAM syntax into AWS CloudFormation syntax, enabling you to save time and build, test, and deploy serverless applications faster.

Pre-requisites

  • Have an IAM User with programmatic access.
  • Install the Amazon Lambda Templates (dotnet new -i Amazon.Lambda.Templates)
  • Install the Amazon Lambda Tools (dotnet tool install -g Amazon.Lambda.Tools)
  • Install AWS SAM CLI.

Let's see the code

We developed three AWS Lambda Functions to create, get and list tasks (the task data will be stored in a DynamoBD table). One of the reasons to try AWS SAM is the support to deploy .NET 7 applications, which allows us to use Native AOT compilation (to reduce the cold start times). Download the solution here and open it. Go to the FunctionLibrary project and open the TaskRepository.cs file:

public class TasksRepository
{
    private AmazonDynamoDBClient _amazonDynamoDB;
    private string _tableName;

    public TasksRepository(AmazonDynamoDBClient amazonDynamoDB, string tableName)
    {
        _amazonDynamoDB = amazonDynamoDB;
        _tableName = tableName;
    }

    public System.Threading.Tasks.Task Save(Task task)
    {
        var putItemRequest = new PutItemRequest
        {
            TableName = _tableName,
            Item = new Dictionary<string, AttributeValue> {
                    {
                        "id",
                        new AttributeValue {
                        S = task.Id.ToString(),
                    }
                    },
                    {
                        "description",
                        new AttributeValue {
                        S = task.Description
                        }
                    },
                    {
                        "title",
                        new AttributeValue {
                        S = task.Title
                        }
                    }
                }
        };

        return _amazonDynamoDB.PutItemAsync(putItemRequest);
    }

    public async Task<Task?> Get(Guid id)
    {
        var request = new GetItemRequest
        {
            TableName = _tableName,
            Key = new Dictionary<string, AttributeValue>() { { "id", new AttributeValue { S = id.ToString() } } },
        };

        var response = await _amazonDynamoDB.GetItemAsync(request);

        if(response.HttpStatusCode!= System.Net.HttpStatusCode.OK)
        {
            return null;
        }

        var task = new Task()
        {
            Id = Guid.Parse(response.Item["id"].S),
            Description = response.Item["description"].S,
            Title = response.Item["title"].S
        };

        return task;
    }

    public async Task<Task[]> List()
    {
        var request = new ScanRequest()
        {
            TableName = _tableName
        };

        var response = await _amazonDynamoDB.ScanAsync(request);

        return response.Items.Select(item => new Task()
        {
            Id = Guid.Parse(item["id"].S),
            Description = item["description"].S,
            Title = item["title"].S
        }).ToArray();
    }
}

We are using the DynamoDB low-level API because the DynamoDBContext has some issues when used with Native AOT. Now, go to the PostTaskFunction project and open the Function.cs file:

public class Function
{
    private static TasksRepository _tasksRepository;

    static Function()
    {
        var tableName = Environment.GetEnvironmentVariable("TABLE_NAME");
        _tasksRepository = new TasksRepository(new AmazonDynamoDBClient(), tableName);
    }

    private static async System.Threading.Tasks.Task Main()
    {
        Func<APIGatewayHttpApiV2ProxyRequest, ILambdaContext, Task<APIGatewayHttpApiV2ProxyResponse>> handler = FunctionHandler;
        await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
            .Build()
            .RunAsync();
    }

    public static async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var request = JsonSerializer.Deserialize(input.Body, LambdaFunctionJsonSerializerContext.Default.RegisterTaskRequest)!;

        var task = new Task() { Id = Guid.NewGuid(), Description = request.Description, Title = request.Title };

        await _tasksRepository.Save(task);

        var body = JsonSerializer.Serialize(new RegisterTaskResponse(task.Id), LambdaFunctionJsonSerializerContext.Default.RegisterTaskResponse);

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }
}

public record RegisterTaskRequest(string Description, string Title);

public record RegisterTaskResponse(Guid Id);

[JsonSerializable(typeof(RegisterTaskResponse))]
[JsonSerializable(typeof(RegisterTaskRequest))]
[JsonSerializable(typeof(Task))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyRequest))]
[JsonSerializable(typeof(APIGatewayHttpApiV2ProxyResponse))]
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
{
}

Just regular code generated by the lambda.NativeAOT template. Quite similar code is present under the GetTaskFunction and ListTaskFunction projects. Open the template.yaml file:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Sample SAM Template for aot-app

Globals:
  Function:
    Timeout: 10
    Handler: bootstrap
    Runtime: provided.al2
    MemorySize: 512
    Architectures:
      - x86_64
    Environment:
      Variables:
        TABLE_NAME: !Ref TasksTable

Resources:
  TasksTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String
      TableName: taskstable

  ListTaskFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: dotnet7
    Properties:
      CodeUri: ./src/ListTaskFunction/
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        ListTask:
          Type: Api
          Properties:
            Path: /tasks
            Method: get

  PostTaskFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: dotnet7
    Properties:
      CodeUri: ./src/PostTaskFunction/
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        ListTask:
          Type: Api
          Properties:
            Path: /tasks
            Method: post

  GetTaskFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: dotnet7
    Properties:
      CodeUri: ./src/GetTaskFunction/
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        ListTask:
          Type: Api
          Properties:
            Path: /tasks/{id}
            Method: get

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL for Prod stage for Tasks function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/tasks/"
  • Transform: For AWS SAM templates, you must include this section with a value of AWS::Serverless-2016-10-31
  • Description: A text string that describes the template.
  • Globals: Properties that are common to all your serverless functions, APIs, and simple tables.
  • Resources: The stack resources and their properties. Here we can use all the resources available in Cloud Formation plus:
    • AWS::Serverless::Function: Creates an AWS Lambda function, an AWS Identity and Access Management (IAM) execution role, and event source mappings that trigger the function.
    • AWS::Serverless::SimpleTable: Creates a DynamoDB table with a single attribute primary key. It is useful when data only needs to be accessed via a primary key.
    • AWS::Serverless::Api: Creates a collection of Amazon API Gateway resources and methods that can be invoked through HTTPS endpoints.
    • AWS::Serverless::HttpApi: Creates an Amazon API Gateway HTTP API, which enables you to create RESTful APIs.
    • AWS::Serverless::StateMachine: Creates an AWS Step Functions state machine.
    • AWS::Serverless::Application: Embeds a serverless application from the AWS Serverless Application Repository or an Amazon S3 bucket as a nested application.
    • AWS::Serverless::Connector: Provides a simple and secure method of provisioning permissions between your serverless application resources.
    • AWS::Serverless::LayerVersion: Creates a Lambda LayerVersion that contains library or runtime code needed by a Lambda Function.
  • Outputs: The values that are returned whenever you view your stack's properties.
  • Other sections are Parameters, Mappings, Conditions and Metadata.

At the solution level, run sam build to build the serverless application and then sam deploy --guided, this command will guide you through the deployment. And that's it our application is up and running. You can check the official documentation here. Thanks, and happy coding.