Testing AWS Lambda Functions Locally Using SAM

Raul NaupariRaul Naupari
5 min read

Testing AWS Lambda functions locally can be a crucial step in the development process, as it allows developers to identify and resolve issues before deploying their code to the cloud. In this regard, AWS SAM provides a method for testing our API called sam local, which enables us to perform tasks such as:

  • Run AWS Lambda functions locally.

  • Test API Gateway endpoints.

  • Simulate event triggers from services like S3, SNS, and SQS.

  • And even debug AWS Lambda functions.

All this works using Docker containers to replicate the AWS runtime environment on your local machine. When we issue a sam local command, SAM reads your template file, pulls the appropriate Docker image for the specified Lambda runtime, then packages our code, dependencies, and environment variables into a Docker container, simulating the actual AWS Lambda environment as closely as possible.

Pre-requisites

  • Ensure you have an IAM User with programmatic access.

  • Install the Amazon Lambda Templates with this command: dotnet new -i Amazon.Lambda.Templates

  • Install the Amazon Lambda Tools with this command: dotnet tool install -g Amazon.Lambda.Tools

  • Install the AWS SAM CLI.

  • Ensure that Docker Desktop is up and running.

The Lambda Function

Run the following commands to create the project that will host our Lambda functions:

dotnet new lambda.EmptyFunction -n MyLambdaFunctions -o .
dotnet add src/MyLambdaFunctions package Amazon.Lambda.APIGatewayEvents
dotnet add src/MyLambdaFunctions package Amazon.Lambda.SQSEvents
dotnet new sln -n SamLocal
dotnet sln add --in-root src/MyLambdaFunctions

Open the solution, navigate to the MyLambdaFunctions project, and update the Function.cs file as follows:

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;

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

public class Function
{
    public string FunctionHandler(string input, ILambdaContext context)
    {
        context.Logger.LogInformation($"new message: {input}");

        return "Hello world";
    }

    public APIGatewayHttpApiV2ProxyResponse ApiFunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        context.Logger.LogInformation($"new message: {input.Body}");

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = @"{""Message"":""Hello World""}",
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public void MessageFunctionHandler(SQSEvent evnt, ILambdaContext context)
    {
        foreach (var record in evnt.Records)
        {
            context.Logger.LogInformation($"new message: {record.Body}");
        }
    }
}

We are defining three Lambda functions, two of which have a request and a message as triggers. Create a template.yml file with the following content:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  SAM Local

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambdaFunctions::MyLambdaFunctions.Function::FunctionHandler
      CodeUri: ./src/MyLambdaFunctions/

  MyApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambdaFunctions::MyLambdaFunctions.Function::ApiFunctionHandler
      CodeUri: ./src/MyLambdaFunctions/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get

  MyMessageFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: dotnet6
      Handler: MyLambdaFunctions::MyLambdaFunctions.Function::MessageFunctionHandler
      CodeUri: ./src/MyLambdaFunctions/
      Policies:  
        - SQSPollerPolicy:
            QueueName: !GetAtt SQSQueue.QueueName
      Events:
        SQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt SQSQueue.Arn
            BatchSize: 10

  SQSQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: "mySQSQueue"

Run sam build to build the resources declared in the template. This must be done every time changes are made.

Invoke a Lambda Function Locally

The standard syntax of the command is:

sam local invoke <functionlogicalid> --event <file>

Create a folder named events and create a file called myfunction.txt with the following content:

"My lambda function input"

Run sam local invoke MyFunction --event .\events\myfunction.txt to see the following output:

START RequestId: a996bd93-68f1-4011-867d-9646ec188542 Version: $LATEST
2023-08-21T00:52:01.317Z        a996bd93-68f1-4011-867d-9646ec188542    info    new message: My lambda function input
END RequestId: a996bd93-68f1-4011-867d-9646ec188542
REPORT RequestId: a996bd93-68f1-4011-867d-9646ec188542  Init Duration: 0.09 ms  Duration: 195.77 ms     Billed Duration: 196 ms Memory Size: 512 MB   Max Memory Used: 512 MB
"Hello world"

Generate an Event to Invoke a Lambda Function

We can use sam local generate-event command to generate events for supported AWS services. We can run sam local generate-event to display the list of supported AWS services. The standard syntax of the command is:

sam local generate-event <service> <event>

For example, by running the command sam local generate-event apigateway -h, we can see all the events available for the API Gateway service:

Options:
  -h, --help  Show this message and exit.

Commands:
  authorizer          Generates an Amazon API Gateway Authorizer Event
  aws-proxy           Generates an Amazon API Gateway AWS Proxy Event
  http-api-proxy      Generates an Amazon API Gateway Http API Event
  request-authorizer  Generates an Amazon API Gateway Request Authorizer Event

And, by running sam local generate-event apigateway http-api-proxy -h, we can see different sections of the event that can be modified:

Options:
  --body TEXT         Specify the body name you'd like, otherwise the default
                      = {"test":"body"}
  --stage TEXT        Specify the stage name you'd like, otherwise the default
                      = $default
...

Run sam local generate-event apigateway http-api-proxy > .\events\myapifunction.json and ensure the generated file is in UTF-8 encoding. Now, run sam local invoke MyApiFunction --event .\events\myapifunction.json to see the following output:

START RequestId: 40b6d72f-6c3d-4e05-ac1c-9f8d94a5662c Version: $LATEST
2023-08-21T01:19:15.296Z        40b6d72f-6c3d-4e05-ac1c-9f8d94a5662c    info    new message: eyJ0ZXN0IjoiYm9keSJ9
END RequestId: 40b6d72f-6c3d-4e05-ac1c-9f8d94a5662c
REPORT RequestId: 40b6d72f-6c3d-4e05-ac1c-9f8d94a5662c  Init Duration: 0.19 ms  Duration: 248.69 ms     Billed Duration: 249 ms Memory Size: 512 MB   Max Memory Used: 512 MB
{"statusCode":200,"headers":{"Content-Type":"application/json"},"body":"{{\"Message\":\"Hello World\"}}","isBase64Encoded":false}

The same exercise can be performed for our third Lambda function, run sam local generate-event sqs receive-message --body "Hi" > .\events\mymessagefunction.json and then sam local invoke MyMessageFunction --event .\events\mymessagefunction.json:

Mounting C:\Source\sam-local\.aws-sam\build\MyMessageFunction as /var/task:ro,delegated inside runtime container
START RequestId: ab673232-20c0-407d-8412-c49213b48566 Version: $LATEST
2023-08-21T01:26:22.898Z        ab673232-20c0-407d-8412-c49213b48566    info    new message: Hi
END RequestId: ab673232-20c0-407d-8412-c49213b48566
REPORT RequestId: ab673232-20c0-407d-8412-c49213b48566  Init Duration: 0.26 ms  Duration: 187.51 ms     Billed Duration: 188 ms Memory Size: 128 MB   Max Memory Used: 128 MB

Starting a Local API Gateway

The standard syntax of the command is:

sam local start-api <options>

One of the most useful options is --warm-containers:

  • eager: Containers for all functions are loaded at startup and persist between invocations.

  • lazy: Containers are only loaded when each function is first invoked and persisted for additional invocations.

Without this option, the command will create a new container each time our function is invoked. Run sam local start-api --warm-containers lazy and invoke the Lambda function through the browser:

All the code is available here. Thanks, and happy coding.

0
Subscribe to my newsletter

Read articles from Raul Naupari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Raul Naupari
Raul Naupari

Somebody who likes to code