Amazon Web Services ブログ
.NET Lambda Annotations Frameworkが一般利用可能になりました
この度、Lambda Annotations Framework for .NETの一般提供を開始しました。この新しいプログラミングモデルは、C#ソースジェネレータを使用することで、.NET開発者がC#でLambdaを書くことをより自然に感じられるようにします。この投稿では、.NET開発者にとってより慣用的な.NETのLambda関数の記述を簡素化するフレームワークの使い方を紹介します。
Lambda Annotations Frameworkとは何か
Lambda Annotations Frameworkは、.NET開発者がAWS Lambda関数を作成するための自然なプログラミングモデルを提供します。この新しいフレームワークは、C#のカスタム属性とソースジェネレータを使用して、アノテーションを付与されたLambda関数を通常のLambdaのプログラミングモデルに変換します。ソースジェネレータは新しいC#ソースコードを作成し、コンパイル時に生成されたコードを取り込みます。Lambda Annotations Frameworkは、コンパイル時にコードを変換するため、Lambdaの起動時間には影響を与えません。
ソースジェネレータはC#コンパイラに統合され、.NETプロジェクトのコンパイル時にコード生成を行います。つまり、Lambda Annotations NuGetパッケージ以外に、Lambda Annotationsを使用するための追加ツールは必要ありません。CloudFormationを使ったLambdaのデプロイツールであれば、Lambda Annotationsを使うことができます。これには、AWS Toolkit for Visual Studio、Lambda .NET CLI、またはSAMが含まれます。
以下の C#のLambda 関数は、通常の Lambda プログラミングモデルを使って書かれています。このコードはAmazon API GatewayからのREST APIリクエストを処理します:
public class Functions
{
public APIGatewayProxyResponse LambdaMathPlus(APIGatewayProxyRequest request, ILambdaContext context)
{
if (!request.PathParameters.TryGetValue("x", out var xs))
{
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.BadRequest
};
}
if (!request.PathParameters.TryGetValue("y", out var ys))
{
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.BadRequest
};
}
var x = int.Parse(xs);
var y = int.Parse(ys);
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.OK,
Body = (x + y).ToString(),
Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
};
}
}
同じLambda関数を、Lambda Annotations Frameworkを使って次のように書き換えることができます:
public class Functions
{
[LambdaFunction]
[RestApi("/plus/{x}/{y}")]
public int Plus(int x, int y)
{
return x + y;
}
}
Lambda Annotations Frameworkには、以下の機能が組み込まれています:
- 依存性注入
- AWS CloudFormation の同期
- コード生成
- JSON と YAML CloudFormation テンプレートのサポート
Lambda Annotations Frameworkの使い方
このセクションでは、Lambda Annotations Frameworkを使ってAWS Serverless Applicationを作成します。以下のインストールと設定が必要です:
- Visual Studio 2022
- AWS Toolkit for Visual Studio の最新バージョン
- Amazon API Gateway、AWS Lambda関数、Amazon Simple Storage Service (Amazon S3)バケットを使用しAPIを作成する権限を持つIAMユーザー
Visual Studioを使用していない場合は、Amazon.Lambda.Templates NuGetパッケージから以下に示すのと同じプロジェクトテンプレートを作成することができます。
dotnet new install Amazon.Lambda.Templates
dotnet new serverless.Annotations --output LambdaAnnotations
はじめましょう
Lambda アノテーションは、AWS Toolkit for Visual Studio と .NET CLI の両方で、AWS Serverless Applicationのデフォルトのプロジェクトテンプレートになりました。まずは、AWS Toolkit for Visual StudioからLambdaアノテーションを使った新しいAWS Serverless Applicationを作成してみましょう:
- Visual Studioを開き、新しいAWS Serverless Applicationを作成します。
- プロジェクト名をLambdaAnnotationsにします。
- Select BlueprintページでAnnotations Frameworkを選択し、Finishを選択します。
作成されたプロジェクトには、Function.csファイルに電卓をREST APIとしてシミュレートするLambda関数のコレクションが含まれています。Add
Lambda関数を見ると、LambdaFunction
属性がC#メソッドをCloudFormationテンプレートと同期されたLambda関数として識別しています。HttpApi
属性は、CloudFormationテンプレートにAPI Gatewayイベント設定を追加します。
[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
public int Add(int x, int y, ILambdaContext context)
{
var sum = _calculatorService.Add(x, y);
context.Logger.LogInformation($"{x} plus {y} is {sum}");
return sum;
}
注:API Gateway用ではないLambda関数を書いている場合でも、LambdaFunction属性でLambda Annotationsを使用し、CloudFormationテンプレートでイベントソースを設定することができます。将来的には、API Gatewayと同様のイベント属性をSQSやS3のような他のサービスにも追加したいと考えています。
プロジェクト内のserverless.templateファイルは、Lambda関数のデプロイに使われるCloudFormationテンプレートです。LambdaFunction属性でデコレートされた各C#メソッドは、テンプレート内で対応する宣言を持ちます。下のスクリーンショットのHandlerフィールドが、Lambdaアノテーションのソースジェネレータによって生成されたメソッドに設定されていることに注目してください。
スクラッチからアプリケーションを構築する
Lambda Annotationsをよりよく理解するために、新しいサーバーレスアプリケーションを作ってみましょう。これから作るサーバーレスアプリケーションは、Amazon Translateサービスを使ってテキストを他の言語に翻訳します。その前に電卓APIを削除する必要があります。Functions.csファイルを開き、Functionsクラス内のコードをすべて削除します。.NETプロジェクトを再コンパイルすると、serverless.templateはLambda関数を宣言しなくなります。
依存性注入
依存性注入は、Lambda Annotations Frameworkのファーストクラスの市民です。ASP.NET Coreアプリケーションで依存性注入を行うのと同じように、Lambda関数で依存性注入を行うことができます。依存性注入は、LambdaStartup
属性とLambdaFunction
属性を通してLambda関数に実装されます。Lambda Annotations Frameworkで依存性注入を活用する方法を見てみましょう。
Startup.csを開きます。ConfigureServicesメソッドは、依存性注入のためのオブジェクトを繋ぐのに使用されます。私たちのアプリケーションでは、Amazon TranslateのAWS SDKサービスクライアントを注入する必要があります。
- 以下のNuGetパッケージをLambdaAnnotationsプロジェクトに追加します:
- AWSSDK.Translate
- AWSSDK.Extensions.NETCore.Setup
- StartupクラスのConfigureServicesメソッドに以下のコードを追加します:
services.AddAWSService<Amazon.Translate.IAmazonTranslate>();
Startup.csファイルは以下のようになるはずです:
using Microsoft.Extensions.DependencyInjection;
namespace LambdaAnnotations
{
[Amazon.Lambda.Annotations.LambdaStartup]
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAWSService<Amazon.Translate.IAmazonTranslate>();
}
}
}
StartupクラスがLambdaStartup
属性でデコレートされていることに注目してください。これはLambda Annotations Frameworkに、Startupクラスが依存関係を繋ぐのに使われることを伝えます。
これで完了です!依存性注入のためにAWSサービスを設定し、直接インスタンス化するのではなく、Lambda関数に注入できるようになりました。
関数の依存関係を設定したら、Lambda関数に依存関係を注入する方法は2つあります:Lambda関数のコンストラクタを経由する方法と、Lambda関数のメソッドに直接注入する方法です。コンストラクタへの注入は、関数呼び出しの間で共有できるサービスや、インスタンス化が重いサービスに最適です。メソッドインジェクションは、メソッド呼び出しごとにインスタンス化する必要があるサービスに適しています。
このアプリケーションでは、Translate サービスクライアントを関数呼び出し間で共有できるので、コンストラクタにサービスクライアントをインジェクトできます。これを行うには、Functionsクラスに戻り、IAmazonTranslate
パラメータを宣言するコンストラクタを追加します。
public class Functions
{
private IAmazonTranslate _translateClient;
public Functions(IAmazonTranslate translateClient)
{
_translateClient = translateClient;
}
}
注 今回のアプリケーションではメソッド・インジェクションを使用しませんが、Lambda関数のパラメータにFromServices属性を使用することで、メソッド・インジェクションを行うことができます。
翻訳Lambda関数の実装
翻訳関数の実際の作業は、TranslateFromEnglish
メソッド内で行われます。関数のコンストラクタに注入された IAmazonTranslate
クライアントを使用して、string text
パラメータで指定されたテキストをstring targetLanguageCode
パラメータで指定された言語に翻訳します。以下のコードをコピーして、Functions クラスに貼り付けます。
public async Task<string> TranslateFromEnglish(string targetLanguageCode, string text)
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return response.TranslatedText;
}
ここで、TranslateFromEnglish
メソッドがLambda関数であることをLambdaFunction
属性をデコレートすることによってAnnotations Frameworkに伝える必要があります。LambdaFunctionAttribute
はPolicies
と呼ばれるプロパティを公開しており、Lambda関数が必要とするIAMポリシーを定義することができます。この関数には2つのIAMポリシーが必要です: TranslateReadOnly と AWSLambdaBasicExecutionRole です。
また、TranslateFromEnglish
メソッドを HttpApi
属性でデコレートすることで、API Gateway と Lambda 関数を繋ぐ方法を Annotations Framework に伝える必要があります。HttpApi
属性は、LambdaHttpMethod
パラメータと、関数のルートを定義する文字列テンプレート・パラメータを受け取ります。この関数は POST
リクエストを処理し、これらのリクエストが /translate/{targetLanguageCode}
に送られたときにトリガーされます。
関数によって翻訳されるテキストは POST リクエストのBodyに含まれるので、文字列 text パラメータに FromBody
属性を追加する必要があります。
更新した TranslateFromEnglish メソッドは次のようになります:
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<string> TranslateFromEnglish(string targetLanguageCode, [FromBody] string text)
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return response.TranslatedText;
}
これは良いスタートですが、おそらく本番コードとしては使えないでしょう。例えば、targetLanguageCode
が有効な言語コードでなかったり、翻訳するテキストがなかったり、無効であったり、何か予期せぬエラーが発生した場合はどうなるのでしょうか?おそらく、何か問題が発生したことを知らせる何らかの方法が必要でしょう。
メソッドの戻り値の型をIHttpResult
に変更し、HttpResults
ユーティリティクラスを使用して、必要なステータスコードのIHttpResult
を作成することで、エラーを伝えることができます。以下のコードでは、翻訳が成功した場合は Ok
レスポンスが返されます。UnsupportedLanguagePairException
例外が発生した場合は targetLanguageCode
が無効であったことを意味し、BadRequest
レスポンスが返されます。その他の例外に対しては、InternalServerError
応答が返されます。
OK
レスポンスの一部としてヘッダを含める方法を示すために AddHeader
が使用されています。
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<IHttpResult> TranslateFromEnglish(string targetLanguageCode, [FromBody] string text)
{
try
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return HttpResults.Ok(response.TranslatedText)
.AddHeader("target-language", targetLanguageCode);
}
catch (UnsupportedLanguagePairException)
{
return HttpResults.BadRequest($"Translating from English to {targetLanguageCode} is not supported");
}
catch (Exception ex)
{
return HttpResults.InternalServerError();
}
}
例外が丁寧に処理されるようになったので、問題をデバッグするために多くのロギングが利用できるようにする必要があります。Lambda Annotationsを使用する場合、従来のLambda関数のプログラミングモデルと同様に、ILambdaContext
をメソッドのシグネチャに追加することができます。実際に翻訳されるテキストは大きくなる可能性があるため、テキストはデバッグ用としてログに記録されます。環境変数 AWS_LAMBDA_HANDLER_LOG_LEVEL
が debug に設定されていない限り、LogDebug
メソッドは CloudWatch Logs に書き込まれません。
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<IHttpResult> TranslateFromEnglish(string targetLanguageCode,
[FromBody] string text, ILambdaContext context)
{
try
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
context.Logger.LogDebug("Text to translate:");
context.Logger.LogDebug(text);
var response = await _translateClient.TranslateTextAsync(request);
context.Logger.LogDebug("Translation:");
context.Logger.LogDebug(response.TranslatedText);
return HttpResults.Ok(response.TranslatedText)
.AddHeader("target-language", targetLanguageCode);
}
catch(UnsupportedLanguagePairException)
{
context.Logger.LogWarning($"Invalid target language code: {targetLanguageCode}");
return HttpResults.BadRequest($"Translating from English to {targetLanguageCode} is not supported");
}
catch (Exception ex)
{
context.Logger.LogError("Unknown error performing translate");
context.Logger.LogError(ex.ToString());
return HttpResults.InternalServerError();
}
}
この時点でLambda関数は本番環境にデプロイできるようになったので、AWS Lambdaにプッシュする準備が整いました。.NETプロジェクトを右クリックし、Published to AWS Lambda…メニューオプションを選択することで実行できます。AWS Toolkit for Visual Studioは、Lambda AnnotationsがC#コードと同期されたCloudFormationテンプレートを使って、CloudFormationを通してデプロイを開始します。
Lambda AnnotationsはC#コンパイラの一部としてCloudFormationテンプレートと同期しているので、SAMのような他のツールもデプロイに使用できます。
コード生成
Lambda Annotations Frameworkは、ソースジェネレータを活用して、アノテーションされたLambda関数を通常のLambdaプログラミングモデルに変換します。これは、Lambda Annotations Frameworkの素晴らしい「クオリティ・オブ・ライフ」機能であり、あなたが書く必要のある定型的なコードの量を大幅に削減します。
生成されたLambda関数を見てみましょう。Visual StudioのSolution Explorerウィンドウで:
- LambdaAnnotationsプロジェクトに移動する
- Dependencyを展開する
- Analyzersを展開する
- Amazon.Lambda.Annotations.SourceGeneratorを展開する
- Amazon.Lambda.Annotations.SourceGenerator.Generatorを展開する
- Functions_TranslateFromEnglish_Generated.g.cs を開く
Lambda Annotations Frameworkは、以下のことを行いました:
- Amazon API Gatewayのイベントソースに準拠するためのメソッドシグネチャの更新
- パラメータチェックと例外処理
- Amazon API Gatewayに準拠するための戻り値の型の更新
結論
Lambda Annotations Frameworkを使って開発を始める最も簡単な方法は、Visual studio 2022をダウンロードし、AWS Toolkit for Visual Studio拡張をインストールすることです。その後、AWS Toolkit for Visual Studioに同梱されているAWS Serverless Applicationプロジェクトテンプレートを使用して開始することができます。また、手動で Amazon.Lambda.Annotations NuGet パッケージをサーバーレスアプリケーションに取得することで、Annotations Frameworkを使用することもできます。使ってみて頂き、私たちのgithubリポジトリで感想を聞かせてください。
この投稿では、Lambda Annotations Frameworkを使用してAPI GatewayベースのLambda関数を作成することに注目しました。他のタイプのLambda関数も、LambdaFunction
属性を使用することで、依存性注入とCloudFormationの同期を利用することができます。イベントソースはCloudFormationテンプレートで設定できます。
- 以下は便利なリンクです:
フレームワーク関連の作業はすべて、githubのaws/aws-lambda-dotnetというdotnet lambdaのメインリポジトリの一部です。 - Annotationsのソースコード・ライブラリは、メイン・リポジトリ内のここにあります。
- 上記のフォルダには、現在サポートされている様々なイベントとパラメータ属性の一覧を記載した README.md があります。
Brad Webber ブラッドはアマゾン・ウェブ・サービス(AWS)のシニア・マイクロソフト・ソリューション・アーキテクト。彼がコードを書き始めたのは、Visual Basic 6がまだ一般的だった頃です。最近では、組織の.NETワークロードのモダン化とクラウドへの移行を支援することにほとんどの時間を費やしています。 |
|
Norm Johansonは、20年以上ソフトウェア開発者としてあらゆる種類のアプリケーションを開発してきました。2010年以来、彼はAWSで.NET開発者のエクスペリエンスにフォーカスして働いています。Twitter @socketnorm と GitHub @normj で彼を見つけることができます。 |
日本語翻訳はSAの福井厚が担当しました。原文はこちらです。