.NET on AWS Blog

From Azure Functions to AWS Lambda: A .NET Developer’s Migration Playbook

Introduction

Serverless has become the default for event-driven .NET workloads, and many teams that started on Azure Functions are now consolidating onto AWS to unify their compute, data, and AI in single cloud. If you run C#/.NET serverless functions on Azure and want to move to AWS Lambda, this playbook gives you an end-to-end, repeatable approach.

Migrating .NET serverless workloads to Lambda unlocks significant cost and performance advantages—up to 34% better price-performance with Graviton2 (arm64), sub-second cold starts via Lambda SnapStart for .NET, and the ability to consolidate compute, data, and AI on a single cloud. Lambda further strengthens the value proposition with response streaming for GenAI and large payloads, built-in Function URLs, a first-class managed .NET 10 runtime, and native event-driven integration with AWS services — including direct invocation from 25+ services and poll-based event source mappings for streaming and queue workloads.

This guide organizes the journey into seven phases — from discovery through post-migration optimization.

To help you follow along with the examples in this post, we have published a companion repository on AWS Samples. The repository contains all the code snippets and sample projects referenced throughout this blog.

Prerequisites & Assumptions

To get the most of this guide, you should have the following:

Why AWS Lambda for .NET Workloads

Before diving into mechanics, it helps to understand what you gain by targeting Lambda for a .NET serverless workload.

Benefit What it means
Price-Performance Graviton2 (arm64) functions deliver up to 34% better price-performance. Lambda bills per-millisecond (1 ms granularity) with no per-execution minimum.
Cold-Start Optimization Lambda SnapStart provides sub-second startup for .NET 10 managed runtimes with no code changes. Provisioned Concurrency can eliminate cold starts entirely.
Response Streaming Stream responses progressively to clients — ideal for GenAI, real-time dashboards, and large payloads.
Function URLs Built-in HTTPS endpoints without needing API Gateway for simple use cases.
Managed Runtime Run C#/.NET 10 on a fully managed Lambda runtime — no container packaging required.
Service Integration Event source mappings for queue and stream sources (SQS, Kinesis, DynamoDB Streams, MSK, Amazon MQ, DocumentDB), direct event-driven invocation from 25+ AWS services, and integration with 200+ services through Amazon EventBridge.
Runtime Modernization AWS Transform (agentic AI) can upgrade Lambda runtimes at scale across your organization.

The diagram in Figure 1 summarizes the seven phases and key activities covered in this playbook — from discovery and feature mapping through code migration, IaC, testing, cutover, and post-migration optimization.

The seven-phase migration path from Azure Functions to AWS Lambda — from discovery and feature mapping through code migration, IaC, testing, cutover, and post-migration optimization.

Figure 1. The seven-phase migration path from Azure Functions to AWS Lambda

Phase 1: Discovery and Assessment

Catalog your existing Azure Functions workload — triggers, bindings, dependencies, and configuration — to establish the migration scope.
Begin by compiling a comprehensive inventory of your Azure Functions workload. Capture each of the following:

  • Function Apps and their groupings — Azure groups multiple functions in a Function App; Lambda treats each function independently.
  • Triggers and Bindings — document all triggers (HTTP, Timer, Queue, Blob, Cosmos DB, Event Hub, Service Bus, Event Grid) and input/output bindings.
  • Runtime and Language — note the .NET version and programming model.
  • Configuration — capture App Settings, Connection Strings, Key Vault references.
  • Networking — document VNet integration, Private Endpoints, IP restrictions.
  • Dependencies — list dependent services: Azure Storage, Cosmos DB, Service Bus, Event Grid, Redis, SQL, etc.
  • Durable Functions — identify stateful orchestrations that will map to AWS Step Functions.
  • Monitoring — Application Insights queries, alerts, and dashboards.

Phase 2: Feature Mapping

Map Azure Functions concepts to their AWS Lambda equivalents and assign complexity indicators to prioritize your migration sequence. Azure Functions and AWS Lambda share the same serverless mental model but differ in how triggers, bindings, and platform features are expressed. This section maps the concepts with a complexity indicator to prioritize and sequence your migration.

2.1 Triggers & Dependency Mapping

Azure Functions Trigger AWS Lambda nearest Equivalent Complexity Notes
HTTP trigger API Gateway (REST/HTTP API) or Function URL Low Function URLs for simple setup; API Gateway for routing, auth, throttling
Timer trigger Amazon EventBridge Scheduler Low Cron and rate expressions supported
Queue Storage trigger Amazon SQS event source mapping Low Batch processing, visibility timeout
Service Bus trigger Amazon SQS (standard/FIFO) or SNS Medium SQS FIFO for ordered processing
Event Hubs trigger Amazon Kinesis Data Streams or MSK Medium Event source mapping with batching
Cosmos DB change feed Amazon DynamoDB Streams Medium Real-time change data capture
Blob Storage trigger Amazon S3 event notifications Low Supports prefix/suffix filtering
Event Grid trigger Amazon EventBridge Medium Centralized event bus with schema registry
Kafka trigger Amazon Managed Streaming for Apache Kafka (Amazon MSK) event source mapping Medium Managed Kafka with native Lambda integration
RabbitMQ trigger Amazon MQ (RabbitMQ) event source mapping Low Native support
Durable Functions AWS Step Functions High Orchestration, fan-out/fan-in, human approval workflows
Key Vault AWS Secrets Manager / Parameter Store, a capability of AWS Systems Manager Low Secret and config management
Application Insights Amazon CloudWatch and AWS X-Ray Low Logs, metrics, distributed tracing

2.2 Bindings & Programming Model

Azure Functions uses declarative input/output bindings (via attributes) to connect to services. For AWS Lambda use the Lambda Annotations framework it provides a comparable attribute-driven model: you decorate a method with [LambdaFunction] and an event attribute (for example, [RestApi], [HttpApi], or [SQSEvent]), and the source generator wires up the handler and generates the corresponding SAM resources. For cloud operations not covered by the Lambda Annotations framework, use the AWS SDK for .NET directly within the handler. Powertools for AWS Lambda (.NET) further reduces boilerplate for logging, tracing, parsing, and idempotency.

The following table compares key programming model features between Azure Functions and AWS Lambda:

Feature Azure Functions AWS Lambda
Function grouping Multiple functions per Function App Multiple [LambdaFunction] methods per class/project
Entry point Handler with trigger attribute Method with [LambdaFunction] + event attribute
Dependency injection Built-in DI container (.NET isolated) Built-in DI via [LambdaStartup] + [FromServices]
Middleware Built-in middleware pipeline Powertools for AWS Lambda (.NET) middleware
Shared code Extension bundle Lambda Layers for shared dependencies

2.3 Scaling & Concurrency

The following table maps scaling and concurrency behavior between Azure Functions (Flex Consumption plan) and AWS Lambda:

Feature Azure Functions AWS Lambda
Scale to zero Yes Yes
Concurrency / instance Configurable (>1) 1 invocation per execution environment
Cold-start mitigation Always Ready instances Provisioned Concurrency + SnapStart (.NET 10)
Max instances 1,000 (configurable) 1,000 / region (increasable to 10,000+)
Billing granularity Per-millisecond (100 ms minimum per execution) Per-millisecond (1 ms, no minimum)

2.4 Networking, Observability & Security

The following table maps Azure networking, observability, and security features to their nearest AWS equivalents:

Azure Feature Nearest AWS Equivalent
VNet Integration VPC configuration (ENI-based)
Private Endpoints VPC + ALB / API Gateway (private)
Application Insights Amazon CloudWatch + AWS X-Ray
Log Analytics CloudWatch Logs Insights
Azure Monitor Alerts CloudWatch Alarms + EventBridge
Managed Identity IAM Execution Role
Key Vault references AWS Secrets Manager / Parameter Store
Authentication (EasyAuth + Entra ID) Amazon Cognito / API Gateway authorizers

2.5 State & Orchestration

The following table maps Azure Durable Functions stateful patterns to their AWS Step Functions equivalents:

Azure Nearest AWS Equivalent
Durable Functions (orchestration) AWS Step Functions
Durable Entities (stateful actors) Step Functions + DynamoDB
Fan-out / fan-in patterns Step Functions Map state
Human approval workflows Step Functions callback patterns

Phase 3: Code Migration (C#/.NET 10)

This is the heart of the migration. Convert your Azure Functions handlers to AWS Lambda using Lambda Annotations. Following are side-by-side C# examples showing the Azure Functions pattern and its AWS Lambda equivalent on .NET 10.

AWS SAM

When migrating Azure Functions to AWS Lambda, the AWS Serverless Application Model (AWS SAM) is the recommended framework for this migration. It extends CloudFormation with simplified serverless syntax — declarative YAML that defines Lambda functions, event sources, permissions, and configuration in one place. You define, test (locally), and deploy with a single command.

DI setup with a Lambda Annotations startup class

Start with the startup class. Register all service dependencies in the default DI container.

AWS Lambda (C#/.NET 10) — Startup.cs (Lambda Annotations DI)

using Amazon.Lambda.Annotations;
using Microsoft.Extensions.DependencyInjection;

[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register AWS SDK clients and app services once; injected via [FromServices]
        services.AddSingleton<Amazon.EventBridge.IAmazonEventBridge,
        Amazon.EventBridge.AmazonEventBridgeClient>();
        services.AddSingleton<IOrderService, OrderService>();
    }
}

3.1 HTTP Trigger

HTTP triggers are the most common starting point. On Azure you use [HttpTrigger]; on AWS Lambda you use [HttpApi] or [RestApi] with the Lambda Annotations framework to expose an HTTPS endpoint.

Azure Function

[Function("hello")]
public async Task<HttpResponseData> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
    var name = System.Web.HttpUtility.ParseQueryString(req.Url.Query)["name"];
    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteStringAsync($"Hello, {name}");
    return response;
}

AWS Lambda

public class Functions
{
    // Source generator wires up the handler and generates the API Gateway event
    [LambdaFunction]
    [HttpApi(LambdaHttpMethod.Get, "/hello")]
    public IHttpResult Hello(
        [FromQuery] string name, ILambdaContext context)
    {
        context.Logger.LogInformation($"Saying hello to {name}");
        return HttpResults.Ok(new { message = $"Hello, {name}" });
    }
}

Migration steps

  • Replace HttpTrigger with [LambdaFunction] and [HttpApi]/[RestApi]; bind parameters with [FromQuery], [FromRoute], [FromBody] instead of parsing the raw request.
  • Return IHttpResult (HttpResults.Ok, HttpResults.NotFound) instead of building response objects manually.
  • Update authentication: Microsoft Entra ID → Amazon Cognito / AWS Identity and Access Management (IAM) / API Gateway authorizers.
  • Configure routes (or a Function URL) in IaC template.

3.2 Service Bus Queue to Amazon SQS

Azure Service Bus queues map to Amazon SQS. The Lambda event source mapping polls the queue and invokes your function with a batch of messages.

Azure Function

[Function("ServiceBusQueueTrigger")]
public void Run(
    [ServiceBusTrigger("orderqueue", Connection = "ServiceBusConnection")]
    string myQueueItem, FunctionContext context)
{
    var logger = context.GetLogger("ServiceBusQueueTrigger");
    logger.LogInformation($"Processing message: {myQueueItem}");
}

AWS Lambda

using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.SQS;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;

public class Functions
{
    [LambdaFunction]
    [SQSEvent("@orderqueue", BatchSize = 10)]
    public async Task ProcessOrders(
        SQSEvent sqsEvent,
        [FromServices] IOrderService orders,
        ILambdaContext context)
    {
        foreach (var record in sqsEvent.Records)
        {
            context.Logger.LogInformation($"Processing message: {record.Body}");
            await orders.ProcessAsync(record.Body);
            // Message is deleted when the function succeeds
        }
    }
}

The [SQSEvent] attribute generates the queue reference and event source mapping. The mapping is direct: an Azure Service Bus queue becomes an SQS queue. For ordered processing (Service Bus sessions), use an SQS FIFO queue and read the MessageGroupId from each record’s attributes.

Migration steps

  • Replace [ServiceBusTrigger] with [SQSEvent]; the annotation generates the event source mapping in your SAM template.
  • For ordered processing (Service Bus sessions), use an SQS FIFO queue and read MessageGroupId from record attributes.
  • Configure BatchSize and visibility timeout to match your current throughput.
  • Set up a dead-letter queue (DLQ) for messages that exceed maxReceiveCount.

3.3 Blob Storage events to Amazon S3 events

Azure Blob Storage triggers map to Amazon S3 event notifications. When an object is created in an S3 bucket, Lambda receives an S3Event containing the bucket name and object key — the object content is fetched separately via the SDK. Figure 2 shows the before/after architecture: an Azure Blob Storage trigger writing to Application Insights, re-platformed to an Amazon S3 event source mapping invoking AWS Lambda with CloudWatch and X-Ray observability.

Side-by-side before and after architecture diagram. Left: Azure Blob Storage container triggers an Azure Function via BlobTrigger, logging to Application Insights. Right: Amazon S3 bucket upload invokes AWS Lambda through an S3 event source mapping, with logs and metrics in Amazon CloudWatch and X-Ray. A key-mappings table lists the equivalent services.

Figure 2. Before/after architecture for Blob Storage triggered Azure Function

Azure Function

[Function("BlobStorageTrigger")]
public void Run(
        [BlobTrigger("mycontainer/{name}", Connection = "AzureWebJobsStorage")]
        string myBlobContent,
        string name,
        FunctionContext context)
{
        var logger = context.GetLogger("BlobStorageTrigger");
        logger.LogInformation($"Processing blob: {name}");
        logger.LogInformation($"Blob content length: {myBlobContent.Length} characters");
}

AWS Lambda

using Amazon.Lambda.Annotations;
using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;

public class Functions
{
    [LambdaFunction]
    public async Task HandleS3Upload(
        S3Event s3Event,
        [FromServices] IFileProcessor processor,
        ILambdaContext context)
    {
        foreach (var record in s3Event.Records)
        {
            var bucket = record.S3.Bucket.Name;
            var key = record.S3.Object.Key;
            context.Logger.LogInformation($"Object created: s3://{bucket}/{key}");
            await processor.ProcessAsync(bucket, key);
        }
    }
}

Migration steps

  • Replace [BlobTrigger] with an S3 event notification configured in your SAM template; handle S3Event in the Lambda function.
  • Use AmazonS3Client to read object content — S3 events deliver metadata only, not the blob body.
  • Configure S3 event notifications with prefix/suffix filters to replicate your container path logic.
  • Route failed invocations to a DLQ via the Lambda event source mapping’s error handling configuration.

3.4 Timer Trigger to Amazon EventBridge Scheduler

Timer-triggered functions are common in Azure Functions. On AWS, a schedule expression drives the Lambda through Amazon EventBridge Scheduler (or an EventBridge rule). With Lambda Annotations you handle a CloudWatchEvent (scheduled event) payload; the schedule itself is defined in your IaC template.

Azure Function

[Function("TimerExample")]
public void Run(
    [TimerTrigger("0 */5 * * * *")] TimerInfo timer,  // every 5 minutes
    FunctionContext context)
{
    var logger = context.GetLogger("TimerExample");
    logger.LogInformation($"Timer fired at: {DateTime.UtcNow}");
}

AWS Lambda

using Amazon.Lambda.Annotations;
using Amazon.Lambda.Core;
using Amazon.Lambda.CloudWatchEvents;

namespace MyApp;

public class Functions
{
    [LambdaFunction]
    public void RunOnSchedule(
        CloudWatchEvent<object> scheduledEvent,
        [FromServices] IReportService reports,
        ILambdaContext context)
    {
        context.Logger.LogInformation($"Scheduled run at: {DateTime.UtcNow}");
        reports.GenerateAsync();
    }
}

template.yaml

Resources:
  ScheduledFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: MyApp::MyApp.Functions_RunOnSchedule_Generated::RunOnSchedule
      Runtime: dotnet10
      Architectures: [arm64]
      Events:
        EveryFiveMinutes:
          Type: ScheduleV2                 # EventBridge Scheduler
          Properties:
            ScheduleExpression: rate(5 minutes)   # or cron(0/5 * * * ? *)

Migration steps

  • Replace [TimerTrigger] with a SAM ScheduleV2 event; translate the 6-field NCRONTAB (seconds first) to an EventBridge rate() or cron() expression.
  • Accept CloudWatchEvent<object> instead of TimerInfo; track missed runs via CloudWatch alarms if needed.

3.5 Updating .csproj Dependencies

Replace the Azure Functions and WebJobs extension packages with the AWS Lambda and AWS SDK packages your handlers use, and target Net 10.0. The Lambda Annotations package ships a Roslyn source generator that runs at build time, reference it like any other package, as shown in the following code snippets.

Before — Azure Function (.csproj)

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
  <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.22.0" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.ServiceBus" Version="5.9.0" />
  <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventGrid" Version="3.3.0" />
</ItemGroup>

After — AWS Lambda on .NET 10 with Lambda Annotations (.csproj)

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <AWSProjectType>Lambda</AWSProjectType>
  <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>
 
<ItemGroup>
  <!-- Lambda Annotations: source generator maps attributes to handlers + SAM -->
  <PackageReference Include="Amazon.Lambda.Annotations" Version="1.6.0" />
  <PackageReference Include="Amazon.Lambda.Core" Version="2.5.0" />
  <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4" />
  <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
  <PackageReference Include="Amazon.Lambda.SQSEvents" Version="2.2.0" />
  <PackageReference Include="Amazon.Lambda.S3Events" Version="3.1.0" />
  <PackageReference Include="AWSSDK.SQS" Version="3.7.400" />
  <PackageReference Include="AWS.Lambda.Powertools.Logging" Version="1.6.0" />
</ItemGroup>

3.6 Managed Identity to IAM Roles

Azure Functions authenticate to other Azure services with a Managed Identity — the platform injects credentials into the runtime environment, and the SDK resolves them without explicit credential code. AWS Lambda has a direct analogue: attach an IAM execution role to the function and let the AWS SDK for .NET resolve those credentials via the default credential provider chain.

Azure Function (C#) — Managed Identity

using Azure.Identity;
using Azure.Storage.Blobs;

// DefaultAzureCredential resolves the Function App's Managed Identity
var credential = new DefaultAzureCredential();
var client = new BlobServiceClient(
    new Uri("https://myaccount.blob.core.windows.net"), credential);

AWS Lambda (C#/.NET 10) — IAM execution role + default credentials

using Amazon.S3;

// No credential object needed: the AWS SDK resolves the Lambda
// execution role via the default credential chain.
var client = new AmazonS3Client();   // uses the function's IAM role

// Register once in the [LambdaStartup] class so it is reused:
services.AddSingleton<IAmazonS3, AmazonS3Client>();

template.yaml (AWS SAM — least-privilege execution role)

Resources:
  ProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: MyApp::MyApp.Functions_HandleS3Upload_Generated::HandleS3Upload
      Runtime: dotnet10
      Policies:                         # generates a scoped IAM role
        - S3ReadPolicy:
            BucketName: my-upload-bucket

Migration steps

  • Replace DefaultAzureCredential with parameterless constructors — the Lambda execution role resolves automatically. Locally, the SDK falls back to your AWS CLI profile.
  • Grant permissions using SAM policy templates (e.g. S3ReadPolicy, SQSPollerPolicy) scoped to specific resources.
  • Map Managed Identity role-based access control (RBAC) assignments to equivalent IAM policy statements on the execution role.

3.7 Configuration and Secrets

Azure Functions read settings from local.settings.json in development and from appsettings.json [AS23] [RK24] (with optional Key Vault references) in production, bound through IConfiguration. On AWS, plain-text and non-sensitive configuration are stored in Lambda environment variables, while secrets and sensitive parameters are managed securely in AWS Secrets Manager or Parameter Store. The plain-text/non-sensitive configuration can still be bound through IConfiguration using the environment-variables provider.

Before — Azure (local.settings.json + Key Vault reference)

{
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "OrderQueueName": "orders",
    "DbConnection": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/db)"
  }
}

After — AWS SAM (environment variables + Secrets Manager / Parameter Store)

Resources:
  ProcessOrdersFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: dotnet10
      Environment:
        Variables:
          ORDER_QUEUE_NAME: orders           # non-secret config
          DB_SECRET_ID: prod/orders/db       # pointer to the secret
      Policies:
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretId: prod/orders/db

AWS Lambda (C#/.NET 10) — IConfiguration + Secrets Manager

using Microsoft.Extensions.Configuration;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

// Non-secret config: bind environment variables through IConfiguration
var config = new ConfigurationBuilder()
    .AddEnvironmentVariables()
    .Build();
var queueName = config["ORDER_QUEUE_NAME"];

// Secret value from Secrets Manager (cache across invocations)
var sm = new AmazonSecretsManagerClient();
var secret = await sm.GetSecretValueAsync(
    new GetSecretValueRequest { SecretId = config["DB_SECRET_ID"] });
var dbConnection = secret.SecretString;

Migration steps

  • Move plain-text and non-sensitive configurations to Lambda environment variables; bind via IConfiguration using the environment-variables provider.
  • Move Key Vault references to Secrets Manager (rotating secrets) or Parameter Store SecureString (static config). Store only the secret name in the environment.
  • Cache secrets across warm invocations — use the AWS Parameters and Secrets Lambda Extension for automatic caching.
  • Scope IAM read access (AWSSecretsManagerGetSecretValuePolicy) to the specific secret.

Phase 4: Infrastructure as Code

Define your Lambda functions, event sources, and permissions declaratively using AWS SAM or Terraform.

Provision the migrated workload declaratively. The Lambda Annotations source generator emits SAM resources at build time (serverless.template), so your IaC stays in sync with your code.

The companion repository contains sample templates.

AWS SAM (infra/aws/template.yaml) – defines all Lambda functions, event sources (e.g. API Gateway, SQS, S3), IAM policies, and supporting resources in a single deployable template.

Deploy with sam build && sam deploy.

Terraform — the same resources translate directly to aws_lambda_function, aws_sqs_queue, and aws_lambda_event_source_mapping resources. For details, see AWS Lambda Terraform module.

Phase 5: Testing and Validation

Validate your migrated functions locally and in the cloud before routing production traffic.

5.1 Local Testing with the SAM CLI

Build and invoke your .NET functions locally before deploying, so you can validate handler wiring and event shapes. For details, see SAM CLI for local testing.

AWS SAM CLI — local testing

# Build the .NET 10 functions
sam build
 
# Invoke a single function with a sample event
sam local invoke HelloFunction --event events/apigw-get.json
 
# Run the API locally on http://127.0.0.1:3000
sam local start-api

5.2 Unit / Integration Tests (xUnit, C#)

Because Lambda Annotations handlers are plain methods, you can unit-test them directly — construct the class, pass a test ILambdaContext, and assert on the returned IHttpResult.

FunctionsTests.cs (xUnit + Amazon.Lambda.TestUtilities)

using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.TestUtilities;
using Xunit;
 
public class FunctionsTests
{
    [Fact]
    public void Hello_ReturnsGreeting()
    {
        var functions = new Functions();
        var context = new TestLambdaContext();
 
        var result = functions.Hello("Ada", context);
 
        Assert.Equal(200, result.StatusCode);
        Assert.Contains("Ada", result.Body);
    }
}

5.3 Validation Checklist

☐  Triggers fire and handlers receive the expected event.

☐  IAM role grants least privilege access only.

☐  Secrets resolve from Secrets Manager / Parameter Store.

☐  Cold-start and warm latency meet service-level objectives(SLOs) (measure with/without SnapStart).

☐  CloudWatch logs, metrics, and X-Ray traces appear as expected.

☐  Dead-letter queues (DLQs) receive failed messages after configured retries.

☐  Load test at peak concurrency — no throttling.

Phase 6: Deployment and Cutover

Set up CI/CD, shift traffic incrementally from Azure to AWS, and establish a rollback plan.

6.1 CI/CD with GitHub Actions

name: deploy
on:
  push: { branches: [main] }
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: "10.0.x" }
      - uses: aws-actions/setup-sam@v2
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE }}
          aws-region: us-east-1
      - run: sam build
      - run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset

6.2 Cutover Strategy

Migrate incrementally to keep risk low. A parallel run followed by a weighted cutover is the safest pattern for production traffic.

6.3 Rollback Plan

  • Keep the Azure Function App deployed but idle until the cutover is proven.
  • Use Lambda aliases/versions so you can repoint the previous version instantly.
  • Wire CloudWatch alarms (error rate, p99 latency, DLQ depth) to trigger automatic CodeDeploy rollback.

Phase 7: Post-Migration Optimization

Tune cost, latency, and observability now that your workload is running on Lambda.

7.1 Cost

  • Run on Graviton2 (arm64) for up to 34% better price-performance (default in the IaC templates).
  • Right-size memory with AWS Lambda Power Tuning; higher memory often improves .NET throughput while reducing total cost.
  • Adopt a Compute Savings Plan for steady-state workloads.
  • Cache SDK clients across invocations to minimize initialization and maximize per-ms billing savings.

Total Cost of Ownership (TCO): Two factors drive savings: Lambda bills per-millisecond with no per-execution minimum block, and Graviton2 (arm64) duration pricing is 20% lower than equivalent x86_64 Lambda configurations. When you combine this lower rate with Graviton2’s up to 19% faster execution for compute-intensive workloads, the total net price-performance improvement reaches up to 34%. Use the AWS Pricing Calculator to model your specific traffic.

7.2 Performance

  • Enable SnapStart for sub-second cold starts with no code changes.
  • Use Provisioned Concurrency for paths that cannot tolerate any cold start.
  • Register SDK clients as singleton in [LambdaStartup] for reuse across invocations.
  • Consider Native AOT for .NET where startup and memory footprint are paramount.

7.3 Operational Excellence

Cleanup

Remove all provisioned resources when you’re done to avoid unnecessary charges.

AWS SAM

sam delete --stack-name <your-stack-name>

Terraform

terraform destroy -auto-approve

Azure (decommission after full cutover)

# Delete the Function App

az functionapp delete --name <function-app-name> --resource-group <rg-name>

# Or delete the entire resource group and all its contents

az group delete --name <rg-name> --yes --no-wait

Note: If your S3 buckets contain objects, empty them before deletion:

aws s3 rm s3://<bucket-name> --recursive

Conclusion

Migrating Azure Functions to AWS Lambda is a well-trodden path for .NET teams. By working through the seven phases — discovery, feature mapping, code migration, infrastructure as code, testing, cutover, and optimization — you convert a large, ambiguous migration into a sequence of concrete, low-risk steps.

The Lambda Annotations framework keeps the developer experience close to the Azure Functions model— attribute-driven handlers, built-in dependency injection, and IaC generated from your code — while the .NET 10 managed runtime, SnapStart, Graviton2, and Powertools for AWS Lambda (.NET) mean you rarely trade functionality for the move. Start with a low-complexity function to validate your pipeline end to end, then apply the same patterns to your medium and high-complexity workloads.

Call to Action

Ready to migrate your first Azure Function to AWS Lambda? Here’s how to get started:

Ramkumar Ramanujam

Ramkumar Ramanujam

Ramkumar Ramanujam is a Senior Cloud Consultant at AWS Professional Services. He enables customers to modernize and migrate their .NET workloads to AWS and has special interest in Containers and Serverless technology. Outside of work, he loves drawing/painting and cricket.

Ravi Kulkarni

Ravi Kulkarni

Ravi Kulkarni is a results-oriented Technology Consultant and seasoned full-stack developer with over 14 years of experience spanning Amazon Web Services and Microsoft Azure. His expertise includes .NET, AWS Cloud Services, Azure, SharePoint, and React. Outside of work, he enjoys travelling, spending time with family, and playing chess.