はじめまして、今年の3月にSansan事業部プロダクト開発部にjoinしました辻田です。.NETは全くの未経験でしたが.NETエンジニアとして日々奮闘中です。
初投稿のこの記事ではわたしが趣味でちょくちょく触ってるAWS CDKについて書こうと思います。CDKの言語ではTypeScriptが圧倒的に人気ですが、今回はC#の勉強のためにもC#版を使って、AWS LambdaとAPI Gatewayで超簡単なAPIを構築、CI/CDできるようにするまでを紹介していきます。
成果物はこちらです。
使用技術
.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プロジェクトの雛形を作ってくれます。
src配下にCsCdkSample.sln
が作られたので、これをIDEで開きます。初期状態は以下のようになってます。
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を見てみると、無事に作成されてることが確認できました🎉
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
これで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
でデプロイしていきます。
↑ 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_ID
とAWS_SECRET_ACCESS_KEY
を登録しておきます。
actionsでワークフローを作る
下準備が完了したらワークフローを作っていきます。ありがたいことに.NETのテンプレがあったのでこれを元に必要なものを追加していきます。
テンプレの中身はこんな感じです。
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"
オプションをつけてそのままデプロイするようにしてます。
これで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