tech.guitarrapc.cóm

Technical updates

AWS Lambdaの.NET10対応とC#のファイルベースプログラムのサポート

2025年11月に.NET 10がリリースされましたが、2026年1月6日にAWS Lambdaが.NET 10をサポートしました。 これに伴いファイルベースプログラムのC# Lambda関数もサポートされ、.csファイルだけ用意すればLambda関数をデプロイできるようになりました。

AWS LambdaでC#を書く体験が変わったので、今回はその魅力を紹介します。

ファイルベースプログラムのC#については、以前書いた記事も参考にしてください。

はじめに

ファイルベースプログラムのC# Lambda関数は、.csファイルだけでLambda関数をデプロイできます。

これまでは.csproj + .csファイルを用意 → nugetパッケージを追加 → ビルド → zip化 → デプロイ...といった手順が必要でしたが、簡単になりました。 IaCでは引き続きビルドが必要ですが、dotnet toolでデプロイする分にはビルドステップも不要で、体験的にはPythonやNode.jsのようにスクリプトファイルをそのままデプロイするのに近い感覚です。

  • dotnet toolでデプロイ: .csファイルだけ用意したらデプロイコマンド実行
  • IaCでデプロイ: .csファイルをコンパイル、バイナリを指定してデプロイ

次のコードはコメントを抜いたミニマムなサンプルC#コードで、入力された文字列を大文字に変換して返すだけの関数です。

#:package Amazon.Lambda.Core@2.8.0
#:package Amazon.Lambda.RuntimeSupport@1.14.1
#:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4
#:property TargetFramework=net10.0

using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;

// The function handler
var handler = (string input, ILambdaContext context) =>
{
    return input.ToUpper();
};

await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaSerializerContext>())
  .Build()
  .RunAsync();

[JsonSerializable(typeof(string))]
public partial class LambdaSerializerContext : JsonSerializerContext
{
}

これをToUpper.csという名前で保存し、dotnet lambda deploy-functionコマンドでデプロイするだけで、Lambda関数が作成されます。

File-based C# Lambda関数をデプロイする

もう少し詳しく見ていきましょう。

C#コードはAmazon.Lambda.Templatesで追加できるlambda.FileBasedテンプレートをベースに開始できますし、そんなのを入れなくてもコピー&ペーストでも大丈夫1です。

テンプレートを使う場合は、次のコマンドでプロジェクトを作成します。

dotnet new lambda.FileBased -n ToUpper

あるいは、空のディレクトリを作成して、.csファイルを作成します。今回はToUpper.csという名前にします。

$ tree
.
└── ToUpper.cs

C#コードを用意する

テンプレートから生成されるToUpper.csファイルの中身は先のコードとほぼ同じです。 コード全体を改めて示します。

// C# file-based Lambda functions can be deployed to Lambda using the
// .NET Tool Amazon.Lambda.Tools version 6.0.0 or later.
//
// Command to install Amazon.Lambda.Tools
//   dotnet tool install -g Amazon.Lambda.Tools
//
// Command to deploy function
//    dotnet lambda deploy-function <lambda-function-name> MyLambdaFunction.cs
//
// Command to package function
//    dotnet lambda package MyLambdaFunction.zip MyLambdaFunction.cs


#:package Amazon.Lambda.Core@2.8.0
#:package Amazon.Lambda.RuntimeSupport@1.14.1
#:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4

// Explicitly setting TargetFramework here is done to avoid
// having to specify it when packaging the function with Amazon.Lambda.Tools
#:property TargetFramework=net10.0

// By default File-based C# apps publish as Native AOT. When packaging Lambda function
// unless the host machine is Amazon Linux a container build will be required.
// Amazon.Lambda.Tools will automatically initate a container build if docker is installed.
// Native AOT also requires the code and dependencies be Native AOT compatible.
//
// To disable Native AOT uncomment the following line to add the .NET build directive
// that disables Native AOT.
//#:property PublishAot=false

using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;

// The function handler that will be called for each Lambda event
var handler = (string input, ILambdaContext context) =>
{
    return input.ToUpper();
};

// Build the Lambda runtime client passing in the handler to call for each
// event and the JSON serializer to use for translating Lambda JSON documents
// to .NET types.
await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaSerializerContext>())
  .Build()
  .RunAsync();

// Since Native AOT is used by default with C# file-based Lambda functions the source generator
// based Lambda serializer is used. Ensure the input type and return type used by the function
// handler are registered on the JsonSerializerContext using the JsonSerializable attribute.
[JsonSerializable(typeof(string))]
public partial class LambdaSerializerContext : JsonSerializerContext
{
}

コードはC#のトップレベルステートメントを使ったExecutable assembly handlersスタイル名になっているので、Mainメソッドは不要で代わりにawait LambdaBootstrapBuilder.Create()が必要です。

public async Task<string> HandleRequest(T input, ILambdaContext context)がハンドラーのシグネチャです。第一引数が受け取るイベントデータの型で、第二引数がILambdaContextです。上記コードは、文字列を受け取り大文字に変換して返すので、(string input, ILambdaContext context) => { ... }となっています。 要するに、ハンドラーは第一引数に受け取る型、第二引数にILambdaContextを指定すれば任意の非同期メソッドにできると考えればいいでしょう。

整理すると、開発者が書く部分は2か所です。

1. ハンドラーに処理の実装を書く

Lambdaで処理したい内容はこのハンドラー内に書きます。

var handler = (string input, ILambdaContext context) =>
{
    // ここに処理を書く
};

2. Lambdaで受け取る入力をシリアライズコンテキストに登録する

リフレクションなしでLambdaへの入力をC#オブジェクトに変換するため、System.Text.Json.Serializationのシリアライズコンテキストに型を登録します。これでソースジェネレーターがシリアライズ/デシリアライズコードを生成します。

今回は入力が文字列想定なのでJsonSerializable(typeof(string))をシリアライズコンテキストに登録、ハンドラーに来た文字列をToUpper()で大文字に変換してレスポンスを返します。

[JsonSerializable(typeof(string))] // ここで受け取る型のシリアライズを登録する
public partial class LambdaSerializerContext : JsonSerializerContext
{
}

デプロイする

Lambdaに直接.csファイルをデプロイするdotnet tool Amazon.Lambda.Toolsがあります。 これを使うとCLIでデプロイできるのでインストールします。

$ dotnet tool install -g Amazon.Lambda.Tools
You can invoke the tool using the following command: dotnet-lambda
Tool 'amazon.lambda.tools' (version '6.0.3') was successfully installed.

AWS認証を取得しておきます。

$ aws sso login --profile your-profile

Lambda関数をデプロイします。

ここが従来に比べて大きく変わった点で、.csprojファイルを用意せずに.csファイルだけでデプロイできるようになっています。 これならGitHub ActionsなどのCI/CD環境でデプロイするのも簡単です。

$ dotnet lambda deploy-function ToUpper ToUpper.cs --function-runtime dotnet10 --function-role arn:aws:iam::123456789012:role/lambda-function-Role --function-memory-size 256 --function-timeout 10 --profile your-profile
Amazon Lambda Tools for .NET Core applications (6.0.3)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet

Architecture not provided, defaulting to x86_64 for container build image.
Executing publish command
Starting container for native AOT build using build image: mcr.microsoft.com/dotnet/sdk:10.0-aot.
... invoking 'docker run --name tempLambdaBuildContainer-3fb48080-a444-499e-b6a4-16413be8c5d0 --rm --volume "C:\github\Lambda\DotnetFileAppFunction":/tmp/source/ -i mcr.microsoft.com/dotnet/sdk:10.0-aot dotnet publish "/tmp/source/ToUpper.cs" --output "/tmp/source\artifacts\ToUpper" --configuration "Release" --framework "net10.0" /p:GenerateRuntimeConfigurationFiles=true --runtime linux-x64 --self-contained True  /p:StripSymbols=true' from directory C:\github\Lambda\DotnetFileAppFunction
... docker run: Unable to find image 'mcr.microsoft.com/dotnet/sdk:10.0-aot' locally
... docker run: 10.0-aot: Pulling from dotnet/sdk
... docker run: 59287b3c3c70: Pulling fs layer
... docker run: 06762f394a85: Pulling fs layer
... docker run: e26f93cf9c70: Pulling fs layer
... docker run: 69c84e01b5c0: Pulling fs layer
... docker run: 47849234c411: Pulling fs layer
... docker run: 505db3b3094b: Pulling fs layer
... docker run: 95c4e06fe864: Pulling fs layer
... docker run: a3629ac5b9f4: Pulling fs layer
... docker run: 60fc5ac8adb0: Pulling fs layer
... docker run: da1a80ccb2fc: Pulling fs layer
... docker run: 46f592c23ae7: Pulling fs layer
... docker run: e26f93cf9c70: Download complete
... docker run: 95c4e06fe864: Download complete
... docker run: da1a80ccb2fc: Download complete
... docker run: 59287b3c3c70: Download complete
... docker run: 47849234c411: Download complete
... docker run: a3629ac5b9f4: Download complete
... docker run: 06762f394a85: Download complete
... docker run: 60fc5ac8adb0: Download complete
... docker run: 46f592c23ae7: Download complete
... docker run: a3629ac5b9f4: Pull complete
... docker run: 47849234c411: Pull complete
... docker run: 95c4e06fe864: Pull complete
... docker run: 46f592c23ae7: Pull complete
... docker run: e26f93cf9c70: Pull complete
... docker run: 59287b3c3c70: Pull complete
... docker run: 60fc5ac8adb0: Pull complete
... docker run: 505db3b3094b: Download complete
... docker run: 69c84e01b5c0: Download complete
... docker run: 69c84e01b5c0: Pull complete
... docker run: da1a80ccb2fc: Pull complete
... docker run: 06762f394a85: Pull complete
... docker run: 505db3b3094b: Pull complete
... docker run: Digest: sha256:d68a5e260330b659f7eae596a255bddbdc4e406e3579eb2d85d718ac58dd7dcb
... docker run: Status: Downloaded newer image for mcr.microsoft.com/dotnet/sdk:10.0-aot
... docker run:   Determining projects to restore...
... docker run:   Restored /tmp/source/ToUpper.csproj (in 13.86 sec).
... docker run:   ToUpper -> /root/.local/share/dotnet/runfile/ToUpper-6855d7fa7559aae751b6be03e6497d359004d36f9f6dae455063950209971e3d/bin/release_linux-x64/ToUpper.dll
... docker run:   Generating native code
... docker run:   ToUpper -> /tmp/source/artifacts/ToUpper/
Zipping publish folder C:\github\Lambda\DotnetFileAppFunction\artifacts\ToUpper to C:\github\Lambda\DotnetFileAppFunction\artifacts\ToUpper.zip
... zipping: ToUpper
Created publish archive (C:\github\Lambda\DotnetFileAppFunction\artifacts\ToUpper.zip).
Creating new Lambda function ToUpper
New Lambda function created

arm64で動作させることもできますが、NativeAOTでのarm64ビルドはarm64マシンが必要です。

# x86_64マシンでarm64デプロイしようとするとエラーになる
$ dotnet lambda deploy-function ToUpper-arm64 ToUpper.cs --function-architecture arm64 --function-runtime dotnet10 --function-role arn:aws:iam::123456789012:role/lambda-function-Role --function-memory-size 256 --function-timeout 10
Amazon Lambda Tools for .NET Core applications (6.0.3)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet
Host machine architecture (X64) differs from Lambda architecture (Arm64). Building Native AOT Lambda functions require the host and lambda architectures to match.

また、引数を最後に持っていくとFunction名がarm64になるので注意です2

# X: なぜかx86_64でビルドされる上、Function名がarm64になる
$ dotnet lambda deploy-function ToUpper-arm64 ToUpper.cs --function-runtime dotnet10 --function-role arn:aws:iam::123456789012:role/lambda-function-Role --function-memory-size 256 --function-timeout 10 --function-architecture arm64

ビルド~デプロイ処理を見てみると、mcr.microsoft.com/dotnet/sdk:10.0-aotコンテナを使ってNative AOTビルドしています。 また、ビルドしたバイナリToUpperをzip化してLambda関数を作成しています。デプロイ後のファイルツリーを見てみると、artifactsディレクトリにアップロードしたToUpper.zipがあり、ToUpperディレクトリにNative AOTでビルドされたバイナリが入っています。

.
│  ToUpper.cs
│
└─artifacts
    │  ToUpper.zip
    │
    └─ToUpper
            ToUpper
            ToUpper.dbg

ToUpper.zipの中身はToUpperバイナリだけが入っています。

$ unzip -l artifacts/ToUpper.zip
Archive:  artifacts/ToUpper.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
  6925632  1980-00-00 00:00   ToUpper
---------                     -------
  6925632                     1 file

このことから、ファイルベースのC# Lambda関数は従来通りzip化されたバイナリをアップロードしていることがわかります。

動作確認する

デプロイされた結果です。

Lambdaコンソール

Lambda HandlerはToUpperに設定されます。通常C#の場合はNamespace.ClassName::MethodNameの形式ですが、ファイルベースの場合は名前空間やクラス名が省略され直接バイナリ名になります。

Lambda HandlerはToUpper

実行してみると、ToUpper関数が動作していることがわかります。テストイベントを以下のように設定します。

"foobar"

実行すると、以下のように返ってきます。

"FOOBAR"

Functionの実行結果

いい感じですね。

JSONを入力する

先ほどの例では文字列を受け取る単純なケースでしたが、実際のLambda関数ではJSONオブジェクトを扱うことが多いでしょう。 現在のコードのままJSONを与えると例外が発生します。

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

Lambdaを実行すると例外ログが出ます。

START RequestId: 08c68f6d-02c2-4d55-b4e9-3f1aba3acbf9 Version: $LATEST
2026-01-14T06:09:44.113Z    08c68f6d-02c2-4d55-b4e9-3f1aba3acbf9    fail    Amazon.Lambda.Serialization.SystemTextJson.JsonSerializerException: Error converting the Lambda event JSON payload to type System.String: The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
 ---> System.Text.Json.JsonException: The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'StartObject' as a string.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_ExpectedString(JsonTokenType) + 0x19
   at System.Text.Json.Utf8JsonReader.GetString() + 0xa5
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader&, Type, JsonSerializerOptions, ReadStack&, T&, Boolean&) + 0x1e8
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader&, T&, JsonSerializerOptions, ReadStack&) + 0x81
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack&, Utf8JsonReader&, Exception) + 0x48
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader&, T&, JsonSerializerOptions, ReadStack&) + 0x21b
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader&, ReadStack&) + 0x26
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1, JsonTypeInfo`1, Nullable`1) + 0xd6
   at Amazon.Lambda.Serialization.SystemTextJson.SourceGeneratorLambdaJsonSerializer`1.InternalDeserialize[T](Byte[]) + 0x78
   at Amazon.Lambda.Serialization.SystemTextJson.AbstractLambdaJsonSerializer.Deserialize[T](Stream) + 0x183
   --- End of inner exception stack trace ---
   at Amazon.Lambda.Serialization.SystemTextJson.AbstractLambdaJsonSerializer.Deserialize[T](Stream) + 0x229
   at Amazon.Lambda.RuntimeSupport.HandlerWrapper.<>c__DisplayClass44_0`2.<GetHandlerWrapper>b__0(InvocationRequest invocation) + 0x45
   at Amazon.Lambda.RuntimeSupport.LambdaBootstrap.<>c__DisplayClass26_0.<<InvokeOnceAsync>b__0>d.MoveNext() + 0x1d5
END RequestId: 08c68f6d-02c2-4d55-b4e9-3f1aba3acbf9
REPORT RequestId: 08c68f6d-02c2-4d55-b4e9-3f1aba3acbf9  Duration: 55.06 ms  Billed Duration: 56 ms  Memory Size: 256 MB Max Memory Used: 37 MB

これに対応するには、入力JSONを表すC#クラスを用意し、シリアライズコンテキストに登録します。

[JsonSerializable(typeof(HelloWorldEvent))]
//[JsonSerializable(typeof(string))]
public partial class LambdaSerializerContext : JsonSerializerContext
{
}

public record HelloWorldEvent
{
    [JsonPropertyName("key1")]
    public required string Key1 { get; init; }
    [JsonPropertyName("key2")]
    public required string Key2 { get; init; }
    [JsonPropertyName("key3")]
    public required string Key3 { get; init; }
}

受け取ったHelloWorldEventオブジェクトを使うようにハンドラーを書き換えます。

var handler = (HelloWorldEvent input, ILambdaContext context) =>
{
    return $"{input.Key1}, {input.Key2}, {input.Key3}".ToUpper();
};

変更後のToUpper.csのコードは次の通りです。

#:package Amazon.Lambda.Core@2.8.0
#:package Amazon.Lambda.RuntimeSupport@1.14.1
#:package Amazon.Lambda.Serialization.SystemTextJson@2.4.4

#:property TargetFramework=net10.0

using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;

// 👇 HelloWorldEventを受け取るように書き換える
var handler = (HelloWorldEvent input, ILambdaContext context) =>
{
    return $"{input.Key1}, {input.Key2}, {input.Key3}".ToUpper();
};

await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer<LambdaSerializerContext>())
    .Build()
    .RunAsync();

// 👇 シリアライズコンテキストにHelloWorldEventを登録する
[JsonSerializable(typeof(HelloWorldEvent))]
public partial class LambdaSerializerContext : JsonSerializerContext
{
}

// 👇 JSONを表すC#クラスを追加
public record HelloWorldEvent
{
    [JsonPropertyName("key1")]
    public required string Key1 { get; init; }
    [JsonPropertyName("key2")]
    public required string Key2 { get; init; }
    [JsonPropertyName("key3")]
    public required string Key3 { get; init; }
}

これで受け取ったJSONの値を大文字に変換して返せます。

{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}

JSONを受け取って大文字で返す

IaCからデプロイする

PulumiのAWS SDKでもファイルベースプログラムのC# Lambda関数をデプロイできます。 ただ、dotnet toolからのデプロイと違って明示的に事前ビルドが必要なので微妙です。

ガワだけIaCで作っておいてCodeはIgnore、関数自体は別途デプロイしたほうが扱いやすいです。

var name = "dotnet-file-based-lambda";
var functionName = "ToUpper";
var handlerName = "ToUpper";
var functionCodePath = "Lambda/DotnetFileAppFunction/artifacts/ToUpper/ToUpper"; // 事前にビルドしておいたバイナリのパス
var roleArn = "arn:aws:iam::123456789012:role/lambda-function-Role";

var lambdaHash = HashHelper.CreateHashSha256(functionCodePath);

var cloudwatchLogs = new Pulumi.Aws.CloudWatch.LogGroup($"{name}-lambda-loggroup", new()
{
    Name = $"/aws/lambda/{functionName}",
    RetentionInDays = 7,
}, opt);

var lambda = new Pulumi.Aws.Lambda.Function($"{name}-lambda-function", new()
{
    Name = functionName,
    Description = AwsConstants.Descriptions.Default,
    Code = new FileArchive(Directory.GetParent(functionCodePath)!.FullName.NormalizePath()),
    Role = roleArn,
    Runtime = "dotnet10",
    Handler = handlerName,
    Timeout = 10,
    SourceCodeHash = lambdaHash,
}, opt);

パフォーマンスについて

AWS Blogで、.NET 8に比べて.NET 10がパフォーマンス低下するケース(#120288)について言及されています。これに関しては、.NET 10.0.2で解消したとのことなので、安心できそうです。

.NET 10.0.2でパフォーマンス低下が改善

まとめ

AWS Lambdaが.NET 10をサポートしたことで、ファイルベースプログラムのC# Lambda関数が使えるようになりました。 .csファイルだけでLambda関数をデプロイできるので、C#でのLambda開発がかなり手軽になります。

C#のラムダ関数は、PythonやNode.jsに比べてビルドが必要で、C#を意識するのはお手軽じゃないと感じていました。PythonやNode.jsのような手軽さでC#が書けるのは嬉しい変化です。 .NET 10におけるファイルベースプログラムは割と良い感じなので、ぜひ試してみてください。

参考


  1. 私はコピー&ペーストで書いてる
  2. コマンドの途中に--function-architecture arm64を入れると回避できます