This tutorial shows you how to set up an AWS Lambda project using Native AOT compilation with Powertools for .NET
Logger, addressing performance, trimming, and deployment considerations.
Prerequisites
- An AWS account with appropriate permissions
- A code editor (we'll use Visual Studio Code in this tutorial)
- .NET 8 SDK or later
- Docker (required for cross-platform AOT compilation)
1. Understanding Native AOT
Native AOT (Ahead-of-Time) compilation converts your .NET application directly to native code during build time rather
than compiling to IL (Intermediate Language) code that gets JIT-compiled at runtime. Benefits for AWS Lambda include:
- Faster cold start times (typically 50-70% reduction)
- Lower memory footprint
- No runtime JIT compilation overhead
- No need for the full .NET runtime to be packaged with your Lambda
First, ensure you have the .NET 8 SDK installed:
Install the AWS Lambda .NET CLI tools:
| dotnet tool install -g Amazon.Lambda.Tools
dotnet new install Amazon.Lambda.Templates
|
Verify installation:
3. Creating a Native AOT Lambda Project
Create a directory for your project:
| mkdir powertools-aot-logger-demo
cd powertools-aot-logger-demo
|
Create a new Lambda project using the Native AOT template:
| dotnet new lambda.NativeAOT -n PowertoolsAotLoggerDemo
cd PowertoolsAotLoggerDemo
|
Add the AWS.Lambda.Powertools.Logging package:
| cd src/PowertoolsAotLoggerDemo
dotnet add package AWS.Lambda.Powertools.Logging
|
5. Implementing the Lambda Function with AOT-compatible Logger
Let's modify the Function.cs file to implement our function with Powertools Logger in an AOT-compatible way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 | using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;
using System.Text.Json;
using AWS.Lambda.Powertools.Logging;
using Microsoft.Extensions.Logging;
namespace PowertoolsAotLoggerDemo;
public class Function
{
private static ILogger _logger;
private static async Task Main()
{
_logger = LoggerFactory.Create(builder =>
{
builder.AddPowertoolsLogger(config =>
{
config.Service = "TestService";
config.LoggerOutputCase = LoggerOutputCase.PascalCase;
config.JsonOptions = new JsonSerializerOptions
{
TypeInfoResolver = LambdaFunctionJsonSerializerContext.Default
};
});
}).CreatePowertoolsLogger();
Func<string, ILambdaContext, string> handler = FunctionHandler;
await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
.Build()
.RunAsync();
}
public static string FunctionHandler(string input, ILambdaContext context)
{
_logger.LogInformation("Processing input: {Input}", input);
_logger.LogInformation("Processing context: {@Context}", context);
return input.ToUpper();
}
}
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(ILambdaContext))] // make sure to include ILambdaContext for serialization
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
{
}
|
6. Updating the Project File for AOT Compatibility
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 | <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Enable AOT compilation -->
<PublishAot>true</PublishAot>
<!-- Enable trimming, required for Native AOT -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Set trimming level: full is most aggressive -->
<TrimMode>full</TrimMode>
<!-- Prevent warnings from becoming errors -->
<TrimmerWarningLevel>0</TrimmerWarningLevel>
<!-- If you're encountering trimming issues, enable this for more detailed info -->
<!-- <TrimmerLogLevel>detailed</TrimmerLogLevel> -->
<!-- These settings optimize for Lambda -->
<StripSymbols>true</StripSymbols>
<OptimizationPreference>Size</OptimizationPreference>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Assembly attributes needed for Lambda -->
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<AWSProjectType>Lambda</AWSProjectType>
<!-- Native AOT requires executable, not library -->
<OutputType>Exe</OutputType>
<!-- Avoid the copious logging from the native AOT compiler -->
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.12.0"/>
<PackageReference Include="Amazon.Lambda.Core" Version="2.5.0"/>
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4"/>
<PackageReference Include="AWS.Lambda.Powertools.Logging" Version="2.0.0"/>
</ItemGroup>
</Project>
|
Native AOT compilation must target the same OS and architecture as the deployment environment. AWS Lambda runs on Amazon
Linux 2023 (AL2023) with x64 architecture.
The simplest approach is to use the AWS Lambda .NET tool, which handles the cross-platform compilation:
| dotnet lambda deploy-function --function-name powertools-aot-logger-demo --function-role your-lambda-role-arn
|
This will:
- Detect your project is using Native AOT
- Use Docker behind the scenes to compile for Amazon Linux
- Deploy the resulting function
Option B: Using Docker Directly
Alternatively, you can use Docker directly for more control:
On macOS/Linux:
1
2
3
4
5
6
7
8
9
10
11
12
13 | # Create a build container using Amazon's provided image
docker run --rm -v $(pwd):/workspace -w /workspace public.ecr.aws/sam/build-dotnet8:latest-x86_64 \
bash -c "cd src/PowertoolsAotLoggerDemo && dotnet publish -c Release -r linux-x64 -o publish"
# Deploy using the AWS CLI
cd src/PowertoolsAotLoggerDemo/publish
zip -r function.zip *
aws lambda create-function \
--function-name powertools-aot-logger-demo \
--runtime provided.al2023 \
--handler bootstrap \
--role arn:aws:iam::123456789012:role/your-lambda-role \
--zip-file fileb://function.zip
|
On Windows:
1
2
3
4
5
6
7
8
9
10
11
12
13 | # Create a build container using Amazon's provided image
docker run --rm -v ${PWD}:/workspace -w /workspace public.ecr.aws/sam/build-dotnet8:latest-x86_64 `
bash -c "cd src/PowertoolsAotLoggerDemo && dotnet publish -c Release -r linux-x64 -o publish"
# Deploy using the AWS CLI
cd src\PowertoolsAotLoggerDemo\publish
Compress-Archive -Path * -DestinationPath function.zip -Force
aws lambda create-function `
--function-name powertools-aot-logger-demo `
--runtime provided.al2023 `
--handler bootstrap `
--role arn:aws:iam::123456789012:role/your-lambda-role `
--zip-file fileb://function.zip
|
9. Testing the Function
Test your Lambda function using the AWS CLI:
| aws lambda invoke --function-name powertools-aot-logger-demo --payload '{"name":"PowertoolsAOT"}' response.json
cat response.json
|
You should see a response like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | {
"Level": "Information",
"Message": "test",
"Timestamp": "2025-05-06T09:52:19.8222787Z",
"Service": "TestService",
"ColdStart": true,
"XrayTraceId": "1-6819dbd3-0de6dc4b6cc712b020ee8ae7",
"Name": "AWS.Lambda.Powertools.Logging.Logger"
}
{
"Level": "Information",
"Message": "Processing context: Amazon.Lambda.RuntimeSupport.LambdaContext",
"Timestamp": "2025-05-06T09:52:19.8232664Z",
"Service": "TestService",
"ColdStart": true,
"XrayTraceId": "1-6819dbd3-0de6dc4b6cc712b020ee8ae7",
"Name": "AWS.Lambda.Powertools.Logging.Logger",
"Context": {
"AwsRequestId": "20f8da57-002b-426d-84c2-c295e4797e23",
"ClientContext": {
"Environment": null,
"Client": null,
"Custom": null
},
"FunctionName": "powertools-aot-logger-demo",
"FunctionVersion": "$LATEST",
"Identity": {
"IdentityId": null,
"IdentityPoolId": null
},
"InvokedFunctionArn": "your arn",
"Logger": {},
"LogGroupName": "/aws/lambda/powertools-aot-logger-demo",
"LogStreamName": "2025/05/06/[$LATEST]71249d02013b42b9b044b42dd4c7c37a",
"MemoryLimitInMB": 512,
"RemainingTime": "00:00:29.9972216"
}
}
|
Check the logs in CloudWatch Logs to see the structured logs created by Powertools Logger.
Trimming Considerations
Native AOT uses aggressive trimming, which can cause issues with reflection-based code. Here are tips to avoid common
problems:
- Using DynamicJsonSerializer: If you're encountering trimming issues with JSON serialization, add a trimming hint:
| [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)]
public class MyRequestType
{
// Properties that will be preserved during trimming
}
|
- Logging Objects: When logging objects with structural logging, consider creating simple DTOs instead of complex
types:
| // Instead of logging complex domain objects:
Logger.LogInformation("User: {@user}", complexUserWithCircularReferences);
// Create a simple loggable DTO:
var userInfo = new { Id = user.Id, Name = user.Name, Status = user.Status };
Logger.LogInformation("User: {@userInfo}", userInfo);
|
- Handling Reflection: If you need reflection, explicitly preserve types:
| <ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRoots.xml"/>
</ItemGroup>
|
And in TrimmerRoots.xml:
| <linker>
<assembly fullname="YourAssembly">
<type fullname="YourAssembly.TypeToPreserve" preserve="all"/>
</assembly>
</linker>
|
Lambda Configuration Best Practices
- Memory Settings: Native AOT functions typically need less memory:
| aws lambda update-function-configuration \
--function-name powertools-aot-logger-demo \
--memory-size 512
|
- Environment Variables: Set the AWS_LAMBDA_DOTNET_PREJIT environment variable to 0 (it's not needed for AOT):
| aws lambda update-function-configuration \
--function-name powertools-aot-logger-demo \
--environment Variables={AWS_LAMBDA_DOTNET_PREJIT=0}
|
- ARM64 Support: For even better performance, consider using ARM64 architecture:
When creating your project:
| dotnet new lambda.NativeAOT -n PowertoolsAotLoggerDemo --architecture arm64
|
Or modify your deployment:
| aws lambda update-function-configuration \
--function-name powertools-aot-logger-demo \
--architectures arm64
|
The Powertools Logger automatically logs cold start information. Use CloudWatch Logs Insights to analyze performance:
| fields @timestamp, coldStart, billedDurationMs, maxMemoryUsedMB
| filter functionName = "powertools-aot-logger-demo"
| sort @timestamp desc
| limit 100
|
11. Troubleshooting Common AOT Issues
If you see errors about missing metadata, you may need to add more types to your trimmer roots:
| <ItemGroup>
<TrimmerRootAssembly Include="AWS.Lambda.Powertools.Logging"/>
<TrimmerRootAssembly Include="System.Private.CoreLib"/>
<TrimmerRootAssembly Include="System.Text.Json"/>
</ItemGroup>
|
Build Failures on macOS/Windows
If you're building directly on macOS/Windows without Docker and encountering errors, remember that Native AOT is
platform-specific. Always use the cross-platform build options mentioned earlier.
Summary
In this tutorial, you've learned:
- How to set up a .NET Native AOT Lambda project with Powertools Logger
- How to handle trimming concerns and ensure compatibility
- Cross-platform build and deployment strategies for Amazon Linux 2023
- Performance optimization techniques specific to AOT lambdas
Native AOT combined with Powertools Logger gives you the best of both worlds: high-performance, low-latency Lambda
functions with rich, structured logging capabilities.
Explore using the Embedded Metrics Format (EMF) with your Native AOT Lambda functions for enhanced observability, or try
implementing Powertools Tracing in your Native AOT functions.