.NET Core アプリを AWS Lambda で動かす

# 本投稿について

この投稿では、以下の方法について説明しています。

1. .NET Core で AWS Lambda 関数を開発する方法
2. Lambda 関数として動かすことができる ASP.NET アプリの開発方法
3. 既存の ASP.NET Core アプリを Lambda 関数として動かす方法
  
(※ 本記事は、2020/06/22時点での最新の.NET Core バージョンや各種ツールを使用することを想定に書いています)

# 1. .NET Core の Lambda 関数を作成する

ここでは .NET Core で Lambda 関数を開発する方法について説明します。

## プロジェクトテンプレートをインストールする

まず、以下のコマンドで[Amazon.Lambda.Templatesパッケージ](https://www.nuget.org/packages/Amazon.Lambda.Templates)をインストールしてプロジェクトテンプレートを追加します。

```
dotnet new -i Amazon.Lambda.Templates
```

以下の様なテンプレートを使用できるようになります。

- lambda.SQS (SQSトリガの Lambda 関数)
- lambda.SNS (SNSトリガの Lambda 関数)
- lambda.S3 (S3イベントトリガの Lambda 関数)
- lambda.Kinesis (Kinesis Stream トリガの Lambda 関数)
- lambda.KinesisFirehose (Kinesis Firehose トリガの Lambda 関数)
- lambda.DynamoDB (DynamoDB Stream トリガの Lambda 関数)
- lambda.SimpleApplicationLoadBalancerFunction (ELB(ALB) トリガの Lambda 関数)
- serverless.AspNetCoreWebApp (Lambda上で動く ASP.NET Core Razor Pages アプリ)
- serverless.AspNetCoreWebAPI (Lambda上で動く ASP.NET Core Web API アプリ)

以下のコマンドでインストールされたテンプレートを確認できます。

```
dotnet new lambda --list
```

## テンプレートからプロジェクトを作成する

例えば、SQSのキューにエンキューされたメッセージをトリガーにして起動する Lambda 関数を作成する場合、以下のコマンドでテンプレートを使用してプロジェクト(テストコードを含む)を作成することができます。

```sh
dotnet new lambda.SQS --name {関数名} --region {リージョン名} --profile {プロファイル名}
# 例：dotnet new lambda.SQS --name mysqsfunc --region ap-northeast-1 --profile default
```

上記の例のコマンドで作成される Lambda 関数のコード(Function.cs)は以下の様になるかと思います (コメントは除いています)。
既定の設定のままの場合、以下の FunctionHandler メソッドが Lambda 関数起動時に呼び出されることになります。

```cs:Function.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;

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

namespace mysqsfunc
{
    public class Function
    {
        public Function()
        {
        }

        public async Task FunctionHandler(SQSEvent evnt, ILambdaContext context)
        {
            foreach(var message in evnt.Records)
            {
                await ProcessMessageAsync(message, context);
            }
        }

        private async Task ProcessMessageAsync(SQSEvent.SQSMessage message, ILambdaContext context)
        {
            context.Logger.LogLine($"Processed message {message.Body}");
            await Task.CompletedTask;
        }
    }
}
```

以下がデプロイされる Lambda 関数に関する設定を定義しているファイル(aws-lambda-tools-defaults.json)の中身です。プロジェクトで指定している .NET Core のバージョンと、Lambda 関数で使用するバージョン("framework", "function-runtime")を揃える必要があるので注意してください。

```json:aws-lambda-tools-defaults.json
{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
    "dotnet lambda help",
    "All the command line options for the Lambda command can be specified in this file."
  ],
  "profile": "default",
  "region": "ap-northeast-1",
  "configuration": "Release",
  "framework": "netcoreapp2.0",
  "function-runtime": "dotnetcore2.0",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "mysqsfunc::mysqsfunc.Function::FunctionHandler"
}
```

## プロジェクトを AWS へデプロイする

AWS 上にデプロイする場合、.NET Coreツールである、[Amazon.Lambda.Tools .NET Core Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli) を使用すると便利です。以下のコマンドでインストールすることができます。

```sh
dotnet tool install -g Amazon.Lambda.Tools
```

本ツールをインストールすると、デプロイをする.NET Coreアプリのプロジェクトファイル(.csproj)と同じフォルダにて以下のコマンドを実行することで、プロジェクトを AWS へデプロイすることができます。

```sh
dotnet lambda deploy-function {Lambda関数名}
# 例：dotnet lambda deploy-function samplesqsapp
```

このデプロイで Lambda 関数を新たに作成する場合、以下のように、どの IAM ロールを Lambda 関数に付与するかを聞かれますので、付与する既存の IAM ロールを番号で指定するか、IAM ロールを新規作成する(Create new IAM Role)ように指定します。

```sh
Select IAM Role that to provide AWS credentials to your code:
    1) RoleA
    2) RoleB
    ...
    X) *** Create new IAM Role ***
```

"New Lambda function created" と表示されたら、Lambda 関数のデプロイが完了しています。が、コードをデプロイしただけなので、トリガの設定を自身で行う必要があります。

# 2. Lambda で動かす ASP.NET Core アプリ を作成する

ここでは Lambda 関数として動かすことができる ASP.NET Core アプリの開発方法について説明します。

## テンプレートからプロジェクトを作成する

[Amazon.Lambda.Templatesパッケージ](https://www.nuget.org/packages/Amazon.Lambda.Templates)をインストールして、以下のプロジェクトテンプレートを使用することで、Lambda 関数として動かすことができる ASP.NET Core アプリを簡単に作成できます。

- serverless.AspNetCoreWebApp (Lambda上で動く ASP.NET Core Razor Pages アプリ)
- serverless.AspNetCoreWebAPI (Lambda上で動く ASP.NET Core Web API アプリ)

```sh
dotnet new serverless.AspNetCoreWebApp --name {関数名} --region {リージョン名} --profile {プロファイル名}
# 例：dotnet new serverless.AspNetCoreWebApp --name myaspnetapp --region ap-northeast-1 --profile default
```

## プロジェクトを AWS へデプロイする

そして、上記と同じく、[Amazon.Lambda.Tools .NET Core Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli) を使用することで、以下のコマンドで AWS上 の Lambda 関数としてデプロイすることができます。deploy-function コマンドとは異なり、Cloud Formation のスタックとして、Lambda 関数に加えて、Amazon API Gateway の必要なリソース(ステージ等)などがデプロイされます。

```sh
dotnet lambda deploy-serverless
```

デプロイの際には、Cloud Formation のスタック名と、コードの格納先となる S3 バケット名を指定する必要があります。これらは aws-lambda-tools-defaults.json であらかじめ指定しておくことができます。

# 3. 既存の ASP.NET Core アプリを Lambda で動かす
上記で説明した方法は、Lambda 関数として動かすことができる ASP.NET Core を新規作成する方法ですが、ここでは既存の ASP.NET Core アプリを Lambda で動かす方法について説明します。

## 必要な Nuget パッケージをインストールする

Nuget パッケージの [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer/) をプロジェクトにインストールします。

```sh
dotnet add package Amazon.Lambda.AspNetCoreServer
```

## LambdaEntryPoint クラスを作成する

次に、以下の通りの LambdaEntryPoint クラスをプロジェクトに新規に追加します。  
これから作成する Lambda 関数を API Gateway 経由で呼び出す場合、LambdaEntryクラスは [APIGatewayProxyFunction](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs) 抽象クラスを、Application Load Balancer 経由で呼び出す場合は [ApplicationLoadBalancerFunction](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.AspNetCoreServer/ApplicationLoadBalancerFunction.cs) 抽象クラスを継承して実装する必要があります。これらのクラスを継承した場合、Init メソッドを実装する必要がありますので、以下の様に、スタートアップクラス(Startup.cs)を指定する処理を行わせます。そして、このクラスの FunctionHandlerAsync メソッドを Lambda のハンドラとして指定します (FunctionHandlerAsyncメソッドはLambdaEntryPoint クラスの親の親である AbstractAspNetCoreFunction クラスで[実装されています](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs#L431)ので、自身で実装する必要はありません)。

```csharp:LambdaEntryPoint.cs
using Microsoft.AspNetCore.Hosting;
using Amazon.Lambda.AspNetCoreServer;

namespace SampleAspNetCoreAppOnLambda
{
    public class LambdaEntryPoint : APIGatewayProxyFunction
    {
       protected override void Init(IWebHostBuilder builder)
       {
          builder.UseStartup<Startup>();
       }
    }
}
```

## デプロイを定義するファイルを準備する

そして、[Amazon.Lambda.Tools](https://github.com/aws/aws-extensions-for-dotnet-cli) で AWS 上へデプロイするために、aws-lambda-tools-defaults.json と serverless.template という名前のファイルを以下の通りに作成します。

```json:aws-lambda-tools-defaults.json
{
  "profile": "default",
  "region": "ap-northeast-1",
  "stack-name": "sampleaspnetcoreapp",
  "s3-bucket": "{コードを格納するS3バケット名}",
  "template": "serverless.template"
}
```

```json:serverless.template
{
  "Transform": "AWS::Serverless-2016-10-31",
  "Resources": {
    "AspNetCoreFunction": {
      "Type": "AWS::Serverless::Function",
      "Properties": {
        "Handler": "{既存アプリのアセンブリ名}::{LambdaEntryPointクラスの名前空間}.LambdaEntryPoint::FunctionHandlerAsync",
        "Runtime": "dotnetcore3.1",
        "CodeUri": "",
        "MemorySize": 512,
        "Timeout": 30,
        "Role": null,
        "Policies": [],
        "Environment": {
          "Variables": {}
        },
        "Events": {
          "ProxyResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/{proxy+}",
              "Method": "ANY"
            }
          },
          "RootResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/",
              "Method": "ANY"
            }
          }
        }
      }
    }
  },
  "Outputs": {
    "ApiURL": {
      "Description": "API endpoint URL for Prod environment",
      "Value": {
        "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
      }
    }
  }
}
```

serverless.template は AWS Serverless Application Model (AWS CloudFormation の拡張) のテンプレートになります。[このリファレンス](https://github.com/awslabs/serverless-application-model/blob/develop/versions/2016-10-31.md) を使用して自由に修正することができます。

## プロジェクトを AWS へデプロイする

そして、上記と同様に、以下のコマンドで、既存の ASP.NET Core アプリを Lambda 関数として AWS 上へデプロイすることができます。

```sh
dotnet lambda deploy-serverless
```

今回のデプロイも、先程と同様に Cloud Formation のスタックとして、Lambda 関数に加えて、Amazon API Gateway の必要なリソース(ステージ等)などがデプロイされます。

```
...
Created CloudFormation stack sampleaspnetcoreapp
   
Timestamp            Logical Resource Id                      Status                                   
-------------------- ---------------------------------------- ---------------------------------------- 
2020/07/06 14:00     sampleaspnetcoreapp                      CREATE_IN_PROGRESS                      
2020/07/06 14:00     AspNetCoreFunctionRole                   CREATE_IN_PROGRESS                      
2020/07/06 14:00     AspNetCoreFunctionRole                   CREATE_IN_PROGRESS                      
2020/07/06 14:00     AspNetCoreFunctionRole                   CREATE_COMPLETE                         
2020/07/06 14:01     AspNetCoreFunction                       CREATE_IN_PROGRESS                      
2020/07/06 14:01     AspNetCoreFunction                       CREATE_IN_PROGRESS                      
2020/07/06 14:01     AspNetCoreFunction                       CREATE_COMPLETE                         
2020/07/06 14:01     ServerlessRestApi                        CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApi                        CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApi                        CREATE_COMPLETE                         
2020/07/06 14:01     AspNetCoreFunctionProxyResourcePermissionProd CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApiDeploymentcfb7a37fc3    CREATE_IN_PROGRESS                      
2020/07/06 14:01     AspNetCoreFunctionProxyResourcePermissionProd CREATE_IN_PROGRESS                      
2020/07/06 14:01     AspNetCoreFunctionRootResourcePermissionProd CREATE_IN_PROGRESS                      
2020/07/06 14:01     AspNetCoreFunctionRootResourcePermissionProd CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApiDeploymentcfb7a37fc3    CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApiDeploymentcfb7a37fc3    CREATE_COMPLETE                         
2020/07/06 14:01     ServerlessRestApiProdStage               CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApiProdStage               CREATE_IN_PROGRESS                      
2020/07/06 14:01     ServerlessRestApiProdStage               CREATE_COMPLETE                         
2020/07/06 14:01     AspNetCoreFunctionProxyResourcePermissionProd CREATE_COMPLETE                         
2020/07/06 14:01     AspNetCoreFunctionRootResourcePermissionProd CREATE_COMPLETE                         
2020/07/06 14:01     sampleaspnetcoreapp                      CREATE_COMPLETE                         
Stack finished updating with status: CREATE_COMPLETE
   
Output Name                    Value                                             
------------------------------ --------------------------------------------------
ApiURL                         https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
```

上記の通りにメッセージが出力された後、スタックの出力値として、デプロイされた Web アプリケーションの URL が表示されます。