Disclaimer: I am a consultant at Amazon Web Services, and this is my personal blog. The opinions expressed here are solely mine and do not reflect the views of Amazon Web Services (AWS). Any statements made should not be considered official endorsements or statements by AWS.
In this post, you will learn how you can create, debug, and deploy a .NET 8 Lambda function on AWS using Visual Studio.
Since .NET 8 has just released a few days back, a managed runtime for this is not yet available (at the time of writing this blog). Therefore, we will use Custom Runtime to run a .NET 8 Lambda function.
Since we will use Custom Runtime in this post to deploy the Lambda function, it is important to understand some of the Lambda Internals such as what is Lambda Runtime, the difference between Managed and Custom Runtimes, and a few other lambda-related terminologies.
💡 This section is OPTIONAL; you can skip it and continue directly to Step 1 below.
Knowing the internals helps you to become a better developer. As a result, I strongly advise you to read through this section and have a thorough understanding of .NET Lambda functions. By the end of this section you will have learnt the following:
First of all, let’s quickly go through different lambda-related terminologies that we will use in this post to understand Lambda concepts.
💡 It is important to know that Lambda Runtime ≠ Language Runtime (.NET, Java, etc.)
💡 A Lambda Runtime is a program that runs in an Execution Environment, reads invocation events from the Lambda service, and executes the Function handler. Once an execution is finished, it requests another invocation event, and this process keeps going on until the execution environment is shut down. For .NET, this runtime .NET Lambda Runtime Interface Client (RIC) has been packaged into Amazon.Lambda.RuntimeSupport NuGet package. This Runtime needs to be started as soon as the execution environment is created so that it can start requesting invocation events. How this Runtime is started has been covered later in this post.
The story starts with the bootstrap
file. When an execution environment is created the bootstrap
file is executed. This bootstrap file can be the runtime, or it can invoke another file that creates the runtime. In the .NET lambda function, this is the second case where the bootstrap
executable invokes the .NET Lambda Runtime Interface Client (RIC), which is the actual .NET Lambda Runtime that is packaged into Amazon.Lambda.RuntimeSupport NuGet package.
Once the Lambda Runtime Interface Client (RIC) is initialized or started, it keeps on processing the invocation events until the execution environment is shut down. To get an invocation event, Lambda Runtime or RIC program calls the next invocation API of Lambda Runtime API (an API hosted on the same execution environment compute, acts as a bridge between Lambda Runtime and Lambda Service) which further calls Lambda Service to get an invocation event. If Lambda Service has an event waiting for the execution, it is returned to the Lambda Runtime. Lambda Runtime then executes the Function Handler for the event and returns the response back to the Lambda Service via Lambda Runtime API.
There are 2 types of Lambda Runtime:
A Managed Runtime has everything that it needs to run a Lambda function, you just need to provide it with the deployment package (zip archive). By everything, I mean, it has the following things:
Now once the deployment package is provided, it has the code that needs to be executed, as well as all other dependencies. So this works like a charm!
On the other hand, a Custom Runtime doesn't have either a Language Runtime or Lambda Runtime. Both of these has to be provided by the deployment package. Custom Runtime is just like an empty Linux machine with Lambda Runtime API.
Also, it is important to know that the entry point of a Custom Runtime is a bootstrap file. That means, when an Execution Environment is created for a Custom Runtime, the bootstrap file is executed first.
In order to provide that bootstrap file within the deployment package, you must rename your projects's assembly to bootstrap.
To run a .NET Lambda function with Custom Runtime, you need to understand the following things:
💡 You don't need to make the following changes manually because the AWS Lambda Template in Visual Studio takes care of them.
Keep the assembly name bootstrap
- The executable name of the .NET Lambda function that you create must be bootstrap, so that when the execution environment is created, it will invoke your .NET Lambda executable because it has been named bootstrap.
Use self-contained
deployment to provide missing Langauge Runtime - The Lambda code in the deployment package should either be OS native or self-contained, since there is no language runtime installed in the execution environment when you use custom runtime.
Providing missing Lambda Runtime - In order to provide missing Lambda Runtime, you use Amazon.Lambda.RuntimeSupport NuGet package in your project as it contains the required .NET Lambda Runtime Interface Client (RIC).
Next, you write below code inside the main() method of your Lambda function, which starts the Lambda Runtime, and begins requesting invocation events.
LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
.Build()
.RunAsync();
In the above code, the handler has been passed as a delegate.
AWS Lambda provides an HTTP API for custom runtimes to receive invocation events from the Lambda service and send response data back to the Lambda service. This API is hosted within the Lambda execution environment.
curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next"
Here are the 4 API Methods that have been exposed by Lambda runtime API.
AwsRequestId
/responseAwsRequestId
/errorLambda Runtime for .NET has been open-sourced at Lambda runtime client from the aws/aws-lambda-dotnet repository, and has been packaged into Amazon.Lambda.RuntimeSupport NuGet.
Now let's understand what happens when a .NET lambda function is invoked when you are using the managed runtime.
When the Lambda service receives a request to run a function via the Lambda API, the service first prepares an execution environment.
During this step, the service downloads the code for the function, which is stored in an internal S3 bucket. It then creates an environment with the memory, runtime, and configuration specified.
Next, the bootstrap.sh file is invoked in the execution environment as part of the Init phase.
This bootstrap file determines the type of .NET Lambda function:
Class library handler is straight forward, where a Lambda project is a class library and the Lambda function handler is set to the assembly, type, and method name that the Lambda runtime client invokes. Using reflection, the .NET Lambda runtime client uses the function handler string to identify the method to call in the .NET assembly.
Executing assembly handler is not as straightforward as a Class library handler. This lambda function is basically a console application that has a main() method as an entry point. With top-level statements, the main() method has been generated by the compiler. Such application is compiled into an executable (.exe).
There are two things that you must know when working with executing assembly handler:
First, in the case of executing assembly, you tell the Lambda (or the bootstrap file) to run the assembly, which runs the top-level statements or the main() method. To indicate that you want Lambda to run the assembly, set the Lambda function handler to the assembly name only.
Second thing, in the case of class library handler, the Lambda Runtime client is started by the Lambda (or the bootstrap file), while when using executing assembly, the lambda executes the assembly, but does not start the runtime. So it is your responsibility to start the Lambda Runtime client, which is done by adding the following code in the main() method.
private static async Task Main(string[] args)
{
Func<string, ILambdaContext, string> handler = FunctionHandler;
// Build the Lambda runtime client and passing in the handler as delegate
await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
.Build()
.RunAsync();
}
Here is a snapshot of the bootstrap file, that shows how it runs different executables for different types of Lambda functions. This files is part of .NET Lambda Managed Runtime, and can be accessed from here - bootstrap.sh.
Once the Lambda Runtime client is started, it starts getting invocation events and executing the specified handler.
Create a new Lambda function from Visual Studio.
Select Custom Runtime Function from the available blueprints.
Press the Finish button.
Function
classThe function class looks like the code sample shown below, where you have a main() method.
This main() method is executed when the Lambda executing assembly is invoked.
LambdaBootstrapBuilder.Create() method creates and starts the Lambda Runtime with a handler.
The LambdaBootstrapBuilder class is part of Amazon.Lambda.RuntimeSupport NuGet package.
Once the lambda runtime is initialized, it starts requesting invocation events and executes a lambda handler for each invocation.
public class Function
{
private static async Task Main(string[] args)
{
Func<string, ILambdaContext, string> handler = FunctionHandler;
await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer())
.Build()
.RunAsync();
}
public static string FunctionHandler(string input, ILambdaContext context)
{
return input.ToUpper();
}
}
Here are a few properties in your .csproj file that you need to pay attention.
AssemblyName
PublishReadyToRun
Microsoft.ICU.ICU4C.Runtime
NuGet packageNote that the assembly name has been put bootstrap
intentionally as you are using custom runtime, not a managed runtime.
<AssemblyName>bootstrap</AssemblyName>
When using Lambda with a custom runtime, the Lambda service looks for an executable file named bootstrap within the packaged ZIP file. To enable this, the OutputType
is set to exe and the AssemblyName
to bootstrap
💡 R2R binaries include both codes, MSIL + Native of the same. This reduces the work that needs to be done by JIT.
R2R binaries improve startup performance by reducing the amount of work the just-in-time (JIT) compiler needs to do as your application loads. The binaries contain similar native code compared to what the JIT would produce. However, R2R binaries are larger because they contain both intermediate language (IL) code, which is still needed for some scenarios, and the native version of the same code.
To learn more, refer ReadyToRun Compilation on Microsoft documentation.
Amazon Linux 2023 does not have libicu
a system library for internationalization preinstalled. When publishing to the provided.al2023 runtime Microsoft.ICU.ICU4C.Runtime package need to be included to support internationalization.
💡 Skip this step, if you want to create a Lambda Function with Visual Studio.
Navigate to Lambda Console.
Click the Create Function button.
To create a new Lambda function, provide the following details.
Lastly, click on the Create function button.
Right-click on the Lambda project, and select Publish to AWS Lambda... option.
You should see a wizard like the below screenshot. Make sure you select the following things.
In the next screen, select the IAM Role AWSLambdaBasicExecutionRole as shown in the below screenshot, and press the Upload button.
This publishes the .NET Lambda app in self-contained mode so that the app does not require runtime to be installed on the server. You can have a look at the aws-lambda-tools-defaults.json file to see from where this -self-contained true
is coming.
Navigate to Lambda Console.
Select the Lambda function that you recently created.
On the detail page, select the Test tab.
Enter text in the Event JSON textbox, and press the Test button.
You should see a successful response like below.
Debugging executable assembly functions is a little different. This requires you to run Amazon.Lambda.TestTool first.
Run the below command to install the Mock Lambda Test Tool.
dotnet tool update -g Amazon.Lambda.TestTool-8.0
After that, navigate to C:\Users\<username>\.dotnet\tools
location, you will see all the installed tools.
Run this tool via the command line, which starts the Test Tool in the browser at http://localhost:5050/ URL.
Now, switch back to Visual Studio, and set the AWS_LAMBDA_RUNTIME_API environment variable to localhost:5050.
Right-click, and select Properties.
Navigate to Debug > General tab, and select Open debug launch profiles UI
Set the required environment variable, and close the window.
This creates a launchSettings.json file like this.
{
"profiles": {
"AWSLambda.NET8.Demo": {
"commandName": "Project",
"environmentVariables": {
"AWS_LAMBDA_RUNTIME_API": "localhost:5050"
}
}
}
}
Now, run the application by pressing F5. This starts the app and executes the main() method.
Code in the main() method to start the Lambda Runtime looks for the AWS_LAMBDA_RUNTIME_API environment variable. That’s why the Mock Lambda Test tool needs to be started first.
Now, go back to the lambda test tool, enter the event in the text box, and press the Queue Event button.
The moment you press the Queue Event button, the debugger is hit in Visual Studio.
So this is how you can easily debug and test your Lambda function.
Though we have covered this throughout the post, again re-iterating that, we have: