C# の Lambda 関数ハンドラーの定義 - AWS Lambda

C# の Lambda 関数ハンドラーの定義

Lambda 関数ハンドラーは、イベントを処理する関数コード内のメソッドです。関数が呼び出されると、Lambda はハンドラーメソッドを実行します。関数は、ハンドラーが応答を返すか、終了するか、タイムアウトするまで実行されます。

関数が呼び出され、Lambda が関数のハンドラーメソッドを実行すると、関数に 2 つの引数が渡されます。最初の引数は event オブジェクトです。別の AWS のサービスが関数を呼び出すと、その event オブジェクトには、関数が呼び出された原因となったイベントに関するデータが含まれます。例えば、API Gateway の event オブジェクトには、パス、HTTP メソッド、および HTTP ヘッダーに関する情報が含まれています。正確なイベント構造は、関数を呼び出す AWS のサービスによって異なります。個々のサービスのイベント形式の詳細については、「他の AWS サービスからのイベントを使用した Lambda の呼び出し」を参照してください。

Lambda は関数に context オブジェクトも渡します。このオブジェクトには、呼び出し、関数、および実行環境に関する情報が含まれます。詳細については、「Lambda コンテキストオブジェクトを使用して C# 関数の情報を取得する」を参照してください。

すべての Lambda イベントのネイティブ形式は、JSON 形式のイベントを表すバイトストリームです。関数の入力パラメータと出力パラメータがタイプ System.IO.Stream でない限り、それらをシリアル化する必要があります。LambdaSerializer アセンブリ属性を設定して、使用するシリアライザーを指定します。詳細については、「Lambda 関数のシリアル化」を参照してください。

Lambda 用の .NET 実行モデル

.NET で Lambda 関数を実行するための実行モデルには、クラスライブラリアプローチと実行可能アセンブリアプローチの 2 種類があります。

クラスライブラリアプローチでは、呼び出す関数の AssemblyNameClassNameMethod を示す文字列を Lambda に渡します。この文字列の形式についての詳細は、「クラスライブラリハンドラー」を参照してください。関数の初期化フェーズでは、関数のクラスが初期化され、コンストラクタ内のすべてのコードが実行されます。

実行可能アセンブリアプローチでは、C# 9 の最上位ステートメント機能を使用します。この方法では、関数の呼び出しコマンドを受信するたびに Lambda が実行する実行可能アセンブリが生成されます。Lambda には、実行する実行可能アセンブリの名前のみを指定します。

以下のセクションでは、これら 2 つの方法の関数コードの例を示しています。

クラスライブラリハンドラー

次の Lambda 関数コードは、クラスライブラリアプローチを使用する Lambda 関数のハンドラーメソッド (FunctionHandler) の例を示しています。このサンプル関数では、Lambda は、関数を呼び出すイベントを API Gateway から受信しています。この関数は、データベースからレコードを読み取り、API Gateway レスポンスの一部としてレコードを返します。

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace GetProductHandler; public class Function { private readonly IDatabaseRepository _repo; public Function() { this._repo = new DatabaseRepository(); } public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request) { var id = request.PathParameters["id"]; var databaseRecord = await this._repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord) }; } }

Lambda 関数を作成するときは、関数のハンドラーに関する情報をハンドラー文字列の形式で Lambda に提供する必要があります。これにより、関数の呼び出し時に実行するコードのメソッドを Lambda に指示します。C# では、クラスライブラリアプローチを使用する場合のハンドラー文字列の形式は次のようになります。

ASSEMBLY::TYPE::METHOD。ここで、

  • ASSEMBLY は、アプリケーションの .NET アセンブリファイルの名前です。Amazon.Lambda.Tools CLI を使用してアプリケーションを構築し、.csproj ファイルの AssemblyName プロパティを使用してアセンブリ名を設定しない場合、ASSEMBLY は単に .csproj ファイルの名前になります。

  • TYPE は、NamespaceClassName からなるハンドラー型の正式名称となります。

  • METHOD は、コード内の関数ハンドラーメソッドの名前です。

ここに示すコード例では、アセンブリに GetProductHandler という名前が付けられている場合、ハンドラー文字列は GetProductHandler::GetProductHandler.Function::FunctionHandler になります。

実行可能アセンブリハンドラー

次の例では、Lambda 関数を実行可能アセンブリとして定義します。このコードのハンドラーメソッドには Handler という名前が付いています。実行可能アセンブリを使用する場合は、Lambda ランタイムをブートストラップする必要があります。これを行うには、LambdaBootstrapBuilder.Create メソッドを使用します。このメソッドは、関数がハンドラーとして使用するメソッドと、使用する Lambda シリアライザーを入力として受け取ります。

上位のステートメントの使用の詳細については、AWS コンピューティングブログの「AWS Lambda 用の .NET 6 ランタイムのご紹介」を参照してください。

namespace GetProductHandler; IDatabaseRepository repo = new DatabaseRepository(); await LambdaBootstrapBuilder.Create<APIGatewayProxyRequest>(Handler, new DefaultLambdaJsonSerializer()) .Build() .RunAsync(); async Task<APIGatewayProxyResponse> Handler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) { var id = apigProxyEvent.PathParameters["id"]; var databaseRecord = await this.repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord) }; };

実行可能アセンブリを使用する場合、コードの実行方法を Lambda に指示するハンドラー文字列はアセンブリの名前です。この例では、GetProductHandler になります。

Lambda 関数のシリアル化

Lambda 関数が、Stream オブジェクト以外の入出力タイプを使用する場合は、アプリケーションにシリアル化ライブラリを追加する必要があります。シリアル化は、System.Text.Json および Newtonsoft.Json が提供する標準のリフレクションベースのシリアル化を使用するか、ソース生成のシリアル化を使用して実装できます。

ソース生成のシリアル化を使用する

ソース生成のシリアル化は NETバージョン 6 以降の機能で、コンパイル時にシリアル化コードを生成できます。これにより、リフレクションが不要になり、関数のパフォーマンスが向上します。ソース生成のシリアル化を関数で使用するには、以下を実行します。

  • JsonSerializerContext を継承する新しい部分クラスを作成し、シリアル化または逆シリアル化を必要とするすべての型の JsonSerializable 属性を追加します。

  • LambdaSerializer を設定して SourceGeneratorLambdaJsonSerializer<T> を使用します。

  • アプリケーションコード内の手動シリアル化または逆シリアル化をすべて更新して、新しく作成したクラスを使用するようにします。

ソース生成のシリアル化を使用する関数の例を次のコードに示します。

[assembly: LambdaSerializer(typeof(SourceGeneratorLambdaJsonSerializer<CustomSerializer>))] public class Function { private readonly IDatabaseRepository _repo; public Function() { this._repo = new DatabaseRepository(); } public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request) { var id = request.PathParameters["id"]; var databaseRecord = await this._repo.GetById(id); return new APIGatewayProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = JsonSerializer.Serialize(databaseRecord, CustomSerializer.Default.Product) }; } } [JsonSerializable(typeof(APIGatewayProxyRequest))] [JsonSerializable(typeof(APIGatewayProxyResponse))] [JsonSerializable(typeof(Product))] public partial class CustomSerializer : JsonSerializerContext { }
注記

Lambda でネイティブの事前コンパイル (AOT) を使用する場合は、ソース生成のシリアル化を使用する必要があります。

リフレクションベースのシリアル化を使用する

AWS は、アプリケーションにシリアル化を迅速に追加できる事前構築済みライブラリを提供しています。これは、Amazon.Lambda.Serialization.SystemTextJson または Amazon.Lambda.Serialization.Json NuGet パッケージを使用して設定します。背後では、Amazon.Lambda.Serialization.SystemTextJsonSystem.Text.Json を使用してシリアル化タスクを実行し、Amazon.Lambda.Serialization.JsonNewtonsoft.Json パッケージを使用します。

ILambdaSerializer インターフェイスの実装によって独自のシリアル化ライブラリを作成することもできます。これは、Amazon.Lambda.Core ライブラリの一部として使用できます。このインターフェイスは 2 つのメソッドを定義します。

  • T Deserialize<T>(Stream requestStream);

    このメソッドを実装して、Invoke API から Lambda 関数ハンドラーに渡されるオブジェクトにリクエストのペイロードを逆シリアル化することができます。

  • T Serialize<T>(T response, Stream responseStream);

    このメソッドを実装して、Lambda 関数のハンドラーから返される結果を Invoke API オペレーションが返すレスポンスペイロードにシリアル化することができます。

Lambda Annotations Framework による関数コードの簡略化

Lambda Annotations は、.NET 6 と.NET 8 用のフレームワークで、C# を使用して Lambda 関数を簡単に記述できます。Annotations Framework では、通常のプログラミングモデルを使用して記述された Lambda 関数のコードの多くを置き換えることができます。このフレームワークを使用して記述されたコードでは、ビジネスロジックに集中できるより単純な式が使用されます。

次のコード例は、Annotations Framework を使用して Lambda 関数の記述を簡単にする方法を示しています。最初の例は、通常の Lambda プログラムモデルを使用して記述されたコードを示し、2 番目の例は、Annotations Framework を使用した同等のコードを示しています。

public APIGatewayHttpApiV2ProxyResponse LambdaMathAdd(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) { if (!request.PathParameters.TryGetValue("x", out var xs)) { return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.BadRequest }; } if (!request.PathParameters.TryGetValue("y", out var ys)) { return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.BadRequest }; } var x = int.Parse(xs); var y = int.Parse(ys); return new APIGatewayHttpApiV2ProxyResponse { StatusCode = (int)HttpStatusCode.OK, Body = (x + y).ToString(), Headers = new Dictionary≪string, string> { { "Content-Type", "text/plain" } } }; }
[LambdaFunction] [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")] public int Add(int x, int y) { return x + y; }

Lambda Annotations を使用してコードを簡略化する方法を示す別の例については、awsdocs/aws-doc-sdk-examples GitHub リポジトリにあるこの「cross-service example application」を参照してください。フォルダ PamApiAnnotations は、メイン function.cs ファイルで Lambda Annotations を使用しています。比較のために、PamApi フォルダには、通常の Lambda プログラミングモデルを使用して記述された同等のファイルがあります。

Annotations Framework はソースジェネレーターを使用して、Lambda プログラミングモデルから、2 番目の例にあるコードに変換するコードを生成します。

.NET で Lambda Annotations を使用する方法の詳細については、次のリソースを参照してください。

Lambda Annotations Framework による依存関係インジェクション

Lambda Annotations Framework を使用して、使い慣れた構文を用いて Lambda 関数に依存関係インジェクションを追加することもできます。[LambdaStartup] 属性を Startup.cs ファイルに追加すると、Lambda Annotations Framework がコンパイル時に必要なコードを生成します。

[LambdaStartup] public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IDatabaseRepository, DatabaseRepository>(); } }

Lambda 関数は、コンストラクタインジェクションを使用するか、[FromServices] 属性を使用して個々のメソッドに挿入することで、サービスを挿入できます。

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace GetProductHandler; public class Function { private readonly IDatabaseRepository _repo; public Function(IDatabaseRepository repo) { this._repo = repo; } [LambdaFunction] [HttpApi(LambdaHttpMethod.Get, "/product/{id}")] public async Task<Product> FunctionHandler([FromServices] IDatabaseRepository repository, string id) { return await this._repo.GetById(id); } }

Lambda 関数ハンドラーの制限

ハンドラー署名にはいくつかの制限があることに注意してください。

  • ハンドラーメソッドとその依存関係内で unsafe コンテキストを使用できますが、unsafe ではなく、ハンドラー署名でポインター型を使用する場合があります。詳細については、Microsoft Docs ウェブサイトの「unsafe (C# Reference)」を参照してください。

  • params キーワードを使用して可変数のパラメータを渡さなかったり、可変数のパラメータをサポートするために使用する入力パラメータまたは戻りパラメータとして ArgIterator を使用できない場合があります。

  • ハンドラーは、IList<T> Sort<T>(IList<T> input) などの汎用メソッドではない場合があります。

  • async void 署名を持つ Async ハンドラーはサポートされていません。

C# Lambda 関数のコードのベストプラクティス

Lambda 関数をビルドするときは、次のリストのガイドラインに従って、コーディングのベストプラクティスを実行してください。

  • Lambda ハンドラーをコアロジックから分離します。これにより、関数の単体テストが実行しやすくなります。

  • 関数のデプロイパッケージ内で依存関係を制御します。AWS Lambda 実行環境には多数のライブラリが含まれています。最新の機能やセキュリティ更新プログラムを有効にするために、Lambda はこれらのライブラリを定期的に更新します。この更新により、Lambda 関数の動作が微妙に変化する場合があります。関数で使用する依存関係を完全に制御するには、すべての依存関係をデプロイパッケージでパッケージングします。

  • 依存関係の複雑さを最小限に抑えます。フレームワークを単純化して、実行環境起動時のロードを高速化します。

  • デプロイパッケージをランタイムに必要な最小限のサイズにします。これにより、呼び出しに先立ってデプロイパッケージをダウンロードして解凍する所要時間が短縮されます。.NET で記述された関数の場合は、デプロイパッケージの一部として AWS SDK ライブラリ全体をアップロードしないようにします。代わりに、SDK のコンポーネントを必要に応じて選別するモジュール (DynamoDB、Amazon S3 SDK モジュール、Lambda コアライブラリなど) を使用します。

  • 実行環境の再利用を活用して関数のパフォーマンスを向上させます。関数ハンドラー外で SDK クライアントとデータベース接続を初期化し、静的なアセットを /tmp ディレクトリにローカルにキャッシュします。関数の同じインスタンスで処理された後続の呼び出しは、これらのリソースを再利用できます。これにより、関数の実行時間が短縮され、コストが節約されます。

    呼び出し間でデータが漏れるのを防ぐため、実行環境を使用してセキュリティ上の懸念があるユーザーデータ、イベント、またはその他の情報を保存しないでください。関数がハンドラー内のメモリに保存できない変更可能な状態に依存している場合は、ユーザーごとに個別の関数または個別のバージョンの関数を作成することを検討してください。

  • keep-alive ディレクティブを使用して永続的な接続を維持します。Lambda は、時間の経過とともにアイドル状態の接続を消去します。関数を呼び出すときにアイドル状態の接続を再利用しようとすると、接続エラーが発生します。永続的な接続を維持するには、ランタイムに関連付けられている keep-alive ディレクティブを使用します。例については、「Node.js で Keep-alive を使用して接続を再利用する」を参照してください。

  • 環境変数を使用して、オペレーショナルパラメータを関数に渡します。たとえば、Amazon S3 バケットに書き込む場合、書き込み先のバケット名はハードコーディングせずに、環境変数として設定します。

  • Lambda 関数では、再帰呼び出しを使用しないでください。関数が自身を呼び出すこともあれば、新たに開始されたプロセスで関数が再度呼び出される可能性もあります。これを行うと意図しないボリュームで関数が呼び出され、料金が急増する可能性があります。意図しない呼び出しがいくつも見つかった場合は、すぐに関数の予約済同時実行数を 0 に設定して、コードを更新している間のすべての関数の呼び出しをスロットリングします。

  • Lambda 関数コードで文書化されていない非公開の API を使用しないでください。AWS Lambda マネージドランタイムでは、Lambda が Lambda の内部 API にセキュリティと機能面の更新を定期的に適用します。これらの内部 API 更新には後方互換性がないことがあり、関数にこれらの非公開 API に対する依存関係がある場合、呼び出しの失敗などの意図しない結果につながります。公開されている API のリストについては、「API リファレンス」を参照してください。

  • 冪等性コードを記述します。関数の記述に冪等性コードを使用すると、重複するイベントが同じ方法で処理されるようになります。コードでは、イベントを適切に検証し、重複するイベントを適切に処理する必要があります。詳細については、「Lambda 関数を冪等にするにはどうすればよいですか?」を参照してください。