.NET Annotations Lambda Framework を使って .NET 6 のサーバーレス API を作成する

2022.12.06

いわさです。

この記事は 「AWS LambdaとServerless Advent Calendar 2022」と「ゆるWeb勉強会@札幌 Advent Calendar 2022」の 6 日目の記事となります。

AWS Lambda ではランタイムとして .NET を選択することが出来ます。
実は .NET 6 から .NET Annotations Lambda Framework というものが試験的に導入され、より直感的にサーバーレスアプリケーションの実装が出来るようになっています。
まだ触ったことが無かったのでそのあたりを本日は触ってみたいと思います。

本機能はプレビューです。今後変更が発生する場合があります。

通常はこう

Visual Studio 2022 の Lambda Serverless テンプレートを使って API Gateway + Lambda 関数な API を作成しようとすると通常は以下のような実装となります。

using System.Net;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace AWSServerless1;

public class Functions
{
    /// <summary>
    /// Default constructor that Lambda will invoke.
    /// </summary>
    public Functions()
    {
    }


    /// <summary>
    /// A Lambda function to respond to HTTP Get methods from API Gateway
    /// </summary>
    /// <param name="request"></param>
    /// <returns>The API Gateway response.</returns>
    public APIGatewayProxyResponse Get(APIGatewayProxyRequest request, ILambdaContext context)
    {
        context.Logger.LogInformation("Get Request\n");

        var response = new APIGatewayProxyResponse
        {
            StatusCode = (int)HttpStatusCode.OK,
            Body = "Hello AWS Serverless",
            Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
        };

        return response;
    }
}

通常は API Gateway のリクエストとレスポンスの処理のためにAPIGatewayProxyRequestAPIGatewayProxyResponseを使用すると思います。
さらに上記のコードを実装した後に SAM テンプレート上でもハンドラーの追加・変更が必要です。

serverless.template

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application.",
  "Resources": {
    "Get": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "AWSServerless1::AWSServerless1.Functions::Get",
        "Runtime": "dotnet6",
        "CodeUri": "",
        "MemorySize": 256,
        "Timeout": 30,
        "Role": null,
        "Policies": [
          "AWSLambdaBasicExecutionRole"
        ],
        "Events": {
          "RootGet": {
            "Type": "Api",
            "Properties": {
              "Path": "/",
              "Method": "GET"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
        "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
      }
    }
  }
}

ビジネスロジックのコード量にも依るとは思いますが、感覚的には Amazon API Gateway や AWS Lambda で利用するためのコードが大半な印象です。
通常の .NET 開発と比較すると開発中にもそのあたりを常に気をつける必要が出てきます。

AWS Toolkit for Visual Studio 2022 の .NET Annotations Lambda Framework を使う

ここからが本題です。

Visual Studio を使って .NET なサーバーレスアプリケーションを開発する場合は AWS Toolkit for Visual Studio 2022 を使用することが多いと思います。
この AWS Toolkit for Visual Studio 2022 にはサーバーレスアプリケーションのテンプレートが用意されており、その中のひとつに.NET Annotations Lambda Frameworkがあります。
このフレームワークを導入するとアノテーションを使うことで API Gateway のプロキシクラスや SAM テンプレート周りの繋ぎこみの部分を良い感じに吸収してくれます。

新規プロジェクトを AWS Serverless Application (.NET Core - C#) のプロジェクトテンプレートで作成し Blueprint は Annotations Framework を選択します。

SAM テンプレートに関連するため、AWS Lambda Project (.NET Core - C#) のプロジェクトテンプレートでは Blueprint 一覧には表示されません。
また、Roslyn のソースジェネレータ機能を使っているため F# でも利用出来ません。

関数を実装してみる

試しにデフォルトの関数を削除し、新規で関数を作成してみます。(以下ハイライト部分)
ポイントはLambdaFunction属性とHttpApi属性です。

Function.cs

using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.APIGateway;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace AWSServerless2
{
    /// <summary>
    /// A collection of sample Lambda functions that provide a REST api for doing simple math calculations. 
    /// </summary>
    public class Functions
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        public Functions()
        {
        }

        [LambdaFunction()]
        [HttpApi(LambdaHttpMethod.Get, "/hoge/{x}/{y}")]
        public bool Hoge(string x, string y, ILambdaContext context)
        {
            return x == y ;
        }
    }
}

そうすると SAM テンプレートも以下のように自動でハンドラーや API 定義が作成されました。
アノテーションに従ってハンドラーやパスが自動生成されています。

serverless.template

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Transform": "AWS::Serverless-2016-10-31",
  "Description": "An AWS Serverless Application.",
  "Resources": {
    "AWSServerless2FunctionsHogeGenerated": {
      "Type": "AWS::Serverless::Function",
      "Metadata": {
        "Tool": "Amazon.Lambda.Annotations",
        "SyncedEvents": [
          "RootGet"
        ]
      },
      "Properties": {
        "Runtime": "dotnet6",
        "CodeUri": ".",
        "MemorySize": 256,
        "Timeout": 30,
        "Policies": [
          "AWSLambdaBasicExecutionRole"
        ],
        "PackageType": "Zip",
        "Handler": "AWSServerless2::AWSServerless2.Functions_Hoge_Generated::Hoge",
        "Events": {
          "RootGet": {
            "Type": "HttpApi",
            "Properties": {
              "Path": "/hoge/{x}/{y}",
              "Method": "GET",
              "PayloadFormatVersion": "2.0"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
        "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
      }
    }
  }
}

これすごいですね。
そのままデプロイしてみましょう。

そのまま機能していますね。

% curl https://rhziyx3es3.execute-api.ap-northeast-1.amazonaws.com/hoge/111/222
False
% curl https://rhziyx3es3.execute-api.ap-northeast-1.amazonaws.com/hoge/111/111
True

汎用的なインターフェースで関数を作成し、アノテーションをいくつか設定するだけで API Gateway と Lambda を構成することが出来ました!
あまり API Gateway や Lambda というところを意識せずにまるで標準の .NET 関数を実装する感覚でコードを記述しデプロイすることが出来ました。
これによって開発者はビジネスロジックに集中することが出来るというわけですね。良さげです。

さいごに

簡単ではありますが、本日は .NET Annotations Lambda Framework を使って .NET 6 のサーバーレス API を作成してみました。
DI の仕組みも用意されています。まだプレビュー中ですが興味のある方は以下もご参照ください。