Testing .NET 6 Lambda Containers with the Runtime Interface Emulator

Download full source code.

It’s easy to run a .NET serverless application inside a container on AWS Lambda.

If you use the dotnet templates, it will set up an application and test project for you.

In the test project, you can easily write tests to test your code from inside Visual Studio, VS Code, Rider, or command line.

But what if you want to test the application when it is running inside the container? You could deploy it to AWS Lambda and run your tests there, but there is an easier way - the Lambda Runtime Interface Emulator, more on that later, first things first though.

Some background

When using .NET serverless Lambda that responds to HTTP requests, the .NET application is not running Kestrel or any other web server. Instead, an AWS API Gateway receives the HTTP request, examines it, converts it to an APIGatewayProxyRequest in JSON form, and sends this JSON to the Lambda. The original HTTP request is never sent to the Lambda, the values inside the HTTP request (query string, body, etc.), are copied to the APIGatewayProxyRequest JSON.

Setting up

Create a serverless application using -

dotnet new serverless.image.EmptyServerless --name SimpleLambdaContainerForRIE

This will give you a src directory with the application code, and a test directory with a test project. The test project uses xUnit and may be enough for your testing needs.

If you look inside the src\SimpleLambdaContainerForRIE\Dockerfile you will see that it copies binaries from the bin/Release/lambda-publish directory. This is important when we compile the application.

The code

There will be a Get method in the Function.cs file, change it to the following -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public APIGatewayProxyResponse Get(APIGatewayProxyRequest request, ILambdaContext context)
{
    context.Logger.LogInformation("Get Request\n");
    Console.WriteLine($"Id = {request.PathParameters["Id"]}");
    Console.WriteLine($"request: {JsonSerializer.Serialize(request)}");
    var response = new APIGatewayProxyResponse
    {
        StatusCode = (int)HttpStatusCode.OK,
        Body = $"You were looking for something with an Id of : {request.PathParameters["Id"]}",
        Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
    };

    return response;
}

On line 4, you grab the Id and on line 5, you print out the whole of the APIGatewayProxyRequest object.

Add a Post method -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public APIGatewayProxyResponse Post(APIGatewayProxyRequest request, ILambdaContext context)
{
    context.Logger.LogInformation("Post Request\n");
    Person p = JsonSerializer.Deserialize<Person>(request.Body);
    Console.WriteLine($"The person is {p}");
    var response = new APIGatewayProxyResponse
    {
        StatusCode = (int)HttpStatusCode.Created,
        Body = $"You sent a new person - {p} ",
        Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
    };
    return response;
}

On line 4, the incoming Person is deserialized from the request.Body.

All this is fairly straightforward, the tricky part was constructing HTTP requests that exercised these methods. But first, you have to get the container running.

Building the application, image

Change to the src/SimpleLambdaContainerForRIE directory.

As mentioned above the Dockerfile expects the compiled application to be located in a specific directory, build it using -

dotnet build -c Release -o .\bin\Release\lambda-publish\ 

Build the container image -

docker build -t simple_lambda_container_for_rie:latest .

Running the container and hitting the methods

Your container image has both your code and the Lambda Runtime Interface Emulator (RIE). There are two methods to test, Get and Post.

You have to pass one of the methods as the entrypoint to the container as you run it, meaning you can only test one method at a time.

The entrypoint is made up of AssemblyName::Namespace.Class::Method.

Start with the Get method -

docker run -it -p 9000:8080 simple_lambda_container_for_rie:latest SimpleLambdaContainerForRIE::SimpleLambdaContainerForRIE.Functions::Get

Now you can send an HTTP request to the Lambda Runtime Interface Emulator on the container, which will then execute the Get method, the request must be in the form of an APIGatewayProxyRequest.

I like using the REST Client extension for VS Code, here is the request you need to send -

POST http://localhost:9000/2015-03-31/functions/function/invocations HTTP/1.1
content-type: application/json

{
    "PathParameters": {
        "Id": "999"
    }
}

To test the Post method, restart the container using the Post entrypoint -

POST http://localhost:9000/2015-03-31/functions/function/invocations HTTP/1.1
content-type: application/json

{
    "Body": "{ \"FirstName\": \"Alan\", \"LastName\": \"Adams\"  }"
}

You will receive an HTTP response and see log information printed to the console.

If you want to make changes to your source, rebuild the code, rebuild the image, and start the container here are the three commands in one line -

dotnet build -c Release -o .\bin\Release\lambda-publish\ ; docker build -t simple_lambda_container_for_rie:latest . ; docker run -it -p 9000:8080 simple_lambda_container_for_rie:latest SimpleLambdaContainerForRIE::SimpleLambdaContainerForRIE.Functions::Get

That’s it, your application code running inside a container, and tested locally using the RIE.

Figuring out the shape of APIGatewayProxyRequest

Working out what the correct JSON payload was for the request to RIE was not obvious, you could examine the source code of APIGatewayProxyRequest, or navigate to the code stub via Visual Studio (Code), and work backwards from there. You could also use the test project to construct APIGatewayProxyRequest, and serialize it, but that requires knowing a bit about the shape of the APIGatewayProxyRequest.

I did it by deploying the application to the real Lambda service, and altering the Get and Post methods to return the APIGatewayProxyRequest object in the response to each method call. Then I hit the endpoints with traditional HTTP requests and each time I saw how my HTTP request was converted into APIGatewayProxyRequest.

You can see examples of the JSON representation of for the Get and Post methods in APIGatewayProxyRequest in the attached zip.

Download full source code.

comments powered by Disqus

Related