CloudFormationを使わないLambdaのCICDについて

## はじめに
この記事は[ディップ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dip-dev)の10日目の投稿です。

この記事では、あるシステムで導入したLambdaのCICDについて紹介をしたいと思います。

## LambdaのCICDについて
LambdaのCICDは現在(2023/12時点)でCloudFormationを使用したものがメジャーかと思いますが、  
今回はそちらを使わず、以下の内容で構成しました。  
CloudFormationを使用しなかった背景などは後ほど説明させていただきます。

### 使用したもの
- GithubEnterprise(GHE)
- AWS CodePipeline
- AWS CodeBuild
- AWS CodeDeploy
- AWS S3
- AWS Lambda
- AWS SQS(テスト実行用)

![lambda_ci_cd_v1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629374/7ee75939-cccb-fa9c-74e7-ce070e862db4.png)

## こんなことができます
### GitHubEnterpriseの特定ブランチが更新された際に自動デプロイ開始
CodePipelineを骨組みとして使用しているので、CodePipelineの機能でGHEと連携させることで特定ブランチの更新をトリガーに自動デプロイを開始することができます。

### テストコードを実行してデプロイ前に自動テスト
CodePipelineにテストコード実行用のCodeBuildを組み込むことでデプロイ工程の中でテストコードを実行し、単体テストを自動化できます。

### リリース前にLambdaの環境でテスト実行
本番環境で実際に処理を実行する環境としてこちらの構成ではLambdaのエイリアスを採用しています。  
CodeDeployでエイリアスが参照するバージョンを切り替える仕組みとしているため、  
ビルド資産がLambdaに適用された後もエイリアスのバージョンが切り替わるまで本番環境は最新資産にならず、Lambda本体の実行条件を満たすことでLambdaの環境でリリース前に最新資産を実行をすることができます。

### もしもの事態でリリース後にリリース前の環境に戻さないといけなくなった場合、バージョン変更のみで対応が可能
リリース後に何らかの問題がみられた場合などでリリース前資産に本番環境を戻さないといけなくなった場合、ほとんどの場合エイリアスのバージョンを切り替えることでリリース前の環境に戻すことができます。

### 全ての構成をTerraform単体で管理可能
デプロイにCodeBuildを採用しているため、CloudFormationを使用しておらず、全ての構成をTerraformで一括管理することができます。  
CodeBuildを使用する場合はCodeBuildのコードを作成する必要があることがネックですが、今回作成したコードはシンプルなので他のプロジェクトでも流用しやすい構成になっていると思います。  
今回CloudFormationを採用しなかった要因なのですが、全ての構成をTerraformで管理したいという要望があり、  
CloudFormationを使用するとLambdaの構成をCloudFormationで管理することになるため、見送った次第です。

## 構成の詳細
上記で紹介した構成の詳細について一つずつ説明させていただきます。

### GithubEnterprise
私のチームではGithubEnterpriseを資産管理に使用しており、今回の構成でもGithubEnterpriseを採用しています。

### AWS CodePipeline
デプロイ構成の骨組みとしてCodePipelineを採用しました。  
GithubEnterpriseと相性がよく、実行順の管理や並列実行などが容易に構成でき便利です。  
また、CodeDeployのフェーズの前に承認ブロックを入れておくことで事前に本番Lambda環境で動作確認をしてからリリースすることができます。  
(現時点でLambda版CodeDeployにECS版のようなタスクセット切り替えボタン機能などがないためCodePipelineの機能で補っています)

### AWS CodeBuild
以下の用途で採用しており、それぞれ説明します。

#### テスト
テストコードを実行します。

#### ビルド
資産をビルドしてS3にビルド資産をアップロードします。
この処理はテストと並列で実行しても問題ないため、テストともに並列で開始します。

#### デプロイ
S3にアップロードされている資産をLambdaへデプロイします。  
こちらはプロジェクト問わずシンプルな構成なのでbuildspecを公開します。

| 変数名 | 用途 |
| --- | --- |
| FUNCTION_NAME | デプロイ先のLambda関数名 |
| S3_BUCKET | ビルド資産格納先のS3のバケット名 |
| S3_KEY | ビルド資産格納先のS3のキー |

```yaml
version: 0.2
phases:
  post_build:
    commands:
      - echo ----------------------------
      - echo deploy start
      - echo ----------------------------
      - # デプロイ
      - aws lambda update-function-code --function-name ${FUNCTION_NAME} --s3-bucket ${S3_BUCKET} --s3-key ${S3_KEY}
      - # デプロイ処理完了に非同期で時間がかかるため待機
      - sleep 60
      - # バージョン作成
      - aws lambda publish-version --function-name ${FUNCTION_NAME}
```

#### CodeDeploy用appspec作成
CodeDeployを採用しているためそのappspec作成をします。
Lambda自身の最新バージョンを置き換え先のバージョンとして、
エイリアスで使用しているバージョンを置き換え元のバージョンとして取得し、設定しています。

| 変数名 | 用途 |
| --- | --- |
| FUNCTION_NAME | デプロイ先のLambda関数名 |
| ALIAS_NAME | Lambdaのエイリアス名 |
| CURRENT_VERSION | Lambdaの現行バージョン、実行ごとに自動で取得しています |
| NEW_VERSION | 新しく発行したバージョン、実行ごとに自動で取得しています |

##### buildspec.yaml
```yaml
version: 0.2
phases:
  post_build:
    commands:
      - echo ----------------------------
      - echo create artifacts start
      - echo ----------------------------
      - CURRENT_VERSION=$(echo $(aws lambda list-aliases --function-name ${FUNCTION_NAME} | grep FunctionVersion | tail -1 | tr -cd "[0-9]"))
      - NEW_VERSION=$(echo $(aws lambda list-versions-by-function --function-name ${FUNCTION_NAME} | grep Version | tail -1 | tr -cd "[0-9]"))
      - sh sed_from_env.sh //ここでappspecの変数置き換え処理をしています。
artifacts:
  base-directory: $CODEBUILD_SRC_DIR
  files:
    - appspec.json
```

##### appspec.template.json
CodeDeployに渡すappspec.jsonのテンプレートです。  
環境変数置き換えバッチによって`${環境変数名}`となっている部分を環境変数値に置き換えて使用しています。

```json
{
    "version": 0.0,
    "Resources": [{
        "KBBaitoAPIEntrySendFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Name": "${FUNCTION_NAME}",
                "Alias": "${ALIAS_NAME}",
                "CurrentVersion": "${CURRENT_VERSION}",
                "TargetVersion": "${NEW_VERSION}"
            }
        }
    }]
}
```

##### 環境変数置き換えバッチ
ファイルに記載された`${環境変数名}`をその環境変数値に置き換えるバッチです。  
buildspec.ymlの通り、CodeBuild環境で呼び出しているため、CodeBuildの環境変数で置き換えられます。

<details><summary>バッチ内容</summary><div>

```sh
#!/bin/bash
set -e

TARGET="appspec"
TEMPLATE_FILE="${TARGET}.template.json"
TMP_FILE="${TARGET}.tmp"
OUTPUT_FILE="${TARGET}.json"
DIFF="diff -y --suppress-common-lines"

# すでにファイルが有るのであればそれをそのまま使用する
if [ ! -f ${OUTPUT_FILE} ]; then
  cp -p ${TEMPLATE_FILE} ${OUTPUT_FILE}
fi 

for row in `env`
do
  key=`echo $row | cut -d \= -f 1`
  val=`echo $row | cut -d \= -f 2`

  cat ${OUTPUT_FILE} | sed -e "s|\${${key}}|${val}|g" > ${TMP_FILE}
  mv ${TMP_FILE} ${OUTPUT_FILE}
done

set +e
```

</div></details>

### AWS CodeDeploy
CodeBuildによって作成されたappspec.ymlに沿って、Lambdaエイリアスの参照バージョンを切り替えます。

### AWS S3
CodeBuildでビルドしたデプロイ資産を一時格納します。
S3経由のアップロードでなければLambdaの仕様で容量制限があるため、S3を経由してアップロードしています。

### AWS Lambda
業務処理を行っている今回のデプロイ対象です。
Lambda本体とエイリアスで構成しており、業務処理はエイリアスで実行しています。
CodeDeploy実行前にLambda本体を実行することでアップロードした最新資産を業務処理に影響させずにテスト実行することも可能です。

### AWS SQS(テスト実行用)
今回の導入したシステムではSQSをトリガーにLambdaを動かす仕組みを採用しているため、テスト実行用のSQSを追加しています。
こちらについてはLambdaのトリガーになっているもので置き換えて良いですし、Lambdaコンソールのテスト実行でも良いと思います。

## さいごに
今回CloudFormationを採用せずにLambdaのCICDを構築しましたが、
インフラの設定変更と資産リリースを切り離して行えたりで色々と運用してみて便利な面もありましたのでおすすめです。
個人的には、とはいえデプロイ部分はAWSサービスで汲み取って欲しいなという思いもあり、CodeDeployにLambdaの資産デプロイ機能も追加されないかなと思っています。