【AWS】Gitマージ -> Alemicマイグレーション&ZIP化してS3にアップ&LambdaにそのZIPを適用するCI/CDの構築手順

# 概要
AWS Lambdaで動かしているPythonアプリがあり、そのCI/CDについての記事になります。
本記事では、「Gitでマージしたら、自動でソースコードのZIPがS3にアップされ、また、そのZIPがLambdaにアップロードされる」という流れを実装しました。

また、S3アップ前にはAlembicでDBマイグレーションを行うようにしました。
（意外とこの部分の記事が少なかったので参考になればと思います）

### CI/CDの流れ

- Gitでマージ -> CodePipeline自動起動 -> Codebuild実行。`buildspec.yaml`にて以下を自動実行
    - AlembicでRDS DBへマイグレーション
    - ソースコードをzip化してS3にアップ
    - S3にアップしたzipをLambdaへアップロード

# 手順
## ①Codebuild作成
- Codebuild用のセキュリティグループを作成
    - 作成後、RDS用のセキュリティグループのインバウンドルールに追加する
- Codebuildプロジェクトを作成
    - RDSへアクセスするため、VPCに置く必要がある。RDSがあるVPCとサブネットを選択。セキュリティグループは上記で作成したものを選択する
    - IAMロールには、s3やcloudwatchlogsへの許可、また`"lambda:UpdateFunctionCode"
`のアクセス許可を付与すること（参考までに私が使ったIAMロールは以下に後述）。
    - 環境変数には、DB情報や必要なENVパラメータを登録する。また、今回は後述の`buildspec.yaml`で使うS3バケット名、ZIP名、Lambda関数名も登録しておくこと
    - 作成時、自動でVPCアクセスを許可するIAMポリシーが実行ロールに付与されます

※RDSへのアクセス許可が設定されていない場合、ビルド時に以下のタイムアウトエラーになる

> lib/python3.12/socket.py", line 853, in create_connection
raise exceptions0
File "/***/.pyenv/versions/3.12.4/lib/python3.12/socket.py", line 838, in create_connection
sock.connect(sa)
TimeoutError: timed out

(参考)Codebuildの実行ロール

```json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation",
                "codebuild:CreateReportGroup",
                "codebuild:CreateReport",
                "codebuild:UpdateReport",
                "codebuild:BatchPutTestCases",
                "codebuild:BatchPutCodeCoverages",
                "codebuild:StartBuild",
                "codebuild:StopBuild",
                "codebuild:RetryBuild",
                "lambda:UpdateFunctionCode"  ## これ大事！
            ],
            "Resource": "*"
        }
    ]
}

```


## ②CodePipeline作成
- CodePipelineを作成
    - SourceステージはGithub version2にして、該当リポジトリの指定ブランチを選択
    - Buildステージは上記のCodebuildプロジェクトを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/72ff739f-91a9-018a-41d5-8b33ed63af69.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/f69490d0-064e-fb6f-d2da-7187ad96acc1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/c742ce26-f79f-90e6-d8b2-5c53dc06365f.png)


## ③`buildspec.yaml`作成

まず、ビルド時に必要なライブラリは`requirements_for_build.txt`に記載し、ルートにおいておく。

```requirements_for_build.txt
# 以下は必要に応じて変更してください
awscliv2==2.3.1
alembic==1.13.2
pymysql==1.1.1
SQLAlchemy==2.0.32
```

以下の内容で`buildspec.yaml`を作成してこちらもルートに置く。
（環境変数用の`.env`もルートに）

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.12
    commands:
      - yum update -y
      - pip install -r requirements_for_build.txt

  pre_build:
    commands:
      - echo Pre-build phase
      - echo DB_HOST=$DB_HOST >> .env
      - echo DB_PORT=3306 >> .env
      - echo DB_USER=$DB_USER >> .env
      - echo DB_PASSWORD=$DB_PASSWORD >> .env
      - echo DB_NAME=$DB_NAME >> .env
      - echo $S3_BUCKET_NAME
      - echo $ZIP_FILE_NAME
      - echo $LAMBDA_FUNCTION_NAME

  build:
    commands:
      - echo Build phase
      - cd app/database ## alembicディレクトリがapp/databaseにある場合
      - alembic upgrade head
      - cd ../../
      - export TIMESTAMP=$(date +%Y%m%d%H%M%S)
      - export UNIQUE_ZIP_FILE_NAME="${ZIP_FILE_NAME%.zip}_$TIMESTAMP.zip"
      - echo $UNIQUE_ZIP_FILE_NAME
      - zip -r $UNIQUE_ZIP_FILE_NAME .
      - aws s3 cp $UNIQUE_ZIP_FILE_NAME s3://$S3_BUCKET_NAME/

  post_build:
    commands:
      - echo Post-build phase
      - aws lambda update-function-code --function-name $LAMBDA_FUNCTION_NAME --s3-bucket $S3_BUCKET_NAME --s3-key $UNIQUE_ZIP_FILE_NAME

artifacts:
  files:
    - $UNIQUE_ZIP_FILE_NAME
```

`pre_build`フェーズの環境変数は、Codebuildプロジェクトから取得しています。あらかじめ登録しておいてください。

`zip`コマンドでルート以下全てをZIP化し、
`aws s3 cp`でS3にアップ、
`aws lambda update-function-code`でLambdaにアップロード、
という手順です。

また、上記でタイムスタンプを利用しているのは、S3に保存されるZIP名称を一意にして、どの時点のZIPかを明示的にわかりやすくするためです。こうすることで、以下のようにZIP名に日時が含まれるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/4d8a1cc6-6374-3dca-920a-032f82ff7945.png)


以上で、正常にマイグレーションとLambda関数の更新ができたことを確認できました。
参考になれば幸いです。