C# and AWS Lambdas, Part 3 – Pulumi IaC for Web API and an API Gateway

Full source code available here.

In this the third in a series of posts on using .NET in AWS lambdas I build on the previous where I connected a Http Gateway to a lambda running a Web API application. In that post I built the infrastructure by hand, i.e. via the GUI, pointing and clicking.

In this post I will show how to build everything with Pulumi. I’ve written a few posts on Pulimi before, you can find them here.

As with all IaC, the hard part isn’t the IaC itself, it’s knowing what to do on the platform you are deploying to.
In the previous post I worked out all the required steps and components and got everything working. But the UI takes care of some things that you have to explicitly do in IaC and this is where the challenge is as you get familiar with the platform you are working on.

What’s needed
When you add up all the components needed, it feels like a lot, but eighty lines of C# code sets it all up.

IAM role for the lambda.
Policy attachment for the above role letting it execute the lambda.
The lambda function. This where the Web API code runs.
A Http Gateway.
An integration between the Http Gateway and the lambda.
A route on the Http Gateway that forwards to the integration.
A stage on the Http Gateway.
Permissions for the Http Gateway to execute the lambda.

That last one was not obvious, thank you to Piers Karsenbarg at Pulumi for his assistance.

The code
You need to have Pulumi installed, check their site for instructions.

Here is the stack to setup all the components listed above.

using Pulumi;
using Aws = Pulumi.Aws;

class MyStack : Stack
{
    public MyStack()
    {

        var lambdaRole = new Aws.Iam.Role("PulumiWebApiGateway_LambdaRole", new Aws.Iam.RoleArgs
        {
            AssumeRolePolicy = 
@"{
    ""Version"": ""2012-10-17"",
    ""Statement"": [
        {
        ""Action"": ""sts:AssumeRole"",
        ""Principal"": {
            ""Service"": ""lambda.amazonaws.com""
        },
        ""Effect"": ""Allow"",
        ""Sid"": """"
        }
    ]
}",
        });

        var lambdaPolicyAttachment = new Aws.Iam.PolicyAttachment("PulumiWebApiGateway_LambdaPolicyAttachment", new Aws.Iam.PolicyAttachmentArgs
        {
            Roles =
            {
                lambdaRole.Name
            },
            PolicyArn = Aws.Iam.ManagedPolicy.AWSLambdaBasicExecutionRole.ToString(), 
        });

        var lambdaFunction = new Aws.Lambda.Function("PulumiWebApiGateway_LambdaFunction", new Aws.Lambda.FunctionArgs
        {
            Handler = "WebAPILambda::WebAPILambda.LambdaEntryPoint::FunctionHandlerAsync",
            MemorySize = 128,
            Publish = false,
            ReservedConcurrentExecutions = -1,
            Role = lambdaRole.Arn,
            Runtime = Aws.Lambda.Runtime.DotnetCore3d1,
            Timeout = 4,
            Code = new FileArchive("WebAPILambda.zip"), // I put the zip file in the same dir as this code for this demo, but you should not do this.
        });
        System.Console.WriteLine(Aws.Iam.ManagedPolicy.AWSLambdaBasicExecutionRole.ToString());

        var httpApiGateway = new Pulumi.Aws.ApiGatewayV2.Api("PulumiWebApiGateway_ApiGateway", new Pulumi.Aws.ApiGatewayV2.ApiArgs
        {
            ProtocolType = "HTTP",
            RouteSelectionExpression = "${request.method} ${request.path}",
        });

        var httpApiGateway_LambdaIntegration = new Pulumi.Aws.ApiGatewayV2.Integration("PulumiWebApiGateway_ApiGatewayIntegration", new Pulumi.Aws.ApiGatewayV2.IntegrationArgs
        {
            ApiId = httpApiGateway.Id,
            IntegrationType = "AWS_PROXY",
            IntegrationMethod = "POST",
            IntegrationUri = lambdaFunction.Arn,
            PayloadFormatVersion = "2.0",
            TimeoutMilliseconds = 30000,
        });

        var httpApiGatewayRoute = new Pulumi.Aws.ApiGatewayV2.Route("PulumiWebApiGateway_ApiGatewayRoute", new Pulumi.Aws.ApiGatewayV2.RouteArgs
        {
            ApiId = httpApiGateway.Id,
            RouteKey = "$default",
            Target = httpApiGateway_LambdaIntegration.Id.Apply(id => $"integrations/{id}"),
        });

        var httpApiGatewayStage = new Pulumi.Aws.ApiGatewayV2.Stage("PulumiWebApiGateway_ApiGatewayStage", new Pulumi.Aws.ApiGatewayV2.StageArgs
        {
            ApiId = httpApiGateway.Id,
            AutoDeploy = true,
            Name = "$default",
        });

        var lambdaPermissionsForApiGateway = new Aws.Lambda.Permission("PulumiWebApiGateway_LambdaPermission", new Aws.Lambda.PermissionArgs
        {
            Action = "lambda:InvokeFunction",
            Function = lambdaFunction.Name,
            Principal = "apigateway.amazonaws.com",
            SourceArn = Output.Format($"{httpApiGateway.ExecutionArn}/*") // note it's the ExecutionArn.
            // SourceArn = httpApiGateway.ExecutionArn.Apply(arn => $"{arn}/*") // this is another way of doing the same thing
        });

        this.ApiEndpoint = httpApiGateway.ApiEndpoint.Apply(endpoint =>  $"{endpoint}/api/values");
    }

    [Output]
    public Output<string> ApiEndpoint { get; set; }
}

Run –

pulumi up

This is will show you what is going to be deployed, if it looks ok select yes.

After a short wait you should see something like this the below image indicating that everything has been setup

Now you can click on the APIEndpoint url and it should execute your lambda and return the hello world message.

Full source code available here.

C# and AWS Lambdas, Part 2 – Web API and an API Gateway

Full source code available here.

In the previous post I created a lambda that executed a C# console application. In this post I walkthrough creating a .NET 3.1 Web API application inside a lambda and making it reachable from the web, just like a normal Web API application.

First, update the lambda templates, they have fixed a bug in the csproj file of the template, that I described in the in first post, and added some new templates.

dotnet new -i Amazon.Lambda.Templates::5.1.0

Another useful thing to install are the Amazon.Lambda.Tools package, but I don’t use them in this post.

dotnet tool install -g Amazon.Lambda.Tools 

Or update it if you have it installed already.

dotnet tool update -g Amazon.Lambda.Tools

Create a new application.

dotnet new serverless.AspNetCoreWebAPI --name WebAPILambda

In LambdaEntryPoint.cs change the class it inherits from to Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction.

For the sake of a demo I’m going to change very little, and show a GET and a POST call.

Add a Person class.

namespace WebAPILambda.Models
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public override string ToString()
        {
            return $"{FirstName} {LastName}";
        }
    }
}

Replace the ValuesController class with this –

[Route("api/[controller]")]
public class ValuesController : ControllerBase
{

    [HttpGet]
    public string Get()
    {
        return $"Hello World from inside a lambda {DateTime.Now}";
    }

    [HttpGet("{id}")]
    public string Get(int id)
    {
        return $"You asked for {id}";
    }

    [HttpPost]
    public IActionResult Post([FromBody] Person person)
    {
        return Ok($"You sent {person.ToString()}");
    }
}

Build it. You can try it by starting the application locally and use like a normal Web API application.

Zip the contents of the WebAPILambda/bin/Debug/netcoreapp3.1 directory in a file named WebAPILambda.zip.

We’re done with the code.

Create the Lambda

I’m not going to show the detailed steps because they are described in part 1 of this series of posts.

But there is one difference – make this the handler WebAPILambda::WebAPILambda.LambdaEntryPoint::FunctionHandlerAsync.

Upload the zip as shown in part 1.

You should be able to paste in the content of the test/WebAPILambda.Tests/SampleRequests/ValuesController-Get.json from the source code into the lambda test feature, but the Json generated by the template does NOT work with a LambdaEntryPoint that inherits from APIGatewayHttpApiV2ProxyFunction.

That’s the lambda setup.

Now we need to send some web requests to it.

API Gateway
There are a few choices here, I am going to show how to use the Http API because it is simpler.

Click Create API.

Top of the list should be HTTP API, click Build.

Hit Add integration, choose Lambda, and pick the lambda function defined in the previous step.
Select Version 2.0.
Give the API a name.
Hit next.

For route configuration leave the METHOD as ANY.
Change resource path to /api/values – this matches the route in Web API controller in the application.

In Configure Stages don’t change anything for this simple example.
Hit next.

In Review and Create confirm that it looks correct and hit Create in the bottom right.

You should now have an API Gateway setup that looks something like –

Click on the Invoke URL and you should get a response like –

{
message: “Not Found”
}

Change the url to include the route of the controller and by adding /api/values and you will get –

That is hitting the GET method. In the attached source code there is file named test.http, it has an example of a POST method.

There you go, an API inside a lambda reachable from the web! Not so hard after you’ve done it once.

Full source code available here.

C# and AWS Lambdas, Part 1 – Hello World

Full source code available here.

This is the first in a series of posts on using .NET with AWS Lambdas. It will start with the simplest example that converts a lowercase string to an uppercase string, but by the end you will be running a .NET Web API powered by lambda, fronted by an API gateway where all the infrastructure is setup by Pulumi – this will take a few posts over the next while.

Getting Started
Install the AWS Lambda Templates.

dotnet new -i Amazon.Lambda.Templates

Now you can create projects based on these templates.

Here is the full list of available templates.

Templates                                                 Short Name                                        Language          Tags                  
----------------------------------------------------      --------------------------------------------      ------------      ----------------------
Order Flowers Chatbot Tutorial                            lambda.OrderFlowersChatbot                        [C#]              AWS/Lambda/Function   
Lambda Custom Runtime Function (.NET 5.0)                 lambda.CustomRuntimeFunction                      [C#], F#          AWS/Lambda/Function   
Lambda Detect Image Labels                                lambda.DetectImageLabels                          [C#], F#          AWS/Lambda/Function   
Lambda Empty Function                                     lambda.EmptyFunction                              [C#], F#          AWS/Lambda/Function   
Lex Book Trip Sample                                      lambda.LexBookTripSample                          [C#]              AWS/Lambda/Function   
Lambda Simple Application Load Balancer Function          lambda.SimpleApplicationLoadBalancerFunction      [C#]              AWS/Lambda/Function   
Lambda Simple DynamoDB Function                           lambda.DynamoDB                                   [C#], F#          AWS/Lambda/Function   
Lambda Simple Kinesis Firehose Function                   lambda.KinesisFirehose                            [C#]              AWS/Lambda/Function   
Lambda Simple Kinesis Function                            lambda.Kinesis                                    [C#], F#          AWS/Lambda/Function   
Lambda Simple S3 Function                                 lambda.S3                                         [C#], F#          AWS/Lambda/Function   
Lambda Simple SNS Function                                lambda.SNS                                        [C#]              AWS/Lambda/Function   
Lambda Simple SQS Function                                lambda.SQS                                        [C#]              AWS/Lambda/Function   
Lambda ASP.NET Core Web API                               serverless.AspNetCoreWebAPI                       [C#], F#          AWS/Lambda/Serverless 
Lambda ASP.NET Core Web Application with Razor Pages      serverless.AspNetCoreWebApp                       [C#]              AWS/Lambda/Serverless 
Serverless Detect Image Labels                            serverless.DetectImageLabels                      [C#], F#          AWS/Lambda/Serverless 
Lambda DynamoDB Blog API                                  serverless.DynamoDBBlogAPI                        [C#]              AWS/Lambda/Serverless 
Lambda Empty Serverless                                   serverless.EmptyServerless                        [C#], F#          AWS/Lambda/Serverless 
Lambda Giraffe Web App                                    serverless.Giraffe                                F#                AWS/Lambda/Serverless 
Serverless Simple S3 Function                             serverless.S3                                     [C#], F#          AWS/Lambda/Serverless 
Step Functions Hello World                                serverless.StepFunctionsHelloWorld                [C#], F#          AWS/Lambda/Serverless 
Serverless WebSocket API                                  serverless.WebSocketAPI                           [C#]              AWS/Lambda/Serverless 

The Application
For this example we are going to use lambda.EmptyFunction.

dotnet new lambda.EmptyFunction --name HelloWorldLambda

This sets up two new projects, one for the lambda, and one for the tests.

There seems to be a bug in the template as it is missing line 6, this is very important. Without it, you will get errors like – An assembly specified in the application dependencies manifest (HelloWorldLambda.deps.json) was not found: package: ‘Amazon.Lambda.Core’, version: ‘1.1.0’

Be sure to add

<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

to HelloWorldLambda.csproj.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AWSProjectType>Lambda</AWSProjectType>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.0.1" />
  </ItemGroup>
</Project>

The code of the lambda can stay the same, it takes an input string and converts it to uppercase and returns a string.

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace HelloWorldLambda
{
    public class Function
    {
        
        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?.ToUpper();
        }
    }
}

Build this, and zip up the all the files in the bin/debug/netcoreapp3.1 directory. This zip will be uploaded to the AWS soon.

The Lambda Function
For this post I’m going to show how to create the lambda using the AWS UI. In later post I’ll create it with Pulumi.

Open the AWS Lambada page – https://console.aws.amazon.com/lambda.

Click Create Function.

Give the function a name – HelloWorld is a good choice.
Change the runtime to .NET Core 3.1 (C#/PowerShell).

Click Create Function in the bottom right (no shown in the image above).

Upload the zip created above by clicking Action in the Function Code section of the screen.

Edit the Handler to read

HelloWorldLambda::HelloWorldLambda.Function::FunctionHandler 

This matches the namespace in the application we created.

Open the test tool in the top right of the screen and click “Configure test events”.

Set the event name, and replace the body with this “hello world”.

That’s everything setup. Let’s run it.

Running the Lambda
Hit the “Test” button in the top right.

You should see something like – “Execution result: succeeded(logs)”. Expand the Details and you will see “HELLO WORLD” and bunch of other information about the execution of the lambda.

That’s it, a C# .NET Core 3.1 Hello World lambda up and running.

In the next post I’ll do something a little more interesting, I’ll put an API Gateway in front of a lambda, so the lambda can be called from the web.

Full source code available here.