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 StudioLambda .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を使用していない場合は、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を作成してみましょう:

  1. Visual Studioを開き、新しいAWS Serverless Applicationを作成します。
  2. プロジェクト名をLambdaAnnotationsにします。
  3. Select BlueprintページでAnnotations Frameworkを選択し、Finishを選択します。

null

作成されたプロジェクトには、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サービスクライアントを注入する必要があります。

  1. 以下のNuGetパッケージをLambdaAnnotationsプロジェクトに追加します:
  • AWSSDK.Translate
  • AWSSDK.Extensions.NETCore.Setup
  1. 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に伝える必要があります。LambdaFunctionAttributePoliciesと呼ばれるプロパティを公開しており、Lambda関数が必要とするIAMポリシーを定義することができます。この関数には2つのIAMポリシーが必要です: TranslateReadOnlyAWSLambdaBasicExecutionRole です。

また、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_LEVELdebug に設定されていない限り、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 StudioSolution Explorerウィンドウで:

  1. LambdaAnnotationsプロジェクトに移動する
  2. Dependencyを展開する
  3. Analyzersを展開する
  4. Amazon.Lambda.Annotations.SourceGeneratorを展開する
  5. Amazon.Lambda.Annotations.SourceGenerator.Generatorを展開する
  6. 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

Norm Johansonは、20年以上ソフトウェア開発者としてあらゆる種類のアプリケーションを開発してきました。2010年以来、彼はAWSで.NET開発者のエクスペリエンスにフォーカスして働いています。Twitter @socketnorm と GitHub @normj で彼を見つけることができます。

日本語翻訳はSAの福井厚が担当しました。原文はこちらです。