Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

.NETなCDKで.NETなLambdaを自動デプロイしていく

はじめまして、今年の3月にSansan事業部プロダクト開発部にjoinしました辻田です。.NETは全くの未経験でしたが.NETエンジニアとして日々奮闘中です。

初投稿のこの記事ではわたしが趣味でちょくちょく触ってるAWS CDKについて書こうと思います。CDKの言語ではTypeScriptが圧倒的に人気ですが、今回はC#の勉強のためにもC#版を使って、AWS LambdaとAPI Gatewayで超簡単なAPIを構築、CI/CDできるようにするまでを紹介していきます。 

成果物はこちらです。

github.com

使用技術

.NET Core 3.1.408
AWS CDK 1.98.0
AWS Lambda
API Gateway

.NET Core, CDKはインストール済みであることを前提として進めます。
.NET Core CLI で生成したLambdaプロジェクトのバージョンをそのまま使用しています。(Lambdaで使用できる.NET Coreランタイムの最新のバージョンは3.1系です)

ディレクトリ構成

.
├── .github
│   └── workflows
│       └── deploy.yml
├── cdk.json
├── lambda
│   └── HelloHandler
│       ├── src
│       │   └── HelloHandler
│       │       ├── Function.cs
│       │       ├── HelloHandler.csproj
│       │       └── aws-lambda-tools-defaults.json
│       └── test
│           └── HelloHandler.Tests
│               ├── Folder.DotSettings.user
│               ├── FunctionTest.cs
│               └── HelloHandler.Tests.csproj
└── src
    ├── CsCdkSample
    │   ├── CsCdkSample.csproj
    │   ├── CsCdkSampleStack.cs
    │   ├── GlobalSuppressions.cs
    │   └── Program.cs
    └── CsCdkSample.sln

最終的なディレクトリ構成は上記のようになります。src配下にCDKのソース、lambda配下にlambdaのソースがある感じです。

初期構築〜S3バケット作ってみるまで

AWS公式の手順を参考にしながら作っていきます。

CDKで雛形を作成する

$ mkdir cs-cdk-sample
$ cd cs-cdk-sample
$ cdk init app --language csharp

上記コマンドを打つだけでCDKプロジェクトの雛形を作ってくれます。

f:id:misaosyushi:20210513202757p:plain

src配下にCsCdkSample.slnが作られたので、これをIDEで開きます。初期状態は以下のようになってます。

f:id:misaosyushi:20210513202818p:plain

S3のパッケージを追加

まずはスモールスタートで、S3バケットを追加してデプロイしてみます。

src/CsCdkSample配下にcsprojファイルがあるので、ここに移動してパッケージを追加します。

$ cd src/CsCdkSample
$ dotnet add package Amazon.CDK.AWS.S3

CDKではAWSリソースごとにAWS Construct Libraryというライブラリが用意されているので、構築したいリソースのパッケージを追加してコードを書いていくことになります。
このライブラリにはAWSのベストプラクティスが定義されているため、少ないコード量で記述可能になっています。

.NET verのドキュメントは以下です。

https://docs.aws.amazon.com/cdk/api/latest/dotnet/api/index.html

S3バケットを追加、デプロイ

src/CsCdkSample/CsCdkSampleStack.csにS3バケットの定義を追加し、デプロイしてみます。

using Amazon.CDK;
using Amazon.CDK.AWS.S3;

namespace CsCdkSample
{
    public class CsCdkSampleStack : Stack
    {
        internal CsCdkSampleStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            var bucket = new Bucket(this, "CsCdkSampleBucket", new BucketProps {
                Versioned = true
            });
        }
    }
}
$ cdk bootstrap // CDKデプロイ管理用の環境(S3バケット)を作成(初回のみ実行でOK)
$ cdk diff // 差分確認
$ cdk deploy // デプロイ

デプロイ完了したらコンソールからS3を見てみると、無事に作成されてることが確認できました🎉

f:id:misaosyushi:20210511210139p:plain

LambdaとAPI Gatewayを追加する

続いて.NET CoreなLambdaとAPI Gatewayを追加していきます

Lambdaのテンプレートを作成

プロジェクトルートディレクトリにlambdaディレクトリを作成し、その配下にLambdaプロジェクトを作ります。

$ mkdir lambda && cd lambda
$ dotnet new lambda.EmptyFunction --name HelloHandler --profile default --region ap-northeast-1

f:id:misaosyushi:20210513202843p:plain

これでLambdaの雛形と、Lambdaに対するテストプロジェクトが作成されます。

  • src/HelloHandler/Function.cs
    • Lambda本体のコード
  • aws-lambda-tools-defaults.json
    • Lambdaをデプロイするときに指定するコマンドラインオプションの場所の情報
    • あとでCDKでハンドラーの場所を指定するときにfunction-handlerの値を指定します
"profile": "default",
"region": "ap-northeast-1",
"configuration": "Release",
"framework": "netcoreapp3.1",
"function-runtime": "dotnetcore3.1",
"function-memory-size": 256,
"function-timeout": 30,
"function-handler": "HelloHandler::HelloHandler.Function::FunctionHandler"
  • test/HelloHandler.Tests/FunctionTest.cs
    • Function.csに対するテスト

レスポンス200を返すLambdaを作る

API Gatewayにレスポンスを返すため、Amazon.Lambda.APIGatewayEventsのパッケージを追加

$ dotnet add package Amazon.Lambda.APIGatewayEvents

lambda/HelloHandler/src/HelloHandler/Function.csを修正

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

// 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 HelloHandler
{
    public class Function
    {
        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="request"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
        {
            return new APIGatewayProxyResponse
            {
                Body = "Hello CDK!",
                StatusCode = (int) HttpStatusCode.OK
            };
        }
    }
}

雛形は作らている状態なので、パッケージ追加してfunctionの中身をレスポンス200返すように書きかえるだけです。

デプロイパッケージを作成

以下のコマンドでデプロイパッケージを作成します。

$ dotnet publish

このパッケージはLambda関数のコンパイル済みアセンブリと、そのアセンブリのすべての依存関係が含まれます。JavaやKotlinでいうfat-jarと同じようなものだと理解しています。

ちなみにパッケージを作るにはNET Core CLIを使ったほうがLambda専用に最適化されるので推奨されているらしいですが、dotnet lambda deploy-functionのコマンドしか見当たらず、これはデプロイまで実行されてしまうため dotnet publish で対応することにしました。

CDKにLambdaとAPI Gatewayを定義する

まずはConstruct Libraryを追加する必要があるため以下を実行します。(これもS3の時と同じくcsprojがあるディレクトリで実行します。)

$ dotnet add package Amazon.CDK.AWS.Lambda
$ dotnet add Amazon.CDK.AWS.APIGateway

src/CsCdkSample/CsCdkSampleStack.csにLambdaとAPI Gatewayを定義していきます。

using Amazon.CDK;
using Amazon.CDK.AWS.APIGateway;
using Amazon.CDK.AWS.Lambda;

namespace CsCdkSample
{
    public class CsCdkSampleStack : Stack
    {
        internal CsCdkSampleStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
        {
            var helloLambda = new Function(this, "HelloHandler", new FunctionProps
            {
                Runtime = Runtime.DOTNET_CORE_3_1,
                Code = Code.FromAsset("./lambda/HelloHandler/src/HelloHandler/bin/Debug/netcoreapp3.1/publish"),
                Handler = "HelloHandler::HelloHandler.Function::FunctionHandler"
            });

            new LambdaRestApi(this, "HelloApi", new LambdaRestApiProps
            {
                Handler = helloLambda
            });
        }
    }
}

ソースコードで見るとどんなリソースが構築されるのか一目瞭然です。引数やプロパティの説明は公式ドキュメントで丁寧に説明されているので省略させていただきます。

今回の注意点としてはLambdaのCodeにはdotnet publishで生成されたパス(CsCdkSampleStack.csからの相対パス)を指定することと、Handlerの指定の仕方はランタイムによって結構違うのでちゃんと調べましょう、という点かなと思います。

あと、API GatewayではLambdaRestApiを使用することでLambdaプロキシ統合を使用したAPI Gatewayが構築できます。バックエンドにLambdaを使用しない場合はRestApiクラスあたりを使用することになります。

デプロイ

必要なリソースが定義できたのでcdk diffで差分確認、cdk deployでデプロイしていきます。

f:id:misaosyushi:20210512200927p:plain

f:id:misaosyushi:20210512200943p:plain

↑ LambdaやAPI Gatewayの他にCloudWatch Logsへ書き込むためのIAMロールなどもよしなに作成されることが分かります。先ほど作ったS3も削除されます。

デプロイ完了したら、アウトプットとしてAPI Gatewayのエンドポイントがコンソールに出力されるはずなので叩いてみて想定したレスポンスが返ってきたら成功です🎉

$ curl  https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Hello CDK!

CI/CDできるようにする

手軽に使えるGitHub Actionsを使ってテストとデプロイを自動化していきます。

Lambdaのテストを書く

test/HelloHandler.Tests/FunctionTest.csにテストを書きます。こちらもdotnet new lambda.EmptyFunctionしたときに既に雛形は作成されてるので、レスポンスの内容を書きかえるくらいです。

using System.Net;
using Amazon.Lambda.APIGatewayEvents;
using Xunit;
using Amazon.Lambda.TestUtilities;

namespace HelloHandler.Tests
{
    public class FunctionTest
    {
        [Fact]
        public void TestHelloCdkFunction()
        {
            var function = new Function();
            var context = new TestLambdaContext();
            var apiGatewayProxyRequest = new APIGatewayProxyRequest();
            var response = function.FunctionHandler(apiGatewayProxyRequest, context);

            Assert.Equal("Hello CDK!", response.Body);
            Assert.Equal((int) HttpStatusCode.OK, response.StatusCode);
        }
    }
}

AWSクレデンシャル情報をシークレットに登録する

リポジトリの Settings -> Secrets からAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを登録しておきます。

f:id:misaosyushi:20210513083343p:plain

actionsでワークフローを作る

下準備が完了したらワークフローを作っていきます。ありがたいことに.NETのテンプレがあったのでこれを元に必要なものを追加していきます。

f:id:misaosyushi:20210513084743p:plain

テンプレの中身はこんな感じです。

name: .NET

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.x
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-build --verbosity normal

actions/setup-dotnetで.NET SDKをインストールなどをしてくれて、dotnet-versionでバージョン指定するみたいです。Lambdaのランタイムは3.1系なのでバージョンの修正と、その他不要そうなコマンドは削除します。

あとはCDKをインストールするためのNode.jsをいれるためにactions/setup-nodeも追加して、コマンド実行したい順にstepsを定義していきます。

name: .NET

on:
  push:
    branches: [ master ]

jobs:
  build_and_deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup .NET
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.x

    - name: Setup Node.js
      uses: actions/setup-node@v1
      with:
        node-version: 12.x

    - name: Setup AWS CDK
      run: npm install -g aws-cdk@1.98.0

    - name: Lambda test
      run: |
        cd lambda/HelloHandler/test/HelloHandler.Tests
        dotnet test

    - name: Build
      run: |
        cd lambda/HelloHandler/src/HelloHandler
        dotnet publish

    - name: Deploy
      env:
        AWS_DEFAULT_REGION: 'ap-northeast-1'
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: cdk deploy --require-approval "never"

GitHub Actionsのstepsは直列に実行されるので、.NET, Node, CDKのセットアップ -> Lambdaのテスト -> ビルド -> デプロイ の順で書きました。

セットアップ周りはjobに分けて並列で実行したほうが良さそうですが、この規模なのでこのままいってしまいます。

また、cdk deployするときは y/n の入力が必要なのですが、CI上だと入力待ちのままになってタイムアウトしてしまうため--require-approval "never"オプションをつけてそのままデプロイするようにしてます。

f:id:misaosyushi:20210513091356p:plain

これでCI/CD環境も完成です🎉

最後に

CDKをC#で書いたのは初めてでしたが、tsでもC#でも書きっぷりはあまり変わらないので取っ付きやすかったです。雛形作ってくれるのとConstruct Libraryが優秀なおかげであまりC#をがっつり書くことはできなかったですが、知らなかった.NETコマンドとかを色々学べたので良かったです。

いまはIaCでもCDK, Terraform, Pulumi, CFn(AWS), Deployment Manager(GCP)などいろんな選択肢がありますが、最適化されたライブラリを持っていて、かつプログラミング言語で書けるCDKがわたしは気に入っています。(Pulumiは触ったことがないのでわからないですが。。。)

CDKはAWS以外にも、去年にTerraform for CDKが発表されてGCPやAzureにも対応可能になる(いまはまだアルファステージ)し、CDKのアップデートはとても早いので今後が楽しみです。

さらにこの記事を書いてる途中に知ったのですが今年の4/30からCDKでGoのサポートも開発者プレビュー版で出てました。。。素晴らしい。いまはGoで遊んでる場合じゃないのでもう少し余裕がでてきたら触ってみたいと思います。

最後まで読んでいただきありがとうございました!

参考

https://cdkworkshop.com/40-dotnet.html

https://d1.awsstatic.com/webinars/jp/pdf/services/20200303_BlackBelt_CDK.pdf

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html

https://docs.aws.amazon.com/cdk/api/latest/dotnet/api/index.html

© Sansan, Inc.