【AWS】ワンボタンで Lambda & API Gateway をディプロイする環境づくり（AWS-SAM + CloudFormation + CodeBuild）


# はじめに

Lambda関数をはじめとするAWSリソースの設置/更新はAWSコンソールから直接操作もできる。
とはいえ、事故リスクを減らすためにもできる限り自動化しておいたほうが安心安全。
ワンボタンでgithubからコード取得 〜 CloudFormationでLambdaと
API Gatewayを自動更新する環境をつくる手順をまとめる。

サンプルコード↓

https://github.com/uni-ezobafun/ocean_lambda_sam_deploy_ts

### AWS-SAM + CloudFormation + CodeBuildを使うメリット
* AWSコンソールから各種リソースの設定を意図せずに変更するリスクを潰せる
* AWSリソースに関する設定変更の履歴を全てgitで管理できる
* Jenkinsなど別途環境を用意しなくても、AWSコンソール内でディプロイ作業を完結できる
* 更新作業を簡易化 & 効率化して、ディプロイ頻度を上げられる
* CloudFormation自体は無料(設置したリソースは課金される)、CodeBuildは100分/月までジョブ実行無料

### やること
* AWS-SAMのテンプレートを用意して、CloudFormationで自動ディプロイする
* CodeBuildでディプロイジョブを用意して、ワンボタンでCloudFormationを呼べるようにする
* 必要なIAMの権限を整理する
* 設置したLambdaが動くことを確認する

### 前提
* ちゃんと動くLambda関数のコードが用意されていること

本ページではTypescriptでnodejsランタイムを利用するが、
Lambda関数の処理内容やTypescriptのトランスパイル手順については触れない。

また、ここで紹介する手順はPythonをはじめとする他のランタイムにも応用できる。

本ページ上部のサンプルコードも参考に。


# もくじ
1. ディプロイジョブの成果物を保管するS3 bucketを作成する
1. Lambdaに紐付けるIAMロールを作成する
1. AWS-SAMのテンプレートファイルを作成する
1. CloudFormationを動かすshellスクリプトを作成する
1. CodeBuildのビルドジョブを作成する
1. IAMの権限を整理する
1. ビルドジョブを実行して、CloudFormationを見守る
1. 生成されたLambdaを実行してみる



# 1. ディプロイジョブの成果物を保管するS3 bucketを作成する

```
1. uni-cloudformation-artifact-dev
2. uni-codebuild-artifact
```

本ページでは上記の名称でS3 bucketを2つ新規作成する。
それぞれ名称の通り、CloudFormationとCodeBuildが走った際に生成される成果物を保存するのに使う。

S3 bucketを作成する手順（1-1を参照）↓

https://qiita.com/uni_ezobafun/items/81978d1487790b46b8c4



# 2. Lambdaに紐付けるIAMロールを作成する

新規設置するLambdaに紐付けるIAMロールを作成する。
（IAMロールの自動生成もたぶんできるが、個人的には事故を起こしそうで怖いため権限周りは手動でやる。）

IAMのコンソールを開いたら、`Create role`をクリック。
![Screen Shot 2021-06-06 at 19.17.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/e7f89082-2373-6c4f-a5e1-84ac29647020.png)

Lambdaのテンプレートを選択して次へ。
![Screen Shot 2021-06-06 at 19.18.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/118faf0b-826e-5f88-b26f-15e60f22aa0a.png)

Lambda関数の処理内容に応じて必要な権限を追加して次へ。
（本ページで設置するLambda関数はpingを返すだけなので、
なにも権限を付与しない空のIAMロールを作成する。）
![Screen Shot 2021-06-06 at 19.18.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/b6d3cd89-d664-b862-5967-c2f3c5b92082.png)

作成するIAMロールの名称を設定して、完了。
ここでは`lambda-sam-deploy-ts-role `とした。
![Screen Shot 2021-06-06 at 19.46.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/4709e926-b655-41d4-2959-a8645d00da81.png)

IAMロール作成完了したら、arnを控えておく（手順3で使う）。
![Screen Shot 2021-06-06 at 19.55.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/303fb443-743c-84f8-11b2-7f53e28925ea.png)



# 3. AWS-SAMのテンプレートファイルを作成する

そもそもAWS-SAMとは↓

https://aws.amazon.com/jp/serverless/sam/faqs/

下記の通り、CloudFormationに読み込ませるテンプレートファイル（template.yaml）を用意する。
注意すべきは`Role, CodeUri, Handler, Runtime`の4箇所。
`Role`には手順2で作成したIAMロールのarnを書く。
`CodeUri`と`Handler`にはLambdaに配置する関数コードのファイルパスを書くが、
template.yamlを起点とする相対パスで書く点に注意。

下記のtemplate.yamlの場合、各ファイルの位置関係の例は下記の通り。
index.jsではhandler関数が定義されており、これがLambda関数として設置される想定。

``` template.yamlとindex.jsのファイルパス
ocean_lambda_sam_deploy_ts/
├ aws/ - sam/ - dev/ - template.yaml
└ built/ - dev/ - ping/ - index.js
```

``` template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 2
  Api:
    OpenApiVersion: 3.0.2
Resources:
  UniLambdaBasicApi:  # API Gatewayの設定
    Type: AWS::Serverless::Api
    Properties:
      Name: ApigwSamDeployTypescript # 設置するリソース名（任意）
      StageName: dev
      GatewayResponses:
        DEFAULT_4xx:
          ResponseParameters:
            Headers:
              Access-Control-Expose-Headers: "'WWW-Authenticate'"
              Access-Control-Allow-Origin: "'*'"
  UniPingFunc: # Lambdaの設定
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: PingLambdaSamDeployTypescript # 設置するリソース名（任意）
      Runtime: nodejs14.x # 利用するランタイム
      Role: arn:aws:iam::${12桁のAWS-ID}:role/lambda-sam-deploy-ts-role # 手順2で作ったIAMロール
      CodeUri: ../../../built/dev/ping # Lambda関数が置いてあるファイルパス
      Handler: index.handler # Lambda関数の ${ファイル名.メソッド名}
      MemorySize: 128
      Events:
        Vote:
          Type: Api
          Properties:
            Path: /ping # Lambdaを実行するパス
            Method: get
            RestApiId: !Ref UniLambdaBasicApi # 紐付けるAPI Gateway
```



# 4. CloudFormationを動かすshellスクリプトを作成する

そもそもCloudFormationとは↓

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html

shellスクリプトでaws-cliからCloudFormationを呼んで、手順3で用意したtemplate.yamlを渡す。
このshellスクリプト（`lambda_sam_deploy_ts.sh`）を下記のファイルパスに設置する。

``` lambda_sam_deploy_ts.shのファイルパス
ocean_lambda_sam_deploy_ts/
├ aws/ - sam/ - dev/ - template.yaml
|  └   - code_build/ - lambda_sam_deploy_ts.sh
|
└ built/ - dev/ - ping/ - index.js
```

``` lambda_sam_deploy_ts.sh
#!/bin/sh

if [ $# < 1 ]; then
  echo "[ERROR!] one or more arguments are required"
  exit -1
fi

STAGE=$1 # このサンプルではdev/prdを第一引数として受け取って代入する

npm i
npm run build-${STAGE}

aws cloudformation package \
  --template-file aws/sam/${STAGE}/template.yaml \
  --s3-bucket uni-cloudformation-artifact-${STAGE} \ # 手順1で作成したS3 bucket
  --s3-prefix sam/lambda_sam_deploy_ts/${STAGE}/package \ # 任意のファイルパス
  --output-template-file packaged.yaml

aws cloudformation deploy \
  --template-file packaged.yaml \
  --stack-name lambda-sam-deploy-ts # 任意のスタック名
```



# 5. CodeBuildのビルドジョブを作成する

### 5-1. buildspec.yamlを作成する

CodeBuildのジョブで処理される内容は`buildspec.yaml`で定義される。
このyamlファイルをgitにpushしておいて、後ほどジョブ新規作成時に参照パスを指定すると、
定義された処理内容を順に実行してくれる。

本ページの例では、手順4で作成した`lambda_sam_deploy_ts.sh`を実行するような
`buildspec.yaml`を作成し、下記のファイルパスに設置した。
（ややこしくなってしまったので、ページ上部のサンプルコードも参考に）

``` buildspec.yamlのファイルパス
ocean_lambda_sam_deploy_ts/
├ aws/ - sam/ - dev/ - template.yaml
|  └   - code_build/ - lambda_sam_deploy_ts.sh
|             └      - dev/ - buildspec.yaml
└ built/ - dev/ - ping/ - index.js
```

``` buildspec.yaml
version: 0.2

phases:
  pre_build:
    commands:
      - echo "start->lambda_sam_deploy_ts"
  build:
    commands:
      - sh aws/code_build/lambda_sam_deploy_ts.sh dev # 手順4で作成したshellを実行（引数devを渡す）
  post_build:
    commands:
      - echo "finish->lambda_sam_deploy_ts"
artifacts:
  files:
    - built/dev/**/* # 手順1で作成したS3 bucketに設置する成果物

```

### 5-2. CodeBuildのコンソールからプロジェクトを新規作成する

CodeBuildのコンソール画面を開いて、`Create build project`をクリック。
![Screen Shot 2021-06-06 at 20.58.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/bdd35af9-aa6a-ecfe-a17a-179f3f77cf99.png)

任意のプロジェクト名を設定する。
![Screen Shot 2021-06-06 at 21.06.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/50a51fdf-1668-ff5a-1b3b-b0f3e7cb2fdc.png)

① ジョブで使うコードの参照元を選択する（本ページではGitHubを選択）。
② 本ページではパブリックリポジトリを選択（プライベートリポジトリでは、アカウント認証が必要）
③ リポジトリのURLを指定
④ 参照するブランチを指定
（参照先にLambda関数のコード、shellスクリプト、`template.yaml`、`buildspec.yaml`があること）
![Screen Shot 2021-06-06 at 21.05.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/d4c8d6cd-c383-5ad6-2114-cd2b4e30911f.png)

① 実行環境を選択（よしなに）
② 新規作成するプロジェクトに紐付けるIAMロールを指定
 （本ページでは`codebuild-sam-deoloy-service-role`という名称でIAMロールを新規作成）
![Screen Shot 2021-06-06 at 21.05.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/8747e000-de08-3778-a4e1-86cfe9c3d978.png)

① 実行するジョブの処理内容を定義する方法を選択
② `buildspec.yaml`のファイルパスを指定
（本ページでは手順5-1で作成した`buildspec.yaml`を選択）
![Screen Shot 2021-06-06 at 21.10.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/17e2c98a-6130-01e8-6f76-261e343b99bd.png)

ビルドの成果物を保存する設定（よしなに）
（本ページでは手順1で作成したS3 bucketにトランスパイル済のjsファイルを保存）
![Screen Shot 2021-06-06 at 21.09.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/75756999-c53a-ebb0-d8c1-0039557dd886.png)

CloudWatchにログを出力する設定（よしなに）
全て設定したら、`Create build project`をクリック
![Screen Shot 2021-06-06 at 21.09.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/c3c193b4-be91-4fdf-aac5-c085b7c3a89d.png)

これでCloudFormationを起動するプロジェクトを作成できた（まだ実行しない）。
![Screen Shot 2021-06-06 at 21.16.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/a8c5ccec-721b-251a-28fb-216081eaa9ae.png)



# 6. IAMの権限を整理する

IAMロールのコンソールを開いて、手順5-2で作成したCodeBuildのIAMロールを選択
![Screen Shot 2021-06-06 at 21.14.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/b3a367ad-2db7-8a19-97da-16164bfcb248.png)

このIAMロールに紐付いたIAMポリシーを選択
![Screen Shot 2021-06-06 at 21.14.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/74f00b3d-ce9e-7f42-e13a-3f4981b4f0c8.png)

`Edit policy`をクリック
![Screen Shot 2021-06-06 at 21.15.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/64e7fb0b-b0bf-c6db-0ad3-76bdc9bc7c3c.png)

画面上部のJSONタブをクリックする。
必要な権限に応じてJSONの内容を書き換えたら、`Review policy`をクリック
（本ページでは下記`LambdaSamCreationPolicyDev.json`の通りIAMポリシーを設定した。）
![Screen Shot 2021-06-06 at 21.33.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/8878b4e0-ac92-1188-0afd-825bd4859559.png)

``` LambdaSamCreationPolicyDev.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Resource": [
        "arn:aws:logs:ap-northeast-1:260006775748:log-group:/aws/codebuild/lambda_sam_deploy_ts",
        "arn:aws:logs:ap-northeast-1:260006775748:log-group:/aws/codebuild/lambda_sam_deploy_ts:*"
      ],
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ]
    },
    {
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::codepipeline-ap-northeast-1-*"],
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:GetBucketAcl",
        "s3:GetBucketLocation"
      ]
    },
    {
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::uni-codebuild-artifact",
        "arn:aws:s3:::uni-codebuild-artifact/*"
      ],
      "Action": ["s3:PutObject"]
    },
    {
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::uni-cloudformation-artifact-dev",
        "arn:aws:s3:::uni-cloudformation-artifact-dev/*"
      ],
      "Action": ["s3:GetObject", "s3:PutObject"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "codebuild:CreateReportGroup",
        "codebuild:CreateReport",
        "codebuild:UpdateReport",
        "codebuild:BatchPutTestCases",
        "codebuild:BatchPutCodeCoverages"
      ],
      "Resource": [
        "arn:aws:codebuild:ap-northeast-1:260006775748:report-group/lambda_sam_deploy_ts-*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": ["iam:PassRole"],
      "Resource": ["arn:aws:iam::260006775748:role/lambda-sam-deploy-ts-role"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:CreateStack",
        "cloudformation:CreateChangeSet",
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeChangeSet",
        "cloudformation:DescribeStackEvents",
        "cloudformation:ExecuteChangeSet",
        "cloudformation:GetTemplateSummary"
      ],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "lambda:CreateFunction",
        "lambda:DeleteFunction",
        "lambda:GetFunction",
        "lambda:AddPermission",
        "lambda:RemovePermission",
        "lambda:UpdateFunctionCode"
      ],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "apigateway:GET",
        "apigateway:POST",
        "apigateway:PATCH",
        "apigateway:DELETE"
      ],
      "Resource": ["*"]
    }
  ]
}
```
<br>
付与した権限の一覧に誤りが無いか確認して、`Save changes`をクリック。
![Screen Shot 2021-06-06 at 21.40.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/7250596e-e0c9-57a1-fc30-ee89869cd116.png)



# 7. ビルドジョブを実行して、CloudFormationを見守る

これでジョブ実行の準備が整った。
CodeBuildのコンソールに戻って、`Start build`をクリック。
![Screen Shot 2021-06-06 at 21.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/d065057e-3a42-c421-fd77-c0a1336d56c4.png)

`buildspec.yaml`の内容に沿って、順番に処理が実行される。
![Screen Shot 2021-06-06 at 21.22.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/cfaa389f-5193-3a1f-73be-a86f6098a151.png)

ジョブ実行中もしくは終了後に、CloudFormationのコンソールを開いて、新規作成したStackを選択。
![Screen Shot 2021-06-06 at 21.43.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/3d9c5561-c382-6f1b-05ff-02cee29bf54d.png)

画面上部のEventタブをクリックすると、CloudFormationくんが頑張っている様子を覗ける。
Statusが全て`CREATE_COMPLETE`になって、CodeBuildのジョブが完了したら、おわり。
![Screen Shot 2021-06-06 at 21.43.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/4d77eabd-d055-3d54-dc01-0d5d974e53a3.png)



# 8. 生成されたLambdaを実行してみる

Lambdaのコンソールを開いて、新規作成されたFunctionをクリック。
![Screen Shot 2021-06-06 at 22.30.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/80dde127-1cce-3b27-6b62-3bf33fa1684f.png)

Lambdaに紐付けられたAPI Gatewayをクリック。
![Screen Shot 2021-06-06 at 22.31.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/d274e7c0-3509-5227-75cd-b10eb6906fec.png)

`Details`タブをクリックすると展開される`API endpoint`のURLをクリック。
![Screen Shot 2021-06-06 at 22.31.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/ba9d4b37-db17-98d2-b140-033fd3e413f4.png)

Lambda関数が実行されて、レスポンスが返ってきた✌😉
![Screen Shot 2021-06-06 at 22.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/d05a10c9-5403-f2e3-1848-ea8c4b4ac5bf.png)



# トラブルシュート

### CloudFormationが途中でコケる

だいたいIAMロールの権限が足りていない/権限の付与対象が間違っている。
手順7に従ってCloudFormationののEventのエラーログを見れば足りていない権限がわかる場合が多い。
あるいはCodeBuildやCloudWatchに出力されたログが役に立つかもしれない。

### CloudFormationのstackが中途半端なStatusのまま残る

リソース作成に失敗したとき、stackのStatusが`UPDATE_ROLLBACK_FAILED`から変わらなくなることがある。
これはリソースの作成に失敗したが、失敗したリソースを消すこともできずに立ち往生している状態を指す。

この状態では、CodeBuildからビルドジョブを再実行してもstackを生成できずにジョブがコケる。

CloudFormationのコンソールから、`Delete`をクリックしてStackを削除すれば良い。
（stackに紐付けられたリソースが全削除されるため注意が必要）
![Screen Shot 2021-06-06 at 21.43.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1050582/d12bde9b-93f8-3c23-49a7-0aeb5542a1d7.png)

参考↓

https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-update-rollback-failed/

### webpack5でトランスパイルしたTypescriptで、index.handlerが認識されない

個人的にハマったところ。
詳細は下記ページで議論されている。

https://github.com/serverless-heaven/serverless-webpack/issues/651

<br>
とりあえず、webpack4を使うか、CloudFormationの代わりにserverlessを使うのが手っ取り早い（?）
(本ページの根底から揺らぐけど)

https://www.npmjs.com/package/serverless