Testing .NET 6 Lambda Containers with the Runtime Interface Emulator
Want to learn more about AWS Lambda and .NET? Check out my A Cloud Guru course on ASP.NET Web API and Lambda.
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 -
1public APIGatewayProxyResponse Get(APIGatewayProxyRequest request, ILambdaContext context)
2{
3 context.Logger.LogInformation("Get Request\n");
4 Console.WriteLine($"Id = {request.PathParameters["Id"]}");
5 Console.WriteLine($"request: {JsonSerializer.Serialize(request)}");
6 var response = new APIGatewayProxyResponse
7 {
8 StatusCode = (int)HttpStatusCode.OK,
9 Body = $"You were looking for something with an Id of : {request.PathParameters["Id"]}",
10 Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
11 };
12
13 return response;
14}
On line 4, you grab the Id
and on line 5, you print out the whole of the APIGatewayProxyRequest
object.
Add a Post
method -
1public APIGatewayProxyResponse Post(APIGatewayProxyRequest request, ILambdaContext context)
2{
3 context.Logger.LogInformation("Post Request\n");
4 Person p = JsonSerializer.Deserialize<Person>(request.Body);
5 Console.WriteLine($"The person is {p}");
6 var response = new APIGatewayProxyResponse
7 {
8 StatusCode = (int)HttpStatusCode.Created,
9 Body = $"You sent a new person - {p} ",
10 Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
11 };
12 return response;
13}
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.