CDKでLambda作ってSAMでローカルで動作確認したメモ

最近仕事でCDKを触り始めたので、個人でも適当なSlack botでも作ってみようとかと思って触ってみてます。デプロイがとてもラクとはいえ、Lambdaを動作確認しながら作っていくのに毎回デプロイするのはつらみです…

調べてみると、CDKで作ったLambdaでもSAMを使ってローカルで動かす方法があるようなので、スタックの作成からLambdaのローカル実行まで一通り試してみました。

# 環境

* OS
    * Windows10 WSL(Ubuntu 18.04 LTS)
    * macOS Mojave
* CDK 1.46.0
* SAM 0.53.0
* Docker 19.03.11
    * VirtualBox上のUbuntu 18.04 LTS

# CDKでLambdaの作成

## CDKインストール

npmでインストール。

```sh
# CDKインストール
npm install -g aws-cdk
/home/hotaru/.anyenv/envs/nodenv/versions/12.16.3/bin/cdk -> /home/hotaru/.anyenv/envs/nodenv/versions/12.16.3/lib/node_modules/aws-cdk/bin/cdk
+ aws-cdk@1.46.0
added 216 packages from 186 contributors in 13.791s
```

## テンプレート生成

まずは適当なディレクトリを作成。のちのちSlack botを作っていくリポジトリにする予定なので、それらしい名前で掘っておく。

```sh
mkdir slack-auto-stamp-bot
cd slack-auto-stamp-bot
```

`cdk init` でテンプレートを生成。オプションなしで叩くとどんなテンプレートがあるのか教えてくれます。

```sh
cdk init
Available templates:
* app: Template for a CDK Application
   └─ cdk init app --language=[csharp|fsharp|java|javascript|python|typescript]
* lib: Template for a CDK Construct Library
   └─ cdk init lib --language=typescript
* sample-app: Example CDK Application with some constructs
   └─ cdk init sample-app --language=[csharp|fsharp|java|javascript|python|typescript]
```

基本的には `app` を使います。言語はTypeScriptでいくので、

```sh
cdk init app -l typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...

(以下略)

```

と言った感じ。生成後のディレクトリ構成は以下の通り。

```
/slack-auto-stamp-bot
├── README.md
├── bin
│   └── slack-auto-stamp-bot.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── slack-auto-stamp-bot-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── slack-auto-stamp-bot.test.ts
└── tsconfig.json
```

`lib` 配下のファイルにリソースを定義していきます。その前に、構築したいリソース毎にパッケージが別れているので、必要なものを追加しておきます。

```sh
# LambdaとAPI Gatewayを追加
npm add @aws-cdk/aws-lambda @aws-cdk/aws-apigateway
npm WARN slack-auto-stamp-bot@0.1.0 No repository field.
npm WARN slack-auto-stamp-bot@0.1.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.1.3 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ @aws-cdk/aws-apigateway@1.46.0
+ @aws-cdk/aws-lambda@1.46.0
added 17 packages from 1 contributor and audited 794 packages in 13.799s

15 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

この記事ではLambdaしか使いませんが、のちのちAPI Gatewayを使用する予定なので一緒にいれました。
CDKでLambdaを作っていく前に、Lambda本体のコードを作ります。こちらはRubyで。

```sh
mkdir -p lambda/stamp-bot-function
cd lambda/stamp-bot-function/
touch app.rb
```

とりあえずeventをそのまま返すコードでも置いときます。

```ruby:lambda/stamp-bot-function/app.rb
# frozen_string_literal: true

require 'json'

def handler(event:, context:)
  {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.generate(event)
  }
end
```

今度はLambdaのリソースを作っていきます。すでに仕事で触っていたのでなんてことないですが、初めてのときはこちらが参考になりました。

* [AWS CDK Intro Workshop :: AWS Cloud Development Kit (AWS CDK) Workshop](https://cdkworkshop.com/)


```typescript:lib/slack-auto-stamp-bot-stack.ts
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'

export class SlackAutoStampBotStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props)

        const stampBotFunctionProps: lambda.FunctionProps = {
            runtime: lambda.Runtime.RUBY_2_7,
            code: lambda.Code.fromAsset('lambda/stamp-bot-function'),
            handler: 'app.handler'
        }
        const stampBotFunction = new lambda.Function(this, 'stampBotFunction', stampBotFunctionProps)
    }
}
```

`stampBotFunctionProps` のところでLambdaの設定をいろいろ書いてます。最低限 `runtime` 、 `code` 、 `handler` の指定が必要です。ちなみに `lambda.Code.fromAsset()` で渡しているパスは、プロジェクトルートからの相対パスです。
`Function` のコンストラクタには `Stack(このクラス自身)` 、 スタック内で一意となる論理ID、スタックの設定( 作成した `stampBotFunctionProps` )を渡します。

## デプロイ

デプロイする前に、初回はアセット格納用のS3バケットが必要になるので、 `cdk bootstrap` を実行して作成しておきます。

```bash
cdk bootstrap
(node:18789) ExperimentalWarning: The fs.promises API is experimental
 ⏳  Bootstrapping environment aws://xxxxxxxxxxxx/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/3 | 2:18:23 PM | CREATE_IN_PROGRESS   | AWS::S3::Bucket       | StagingBucket
 0/3 | 2:18:25 PM | CREATE_IN_PROGRESS   | AWS::S3::Bucket       | StagingBucket Resource creation Initiated
 1/3 | 2:18:47 PM | CREATE_COMPLETE      | AWS::S3::Bucket       | StagingBucket
 1/3 | 2:18:49 PM | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy
 1/3 | 2:18:50 PM | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy Resource creation Initiated
 2/3 | 2:18:50 PM | CREATE_COMPLETE      | AWS::S3::BucketPolicy | StagingBucketPolicy
 3/3 | 2:18:52 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit
 ✅  Environment aws://xxxxxxxxxxxx/ap-northeast-1 bootstrapped.
```

こんなスタックができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304815/11027a24-5fb6-aca6-4f29-d5f33299818f.png)

まずはビルドします。

```bash
npm run-script build
```

作成されるスタックの確認。 `cdk diff` でスタックの変更内容を確認できます。

```bash
cdk diff
Stack SlackAutoStampBotStack
IAM Statement Changes
┌───┬─────────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                            │ Effect │ Action         │ Principal                    │ Condition │
├───┼─────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${stampBotFunction/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                        │ Managed Policy ARN                                                             │
├───┼─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${stampBotFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter AssetParameters/81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9/S3Bucket AssetParameters81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9S3Bucket1EF80AD6: {"Type":"String","Description":"S3 bucket for asset \"81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9\""}
[+] Parameter AssetParameters/81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9/S3VersionKey AssetParameters81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9S3VersionKey462471F6: {"Type":"String","Description":"S3 key for asset version \"81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9\""}
[+] Parameter AssetParameters/81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9/ArtifactHash AssetParameters81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9ArtifactHash5F87EF54: {"Type":"String","Description":"Artifact hash for asset \"81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9\""}

Conditions
[+] Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}]}

Resources
[+] AWS::IAM::Role stampBotFunction/ServiceRole stampBotFunctionServiceRoleE8B84E9C
[+] AWS::Lambda::Function stampBotFunction stampBotFunction1C7A789E
```

Lambdaの実行に必要なIAMロールも勝手に作ってくれてます。
では、デプロイします。

```bash
# デプロイ
cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬─────────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                            │ Effect │ Action         │ Principal                    │ Condition │
├───┼─────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${stampBotFunction/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                        │ Managed Policy ARN                                                             │
├───┼─────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${stampBotFunction/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
SlackAutoStampBotStack: deploying...
[0%] start: Publishing 81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9:current
[100%] success: Published 81ffb93ce7b48a8e0dca23f447c0835e5b3d9832e0e2cebd6ead445127e6ddf9:current
SlackAutoStampBotStack: creating CloudFormation changeset...





 ✅  SlackAutoStampBotStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/SlackAutoStampBotStack/3be0c370-b2fc-11ea-968b-06ef10f7e39e
```

できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304815/4d8f4801-e929-1094-8360-50d9064bc1a6.png)

適当に動作確認。問題なさそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304815/e251f699-edec-fe34-9537-0edbf5c3ae15.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304815/f4a41a06-3209-e6d4-2ca5-62055ee7f681.png)

## ローカルでテストしたい

動作確認しながら作っていくとなると毎回デプロイは大変なので、ローカルでLambdaを動かしたいです。
公式ドキュメントにSAMでローカル実行する方法が記載されていたので、こちらを参考にやってみます。
SAMとDockerが必要になりますが、こちらはインストール済みとします。自分はVagrantで立てたUbuntuにDockerを入れているので、SAMはそちらに入れました。

* [AWS CDK tools](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/tools.html#sam)

まず、SAMで使用するためのCloudFormationテンプレートを生成します。

```bash
cdk synth --no-staging > template.yaml
```

生成したテンプレートから対象のLambdaの論理IDを探します。 `Type` が `AWS::Lambda::Function` のリソースですね。
Dockerが別ホストで実行されている場合など、Lambda本体のパスが変わってしまう場合は `aws:asset:path` を必要に応じて修正します。

```yaml:template.yaml
  stampBotFunction1C7A789E: # <- これを確認
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:

# (略)

    Metadata:
      aws:cdk:path: SlackAutoStampBotStack/stampBotFunction/Resource
      aws:asset:path: /home/vagrant/docker/slack-auto-stamp-bot/lambda/stamp-bot-function # <- 必要に応じてパスを修正
      aws:asset:property: Code

```

ここまでできたら、あとは適当にLambdaに渡すeventでも作成して…

```json:event_sample/oshi.json
{
  "kotori": "Aya Uchida",
  "you": "Shuka Saito",
  "shizuku": "Kaori Maeda"
}
```

`sam local invoke` で実行！

```
sam local invoke stampBotFunction1C7A789E --event event_sample/oshi.json
Invoking app.handler (ruby2.7)

Fetching lambci/lambda:ruby2.7 Docker container image......
Mounting /home/vagrant/docker/slack-auto-stamp-bot/lambda/stamp-bot-function as /var/task:ro,delegated inside runtime container
START RequestId: a974a2ee-c7dd-1ec1-9ff2-c418511d44bf Version: $LATEST
END RequestId: a974a2ee-c7dd-1ec1-9ff2-c418511d44bf
REPORT RequestId: a974a2ee-c7dd-1ec1-9ff2-c418511d44bf	Init Duration: 372.29 ms	Duration: 3.59 ms	Billed Duration: 100 ms	Memory Size: 128 MB	Max Memory Used: 22 MB

{"statusCode":200,"headers":{"Content-Type":"application/json"},"body":"{\"kotori\":\"Aya Uchida\",\"you\":\"Shuka Saito\",\"shizuku\":\"Kaori Maeda\"}"}
```

実行できました！

# 所感

使用する言語の知識は必要になりますが、CloudFormationのyamlよりも内容把握しやすかったりするので、個人的にはCDKとても良きです。
ローカルでの動作確認の手段もちゃんと検証できたので、いろいろ作って遊んでみたいです。