CodeBuildとSAMを組み合わせてLambdaを自動デプロイする

## 背景
複数のLambdaで構成されているサーバレスアプリにて、Lambdaのデプロイをコンソールから行なっていました。
時間がかかるのとオペミスのリスクがあったのでCodeCommitにpushするとAWS SAMで自動デプロイが行われるデプロイパイプラインを構築しました。

[SAMの初歩的なこと](https://qiita.com/lyd-ryotaro/items/6ae7f8813d4769dbf184)はこちら
上記のSAM操作をCodeBuildにて行う、というイメージです。

## 全体図
CodeCommit→CodePipeline→CodeBuildでSAMを起動してデプロイという流れ
CodeBuildが肝になってきます
[![Image from Gyazo](https://i.gyazo.com/438c293928c1f1710819c3a1d6008da1.png)](https://gyazo.com/438c293928c1f1710819c3a1d6008da1)

## CodePipelineとCodeBuildの準備

### CodePipelineの準備
パイプラインを新規作成する
ソースプロバイダーをGitHubやCodeCommitの任意のブランチに設定

:::note info
CodeBuildに変更されたファイルを伝えるため、完全クローンを選択する
:::

[![Image from Gyazo](https://i.gyazo.com/84734770ad3d25d24c0de0f013de220b.png)](https://gyazo.com/84734770ad3d25d24c0de0f013de220b)


### CodeBuildの準備
ビルドステージでビルドプロジェクトを作成する
環境変数に`buildspec.yml`で使用する`S3_BUCKET`を設定する

[![Image from Gyazo](https://i.gyazo.com/cd8a9e99af3b973e8461197ce5eb9699.jpg)](https://gyazo.com/cd8a9e99af3b973e8461197ce5eb9699)


:::note info
CodeBuildロールにCodeCommitやS3やCloudFormation、作成AWSリソースの権限を設定する
ハマりポイント！💡
:::

[![Image from Gyazo](https://i.gyazo.com/78ad56fadd9027d9b84f6fb005a0f391.png)](https://gyazo.com/78ad56fadd9027d9b84f6fb005a0f391)

ビルドステージでSAMのデプロイが完了しているのでCodePipelineのデプロイステージはスキップする


## リポジトリの準備

## ディレクトリ構造
```terminal
$ tree
.
├── README.md
├── SAM_test-sam-a
│   ├── index.mjs
│   ├── samconfig.toml
│   └── template.yaml
├── SAM_test-sam-b
│   ├── index.mjs
│   ├── samconfig.toml
│   └── template.yaml
├── SAM_test-sam-c
│   ├── index.js
│   ├── samconfig.toml
│   └── template.yaml
└── buildspec.yml
```

### buildspec.ymlの準備
リポジトリのルートに`buildspec.yml`を作成する
変更があったLambdaの判断、SAMでのデプロイを担当する

```yaml:buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.11
    commands:
      - echo Installing dependencies...
      - pip install toml
  build:
    commands:
      - echo Build started...
      - |
        set -e # シェルスクリプトがエラーを起こしたら実行を止める
        CHANGED_LAMBDA_DIRS=$(git diff --dirstat=files,0 HEAD^ HEAD | cut -d '%' -f 2 | cut -f 2 -d ' ')
        for DIR in $CHANGED_LAMBDA_DIRS # 変更があったファイルをforで回してSAMデプロイする
        do
          if [[ $DIR = SAM_* ]] # SAMデプロイの対象かを判断
          then
            echo "Building and deploying $DIR..."
            ORIGINAL_DIR=$(pwd)
            cd $DIR
            TARGET_NAME=$(python -c "import toml; print(toml.load(open('samconfig.toml'))['default']['deploy']['parameters']['stack_name'])") # samconfig.tomlからスタック名を取得
            echo "TARGET_NAME $TARGET_NAME..."
            sam build --template template.yaml --build-dir .aws-sam/build # ビルドしてアーティファクトを生成
            echo Build done!
            sam package --template-file .aws-sam/build/template.yaml --s3-bucket $S3_BUCKET --s3-prefix $TARGET_NAME --output-template-file .aws-sam/build/packaged.yaml # 生成されたアーティファクトをS3に保存
            echo Package done!
            sam deploy --template-file .aws-sam/build/packaged.yaml --stack-name $TARGET_NAME --s3-bucket $S3_BUCKET --config-file samconfig.toml --capabilities CAPABILITY_IAM # S3のアーティファクトから実リソースを生成
            echo Deploy done!
            cd $ORIGINAL_DIR
          fi
        done
```

`CHANGED_LAMBDA_DIRS=$(git diff --dirstat=files,0 HEAD^ HEAD | cut -d '%' -f 2 | cut -f 2 -d ' ')`によって、どのLambdaが変更されたかを検出しています。
これを行うためには、CodePipelineのソースステージで完全クローンが必要になります。
変更があったLambdaを対象にしてSAMコマンドを実行します。

### その他ローカルソースコード
#### Lambda関数コード
```javascript:index.js
// テスト用 ただ 'Hello SAM' を出力するコード
export const handler = async (event) => {
  console.log('Hello SAM C!!!!')
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from SAM C!!!!'),
  };
  return response;
};
```

#### SAMテンプレートファイル
```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for SAM_test-sam-c

Resources:
  # Lambda のリソース設定
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: SAM_test-sam-c
      CodeUri: .
      Handler: index.handler
      Runtime: nodejs16.x
      Timeout: 30
      Role: arn:aws:iam::<account-id>:role/service-role/SAM_test-sam-c-role-pcj1qpgb
      Environment:
        Variables:
          ENV_1: 1
          ENV_2: 'ENV2'
          ENV_3: 'ENV3'
  # CloudWach ロググループのリソース設定
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${Function}
      RetentionInDays: 365 # ログの保存期間

# CloudFormationデプロイログ出力用
Outputs:
  Function:
    Description: "SAM_AmazonConnect_MIVR_GetFacilityData Lambda Function ARN"
    Value: !GetAtt Function.Arn
```

#### SAMのコンフィグファイル
```toml:samconfig.toml
version = 0.1

[default.build.parameters]
debug = true
use_container = false # Lambda向けコンテナを使用するか

[default]
[default.deploy]
[default.deploy.parameters]
debug = true
stack_name = "SAM-test-sam-c"
s3_bucket = "" # CodeBuildの環境変数で設定するため不要
s3_prefix = "SAM-test-sam-c"
region = "ap-northeast-1"
confirm_changeset = false # CodeBuildからのデプロイなので変更セットを確認しないでデプロイする
capabilities = "CAPABILITY_NAMED_IAM" # 固定値 CloudFormationにスタック作成のIAM権限を与える
disable_rollback = true # デプロイ失敗時にスタックをロールバックさせる
tags = [
  "env=\"pro\""
]
```

これで対象リポジトリにPushを行うと変更したLambdaが自動でデプロイされるようになりました！🎉🎉🎉

## ハマりポイント

#### CodeBuildの権限設定でめためた失敗した
CodeBuildがアクセスする範囲が広くて何度もリトライしました🥲
ざっと必要だった権限をつけましたが、アクセス範囲が広すぎるのでどうしようか検討中です。

[![Image from Gyazo](https://i.gyazo.com/5e47582dcc500c9b0250e616edf775d8.png)](https://gyazo.com/5e47582dcc500c9b0250e616edf775d8)

## 参考
https://dev.classmethod.jp/articles/update-codepipeline-supports-codecommit-git-clone-as-source-action/

https://dev.classmethod.jp/articles/should-create-cloudwatch-logs-log-group-when-creating-lambda-with-aws-sam/