C# and AWS Lambdas, Part 8 – .NET 6, inside a Container, inside a Lambda

Full source code available here.

After playing with .NET 5 libraries and Web API applications inside Lambda containers I wanted to see if I could get a .NET 6 library running too.

I cloned https://github.com/aws/aws-lambda-dotnet, and started digging into this Dockerfile.
With a few minor alterations this should be all I needed to change.

The Dockerfile pulls down a .NET runtime from https://dotnetcli.azureedge.net/dotnet.

Grabs an image to build the internal .NET support into the Lambda –

FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS builder

Updating those two elements should be all that is needed.

.NET 6 Lambda Image
I updated the runtime to the latest .NET 6 preview, available from https://versionsof.net/core/6.0/6.0.0-preview.2/. In the attached example, I use the .NET 6 Runtime, but you might want to use the .NET 6 ASP.NET Runtime.

I also updated the builder to .NET 6 (but I found this wasn’t strictly necessary).

FROM mcr.microsoft.com/dotnet/nightly/sdk:6.0.100-preview.2-bullseye-slim-amd64 AS builder

To build the image run this inside the Dotnet6LambdaImage directory –

docker build -t dotnet6_runtime_aws_lambda .

That’s that, you now have a base image to run .NET 6 inside a Lambda container. Now to the library.

The Library
You can create a simple .NET Lambda application using a command like –

dotnet new lambda.EmptyFunction --name Dotnet6LambdaLibrary

You’ll need to update the .csproj file to make the target framework net6.0.

The attached example has stripped away the tests and the directory structure.

Add a Dockerfile that looks like this to the directory where the .csproj file is –

FROM dotnet6_runtime_aws_lambda as base

# https://hub.docker.com/_/microsoft-dotnet-sdk/
FROM mcr.microsoft.com/dotnet/nightly/sdk:6.0.100-preview.2-alpine3.13-amd64 as build

WORKDIR /source

# there is no point doing the restore as it's own step because the donet publish doesn't work with the --no-restore flag.
# COPY *.csproj .
# RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /var/task
COPY --from=build /app/publish .
CMD ["Dotnet6LambdaLibrary::Dotnet6LambdaLibrary.Function::FunctionHandler"]

The first line sets the image built above as the base.
The last line tells the Lambda what to call when running the function.
The rest in the middle are fairly standard Docker commands.

From inside the Dotnet6LambdaLibrary directory, build the library –

docker build -t dotnet6dockerlambdalibrary .

Deploy to AWS as shown in part 6 of this series.

Test as shown in part 1 of this series.

If you want to build and deploy .NET 6 Web API applications to Lambda containers, change the runtime that is pulled down in Dotnet6LambdaImage/Dockerfile to a more suitable one found here https://versionsof.net/core/6.0/6.0.0-preview.2/, then check out part 7 of this series to see how to hook up a HTTP Gateway to the Lamnbda.

I think that wraps up this series on C# and .NET in AWS Lambdas, I thought I was going to post four blogs on the topic, but ended up with eight. Hope they are helpful.

Full source code available here.

C# and AWS Lambdas, Part 7 – .NET 5 Web API inside a Container inside a Lambda, with API Gateway in front

Full source code available here.

In my previous post I showed how to build a .NET 5 library inside a docker image and deploy it to an AWS Lambda. This post is a small extension on that.

I’m going to build a .NET 5 Web API application, turn it into a docker image, deploy it to an AWS Lambda, and connect an API Gateway to the Lambda to call the controller inside the application, inside the container, inside the Lambda!

The Web API application
This is the same as what I did in part 2 of this series.

Use the AWS template –

dotnet new serverless.AspNetCoreWebAPI --name Dotnet5DockerWebAPILambda

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

You can change what the controllers return if you like, in the attached code example I have –

public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public ActionResult Get()
    {
        return Ok($"Hello World - {DateTime.Now}");
    }
    // snip...

The Dockerfile

FROM public.ecr.aws/lambda/dotnet:5.0 as base

FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine-amd64 AS build
WORKDIR /source

COPY *.csproj .
RUN dotnet restore

COPY . .
RUN dotnet publish --no-restore -c Release -o /app/publish

FROM base AS final
WORKDIR /var/task
COPY --from=build /app/publish .
CMD ["Dotnet5DockerWebAPILambda::Dotnet5DockerWebAPILambda.LambdaEntryPoint::FunctionHandlerAsync"]

This Dockerfile uses the AWS .NET 5 base image from their Elastic Container Repository.

Note the CMD that specifies the entry point to the application.

The Lambda
Follow the procedure described in part 6 of this series.

Build the image.

docker build -t dotnet5dockerwebapilambda .

Create a repository to store the image.
Push the image to the repository.

Create the Lambda (same as in part 6).
Select the container image you uploaded.

You won’t be able to test this Lambda function as easily as in the previous post, I have not had luck with the AWS provided test Json files from the .NET project template.

Connecting the API Gateway
I gave an example of how to connect an API Gateway to a Lambda in the second post in this series, the procedure is the same .

Click Create API.

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

Hit Add integration, choose Lambda, and pick the Lambda function created earlier.
Select Version 2.0.
Give the API a name.
Hit next.

Change resource path to $default – this is a wildcard that will match any controller in the Web API 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 –

“Welcome to running ASP.NET Core on AWS Lambda”

Change the url to include the route of the controller and by adding /api/values, the output from that action method should look like –

“Hello World – 3/23/2021 10:22:42 PM”

The Base Image
You might have noticed that I used the same base image in this and the previous post, but the previous post was a .Net 5 library. This means that the base image can run both libraries and Web API, not ideal if all you want to run is the library because you are getting the full .Net 5 ASP.NET Runtime. See update 2 of part 6 of this series for some brief instructions on building a base image with the .NET 5 Runtime.

Full source code available here.

C# and AWS Lambdas, Part 6 – .NET 5 inside a Container inside a Lambda

Full source code available here.

A few months ago AWS released a feature allowing Lambdas to run container images, for larger applications this is easier to work with than a zip file or set of layers, it also lets you move your already containerized apps to Lambda with a small effort.

I was interested to see if I could get a .NET 5 “Hello World” application running in this manner. There is a blog on the AWS site explaining how to do this with Visual Studio and the AWS Toolkit, but primarily use VS Code so I could not leverage those tools, and it would be fun to figure out.

UPDATE 1 – when I published this blog post I was unaware of a pre-made .NET 5 Docker image for AWS Lambda so I built my own following instructions found on an AWS GitHub repo. Those instructions are below.
But it is much easier to use the AWS provided image available here – https://gallery.ecr.aws/lambda/dotnet and use public.ecr.aws/lambda/dotnet:5.0.

UPDATE 2 – the AWS Lambda image above seems to have the full .NET 5 ASP.NET Runtime as opposed to the .NET 5 Runtime, if all you are running in the container is a library, then having the full .NET 5 ASP.NET Runtime will be larger than you need. As of now, I don’t see an image on the AWS ECR page for the .NET 5 Runtime. But you can build it your self following the below instructions, with one change, on line 49 of this file https://github.com/aws/aws-lambda-dotnet/blob/087590ce99274e16e26d37e1dfd73b0b71d1230a/LambdaRuntimeDockerfiles/dotnet5/Dockerfile, change the linked tar.gz to the appropriate file in here https://versionsof.net/core/5.0/.

Building your own base image (optional)
You can’t use just any docker image for a .NET 5 application, it has to have special Lambda and AWS components included, here is how to build your own, if you want to, from the AWS code on GitHub.

But it is much easier to use the one image AWS provides – https://gallery.ecr.aws/lambda/dotnet
Now that you have a base image, let’s move to the application.

Building the hello world container
Create a new .NET Lambda using –

dotnet new lambda.EmptyFunction --name HelloWorldLambdaContainer

This crates a simple library that converts an input string to uppercase.
If you wanted to, you could build this, zip it and deploy it to a Lambda as I showed in part 1 of this series.

But I want to put this in a container.

You need Docker for the rest of this, there are plenty of tutorials out there including one from my friend Steve Gordon.

In the source code directory add a file named Dockerfile with the below content. Note that it’s using the image I put on Docker Hub.

FROM public.ecr.aws/lambda/dotnet:5.0 AS base

FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim as build

WORKDIR /source

COPY *.csproj .
RUN dotnet restore

COPY . .
RUN dotnet publish --no-restore -c Release -o /app/publish

FROM base AS final
WORKDIR /var/task
COPY --from=build /app/publish .
CMD ["HelloWorldLambdaContainer::HelloWorldLambdaContainer.Function::FunctionHandler"]

Everything is in place, build the image.

docker build -t helloworldlambdacontainer .

Over in AWS we need a repository to store the image.

Go to the Elastic Container Registry.

Create a repository.

Navigate into the repository and you will see a button labeled “View push commands”. Follow those instructions to upload the container image to repository.

That’s everything done here, over to the Lambda.

The Lambda
This is similar to what I showed in part one, but this time the function will be based on a container image.

Create a new Lambda function, select Container image from the options.

Give the function a name.

Hit Browse images, and select the container image from the repository created above.

It will take a few moments for AWS to create the function. Once it is up, 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”.

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 is a .NET 5 library running inside container, running inside a Lambda, not bad!

But what if you could get Kestrel up and running and map API Gateway requests to it, that would be fun…and that’s in the next post of this series.

Full source code available here.

C# and AWS Lambdas, Part 5 – Updating the Zip in S3 and Updating the Running Lambda, with Pulumi IaC

Full source code available here.

This post pulls together a few threads I’ve been working on – creation of lambda to run .NET, storing the zip in S3, and updating the .NET lambda when the zip in S3 is updated.

This one took quite while to put together -the permissions, roles and policies were not obvious and I hope it will be of help to you.
This is not a blog post on CI/CD, I am cutting corners by using Pulumi to upload the zip files initially, and then use the AWS command line to send zips to S3. In a future set of posts I will show how to use GitHub Actions to build the infrastructure, and to compile and deploy the .NET lambda directly to S3 from GitHub.

The idea
I want to have a lambda that runs .NET code stored in a zip file in S3. I want to be able to update the zip, and have the .NET lambda run the code in the new zip. I had hoped this would be a little tick box on the lambda, but sadly there is no such box.

Instead I have a second lambda (referred to as the updater lambda) that is triggered by an update on specified bucket in S3. This updater lambda in turn calls an update on the .NET lambda and within a few seconds the .NET lambda will be running the new code. Doesn’t sound easy, but I didn’t think it would be too hard, but take a look at the number of resources needed.

What’s needed
A role to run .NET lambda.
A role to run lambda that updates the .NET lambda, I’m calling this the updater.
A policy to give the updater permissions to update the .NET lambda and S3.
A policy attachment for the .NET lambda.
A policy attachment for the updater lambda.
An S3 bucket.
An S3 bucket object.
The .NET lambda pointing at the bucket and bucket object.
The zip file for for the .NET lambda.
The updater lambda with variables passed in to verify the update of the .NET lambda.
The zip file for the updater lambda – Node.js.
Permission for the bucket to call the updated lambda.
A bucket notification with attached permissions.
Reduce the bucket accessible to the public (no necessary, but good).

That’s a lot more than the tick box I was hoping for.

The code

using Pulumi;
using S3 = Pulumi.Aws.S3;
using Aws = Pulumi.Aws;

class MyStack : Stack
{
    public MyStack()
    {
        string resource_prefix = "PulumiHelloWorldAutoUpdate";

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

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

        // gives the lamaba permissions to other lambdas and s3 - too many permissions, but this is a demo.
        var lambdaUpdatePolicy = new Aws.Iam.Policy($"{resource_prefix}_S3_Lambda_Policy", new Aws.Iam.PolicyArgs{
            PolicyDocument = 
@"{
    ""Version"": ""2012-10-17"",
    ""Statement"": [
        {
            ""Sid"": """",
            ""Effect"": ""Allow"",
            ""Action"": [
                ""s3:*"",
                ""logs:*"",
                ""lambda:*""
            ],
            ""Resource"": ""*""
        }
    ]
}"
        });

        // attach a simple policy to the hello world lambda.
        var lambdaHelloWorldAttachment = new Aws.Iam.PolicyAttachment($"{resource_prefix}_LambdaHelloWorldPolicyAttachment", new Aws.Iam.PolicyAttachmentArgs
        {
            Roles =
            {
                lambdaHelloWorldRole.Name
            },
            PolicyArn = Aws.Iam.ManagedPolicy.AWSLambdaBasicExecutionRole.ToString(),
        });

        // attach the custom policy to the role that runs the update lambda.
        var lambdaUpdateAttachment = new Aws.Iam.PolicyAttachment($"{resource_prefix}_LambdaUpdatePolicyAttachment", new Aws.Iam.PolicyAttachmentArgs
        {
            Roles =
            {
                lambdaUpdateRole.Name
            },
            PolicyArn = lambdaUpdatePolicy.Arn,
        });

        var s3Bucket = new S3.Bucket($"{resource_prefix}_S3Bucket", new S3.BucketArgs
        {
            BucketName = "pulumi-hello-world-auto-update-s3-bucket",
            Versioning = new Aws.S3.Inputs.BucketVersioningArgs
            {
                Enabled = true,
            },
            Acl = "private"
        });

        var s3BucketObject = new S3.BucketObject($"{resource_prefix}_ZipFile", new S3.BucketObjectArgs
        {
            Bucket = s3Bucket.BucketName.Apply(name => name),
            Acl = "private",
            Source = new FileArchive("./Lambdas/helloworld_no_date/helloworld.zip"),
            Key = "helloworld.zip"
        });

        // this is the lambda that runs .NET code
        var lambdaHelloWorldFunction = new Aws.Lambda.Function($"{resource_prefix}_LambdaHelloWorldFunction", new Aws.Lambda.FunctionArgs
        {
            Handler = "HelloWorldLambda::HelloWorldLambda.Function::FunctionHandler",
            MemorySize = 128,
            Publish = false,
            ReservedConcurrentExecutions = -1,
            Role = lambdaHelloWorldRole.Arn,
            Runtime = Aws.Lambda.Runtime.DotnetCore3d1,
            Timeout = 4,
            S3Bucket = s3Bucket.BucketName,
            S3Key = s3BucketObject.Key
        });

        // this is the lambda triggered by an upload to S3 and replaces the zip in the above lambda
        var lambdaUpdateFunction = new Aws.Lambda.Function($"{resource_prefix}_LambdaUpdateFunction", new Aws.Lambda.FunctionArgs
        {
            Handler = "index.handler",
            MemorySize = 128,
            Publish = false,
            ReservedConcurrentExecutions = -1,
            Role = lambdaUpdateRole.Arn,
            Runtime = Aws.Lambda.Runtime.NodeJS14dX,
            Timeout = 4,
            Code = new FileArchive("./Lambdas/LambdaUpdater/index.zip"),
            Environment = new Aws.Lambda.Inputs.FunctionEnvironmentArgs
            {
                Variables = new InputMap<string> {{"s3Bucket", s3Bucket.BucketName}, {"s3Key", "helloworld.zip"}, {"functionToUpdate", lambdaHelloWorldFunction.Name}}
            }
        });

        var s3BucketPermissionToCallLambda = new Aws.Lambda.Permission($"{resource_prefix}_S3BucketPermissionToCallLambda", new Aws.Lambda.PermissionArgs
        {
            Action = "lambda:InvokeFunction",
            Function = lambdaUpdateFunction.Arn,
            Principal = "s3.amazonaws.com",
            SourceArn = s3Bucket.Arn,
        });

        var bucketNotification = new S3.BucketNotification($"{resource_prefix}_S3BucketNotification", new Aws.S3.BucketNotificationArgs
        {
            Bucket = s3Bucket.Id,
            LambdaFunctions = 
            {
                new Aws.S3.Inputs.BucketNotificationLambdaFunctionArgs
                {
                    LambdaFunctionArn = lambdaUpdateFunction.Arn,
                    Events = 
                    {
                        "s3:ObjectCreated:*",
                    },
                }
            },
        }, new CustomResourceOptions
        {
            DependsOn = 
            {
                s3BucketPermissionToCallLambda,
            },
        });

        // keep the contents bucket private
        var bucketPublicAccessBlock = new S3.BucketPublicAccessBlock($"{resource_prefix}_PublicAccessBlock", new S3.BucketPublicAccessBlockArgs
        {
            Bucket = s3Bucket.Id,
            BlockPublicAcls = false,  // leaving these two false because I need them this way 
            IgnorePublicAcls = false, // for a post about GitHub Actions that I'm working on
            BlockPublicPolicy = true,
            RestrictPublicBuckets = true
        });

        this.LambdaUpdateFunctionName = lambdaUpdateFunction.Name;
        this.LambdaHelloWorldFunctionName = lambdaHelloWorldFunction.Name;
        this.S3Bucket = s3Bucket.BucketName;
        this.S3Key = s3BucketObject.Key;
    }

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

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

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

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

Below is the code of the updater lambda. The if checks to make sure that the lambda.updateFunctionCode(..) runs only if the expected file in S3 is updated. The environmental variables were passed in via the Pulumi code above.

const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();

exports.handler = (event) => {
    
    if (event.Records[0].s3.bucket.name == process.env.s3Bucket && event.Records[0].s3.object.key == process.env.s3Key)
    {
        var params = {
            FunctionName: process.env.functionToUpdate,
            S3Bucket: event.Records[0].s3.bucket.name, 
            S3Key: event.Records[0].s3.object.key
        };
        
        // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#updateFunctionCode-property
        lambda.updateFunctionCode(params, function(err, data) {
            if (err) // an error occurred
            {
                console.log(err, err.stack);
            }
            else
            {   
                console.log(data);  
            }
        });
    }
    else
    {
        console.log("bucket name or s3 key did not match expected values.");
        console.log("expected bucket name: " + process.env.s3Bucket + " actual: " + event.Records[0].s3.bucket.name);
        console.log("expected s3 key: " + process.env.s3Key + " actual: " + event.Records[0].s3.object.key);
    }
    console.log("Exiting");
};

The zip attached to this blog post has all the source code needed, you don’t have to add or change anything.

Running it

From the console, run –

pulumi up 

At the end you should see something like this –

Note the outputs. These are the name of your lambdas and the s3 bucket and key –

Outputs:
    LambdaHelloWorldFunctionName: "PulumiHelloWorldAutoUpdate_LambdaHelloWorldFunction-???????"
    LambdaUpdateFunctionName    : "PulumiHelloWorldAutoUpdate_LambdaUpdateFunction-???????"
    S3Bucket                    : "pulumi-hello-world-auto-update-s3-bucket"
    S3Key                       : "helloworld.zip"

Go the aws console, and test the lambda as shown in part 1 of this blog series.

You should get output like this – “HELLO WORLD”.

Updating the zip in S3
Now to try out the real functionality, updating the zip in S3 and see if it runs in the .NET lambda.

In the attached source there is a Lambdas directory with two subdirectories – helloworld_no_date and helloworld_with_date. They contain two variations of the .NET application. The first converts the input text to uppercase, the second converts the input text to uppercase and adds the current date and time.

You can run the below commands to upload each zip file and try out the lambda. A few seconds after you upload, the .NET lambda will use that zip.

// no date
aws s3 cp ./Lambdas/helloworld_no_date/helloworld.zip s3://pulumi-hello-world-auto-update-s3-bucket/helloworld.zip
// with date
aws s3 cp ./Lambdas/helloworld_with_date/helloworld.zip s3://pulumi-hello-world-auto-update-s3-bucket/helloworld.zip

If you don’t want to go into the AWS UI console to try out the lambda you can invoke it from the command line, but you need to swap the function name below for the one in the output of the pulumi up command –

aws lambda invoke --function-name PulumiHelloWorldAutoUpdate_LambdaHelloWorldFunction-??????? --payload '"hello world"' /dev/stdout

This was long tough one, but I’ve learned a lot about AWS, Pulumi, and even GitHub Actions (more on that soon).

Full source code available here.

C# and AWS Lambdas, Part 4 – Storing the Zip in S3, Setup with Pulumi IaC

Full source code available here.

In the previous post I showed how to use Pulumi to create a lambda, API gateway and upload a zip of Web API application directly to the lambda.

In the post I’m going to use S3 to store the zip of the of a simple .NET application (not a Web API app) and point the lambda at it, brining all the resources up using Pulumi. One drawback of using S3 to store the zip for a lambda is that when you update the zip in S3, AWS doesn’t deploy the new zip to the lambda, but I will show a way of handling that in the next blog post.

Every time I write these posts it feels like a lot of work to set up the infrastructure, learn about the AWS components, policies, etc. But then when I run pulumi destroy -y and pulumi up -y as I make changes I appreciate the speed and predictability.

What’s needed

A lambda role.
A policy to execute the lambda.
An S3 bucket.
Block all public access to the bucket – optional.
Put zip in the bucket.
The lambda function pointing to the zip in the bucket.

A few of these are the same as in the previous post, but the S3 resources are new.

Note that I included BucketPublicAccessBlock section, this makes the S3 bucket more secure than it would be under the default settings.

The code
Here is the code that sets it all up. I’ve included a helloworld.zip file in the attached source code zip. This is NOT how you would normally upload the zip to S3, but it’s easy for demonstration purposes. In a more realistic scenario, you would a CI/CD pipeline to compile the code and drop it in S3.

using Pulumi;
using S3 = Pulumi.Aws.S3;
using Aws = Pulumi.Aws;
class MyStack : Stack
{
    public MyStack()
    {

        var lambdaRole = new Aws.Iam.Role("PulumiHelloWorld_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("PulumiHelloWorld_LambdaPolicyAttachment", new Aws.Iam.PolicyAttachmentArgs
        {
            Roles =
            {
                lambdaRole.Name
            },
            PolicyArn = Aws.Iam.ManagedPolicy.AWSLambdaBasicExecutionRole.ToString(),
        });

        var bucket = new S3.Bucket("PulumiHelloWorld_S3Bucket", new S3.BucketArgs
        {
            BucketName = "pulumi-hello-world-s3-bucket",
            Acl = "private"
        });

        var bucketPublicAccessBlock = new S3.BucketPublicAccessBlock("PulumiHelloWorld_PublicAccessBlock", new S3.BucketPublicAccessBlockArgs
        {
            Bucket = bucket.Id,
            BlockPublicAcls = true,
            BlockPublicPolicy = true,
            RestrictPublicBuckets = true,
            IgnorePublicAcls = true
        });

        var bucketObject = new S3.BucketObject("PulumiHelloWorld_ZipFile", new S3.BucketObjectArgs
        {
            Bucket = bucket.BucketName.Apply(name => name),
            Acl = "private",
            Source = new FileArchive("helloworld.zip")
        });

        var lambdaFunction = new Aws.Lambda.Function("PulumiHelloWorld_LambdaFunction", new Aws.Lambda.FunctionArgs
        {
            Handler = "HelloWorldLambda::HelloWorldLambda.Function::FunctionHandler",
            MemorySize = 128,
            Publish = false,
            ReservedConcurrentExecutions = -1,
            Role = lambdaRole.Arn,
            Runtime = Aws.Lambda.Runtime.DotnetCore3d1,
            Timeout = 4,
            S3Bucket = bucket.BucketName,
            S3Key = bucketObject.Key
        });

        this.LambdaFunctionName = lambdaFunction.Name;
    }

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

To test out the lambda, open it in the AWS console, navigate to the lambda and add a test as described in part 1 of this series of posts.

Full source code available here.