AWS Lambda アプリケーションのCI/CD環境をやってみた

## はじめに
AWS Lambdaを使用したアプリケーショのCI/CDのパターンとして、CodePipelineとCodeBuildを組み合わせて、テストからデプロイまでの自動実行環境を設定してみました。
複雑なアプリケーションでの利用を想定し、AWS Lambdaのレイヤー関数に対応しました。

## 今回の構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/33642d5c-4ca2-4b7b-8563-ac6a6d2e567f.png)

* GitHubのソースコードが更新されたらS3バケットへ反映するCodePipelineを構築
* S3バケットをソースとして、CodeBuildでテストとデプロイを実行

## ソースフォルダ構成
```
myproject
├── ConsoleLoginNotificationLambda
│   └── index.py
├── pytest.ini
├── python   # ← LayerとなるLambda関数は、pythonフォルダに格納する
│   └── layerFunction
│       └── common.py
├── template.yml
└── tests
    ├── ConsoleLoginNotificationLambda
    │   ├── test_1.py
    │   ├── test_2.py
    │   ├── test_2_data.json
    │   └── test_3.py
    └── common
```
以前の記事で作成したコードを上記の構造でGitに格納します。

https://qiita.com/mhuk_mtd/items/f8c57497b31551695a54

今回Lambdaレイヤーを使用した構成とするため、index.pyの一部をcommon.pyに移行します。

```python:common.py
import os
import boto3

def Store_Data(inputData):
    省略    

def Publish_sns(subject, message):
    省略
```

```python:index.py
# coding: utf-8
import json
import base64
import gzip
import datetime
from dateutil import tz
from layerFunction.common import Store_Data # ←common.pyにコードを移動して、インポート
from layerFunction.common import Publish_sns # ←common.pyにコードを移動して、インポート

def lambda_handler(event, context):
    省略
```

プロジェクト直下にpytest.iniを以下の内容で作成し、pytest実行時に必要なモジュールへのパスを通します。
```text:pytest.ini
[pytest]
pythonpath = ./python
             ./ConsoleLoginNotificationLambda
```

構成変更に伴い一部ファイル参照パスを修正します。
```python:test_2.pyとtest_3.py
        省略
        #                       ↓　テストデータの読み込みパスを修正
        with open('tests/ConsoleLoginNotificationLambda/test_2_data.json', 'r', encoding='utf-8') as fp:
        省略
```

samテンプレートファイルtemplate.ymlを作成し、プロジェクト直下に追加します。
```yml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM template for Console Login Notification Lambda

Resources:
  # Lambda Layer
  CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: common-layer
      Description: Layer containing common functions
      ContentUri: python/
      CompatibleRuntimes:
        - python3.12
      RetentionPolicy: Retain

  # Lambda Function
  ConsoleLoginNotification:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ConsoleLoginNotificationLambda/
      Handler: index.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Timeout: 30
      MemorySize: 128
      Layers:
        - !Ref CommonLayer
      Environment:
        Variables:
          LOG_LEVEL: INFO
      Policies:
        - CloudWatchLogsFullAccess
        # Add other required IAM policies here based on your function's needs

Outputs:
  ConsoleLoginNotificationFunction:
    Description: Console Login Notification Lambda Function ARN
    Value: !GetAtt ConsoleLoginNotification.Arn
  ConsoleLoginNotificationFunctionIamRole:
    Description: IAM Role created for Console Login Notification function
    Value: !GetAtt ConsoleLoginNotificationRole.Arn
```

ローカル環境でpytestを実行し、パスが通っていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/4be58a93-744d-4cba-a68e-657a6926fb84.png)

## GitHubとCodePipelineを接続
ソースコードをGitHubに格納します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/a0221440-fd2e-47d3-91ff-f45c9e1cbb47.png)

Pipelineを設定します。
トリガー、ソース、ビルドを設定し、テストとデプロイステージはスキップします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/6485887b-12fe-41b3-9b4c-e3455d2b4b04.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/22337a9c-b008-4112-812a-e0d5bffc3339.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/22524b22-da02-48dc-95d7-5e20b4cf306a.png)


今回mainブランチの更新がかかったらトリガーさせるために下図のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/6a1ae064-381d-43bc-b4a4-c6ef077dbfc3.png)

S3へ同期するためのCodeBuildプロジェクトを作成します。（設定内容は後述）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/a77fbf60-fd9e-4d5f-b807-5d1b4b4e9fe5.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/95ab93b7-5551-4451-9bf6-0f51e3937fe2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/af490e30-2ca2-4ce2-9b69-a0039752f73c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/c70419e6-f01d-4aff-bbac-c30c3b7719a5.png)

### CodeBuild
GitHubリポジトリのソースコードをS3へ同期するための設定です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/40b5d9cc-9fd6-4f88-b1f2-841a5486bc8c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/fe8d381e-cf01-4e31-b582-124f4b4b8fea.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/5a75e4db-5624-4493-a5d5-9029df96c248.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/d54f73b4-96db-42dc-bac8-3701b9afa5a5.png)

```text:buildコマンド
version: 0.2

phases:
  build:
    commands:
       - aws s3 sync . "s3://sam-deployment-bucket-123456789012/myproject" --delete
       - echo "git source synchronized to s3."
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/b3e9d994-1c71-49b5-a3df-9920e806e07e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/f3ff365d-fe3e-4533-ac94-556f6e4cd8f4.png)

サービスロールは新規作成（下図は作成後の編集画面）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/e338e83c-441b-482a-90b2-187f5d0ac68f.png)

**サービスロールには、ソースを格納するS3バケットへの権限を後で追加してください。**
ここまでの設定で、ソースコードをmainブランチへプッシュすると、GitHubリポジトリとS3が同期されます。

### S3同期の確認
テスト用のファイルを作成しコミットします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/1e312511-25b2-43fe-aabb-e0c62c5dd7bf.png)

GitHubでプルリクエストを行い、mainブランチへ追加したテストファイルをプッシュします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/49f64607-23c3-4163-acd4-cc104e3ae390.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/a896a0e1-53a2-4d9f-b104-e0d44cf9144d.png)

ソース変更がトリガーとなり、CodeBuildプロジェクトが開始されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/0b39fedb-f859-464d-8dc3-57ace05cd527.png)

ビルドログより、コンテナ上でS3同期コマンドが実行されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/9ce65e21-8e3c-4fb7-a1d1-70676b75d3cc.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/71a0bd8d-5eff-4c09-9314-dcf57ab5be9b.png)

追加したテストファイルが、S3に反映されました。Pipelineは正しく構成されたようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/13c7dc2e-9da0-4588-9df1-f55a2dc5a8d1.png)

## 自動テストとデプロイ
次に、S3に同期したソースコードからLambda関数をデプロイするための、もう１つのCodeBuildプロジェクトを作成します。Pipelineに組み込み、一連のタスクとして実行できると思いますが、デプロイの直前に人間による確認と操作を要求するようにしたいため、敢えて独立したプロジェクトにします。

### 2つめのCodeBuild（1つめとの違いのみ記載）
S3バケットをソースに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/7fd9f294-b216-42cd-b52f-03e615b8e7f0.png)

環境変数を設定します。（ビルドパッケージを格納するバケットの指定で使用）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/af5edf94-2777-41da-b189-d667cab41f28.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/7b000a35-f45d-4b69-b606-b60509ccfdc7.png)

```text:buildコマンド
version: 0.2

phases:
  install:
    commands:
      - echo "Installing dependencies..."
      - pip install --upgrade pip
      - pip install pytest
      - pip install moto[all]
      - pip install pytest-freezer
      # 必要に応じて、その他の依存関係をインストール
  pre_build:
    commands:
      - echo "Running pre-build tasks..."
      # 必要に応じて、ビルド前のタスクを実行（例：環境変数の設定など）
  build:
    commands:
      - echo "Running tests..."
      - python -m pytest --junitxml=test-results/junit.xml # テスト実行とレポート作成
      - echo "Running sam deploy..."
      - sam deploy
        --template-file template.yml
        --stack-name myproject-app
        --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
        --s3-bucket ${PACAGE_BUCKET_NAME}
        --s3-prefix package
  post_build:
    commands:
      - echo "Post-build tasks..."
      # 必要に応じて、ビルド後のタスクを実行（例：成果物のアップロードなど）
reports:
  pytest_reports:
    files:
      - '**/*'
    base-directory: ./test-results
    file-format: JUNITXML
```

**サービスロールには、CloudFormation、Lambda、IAMを操作するための権限を後で追加してください。**


### テストがエラーとなる場合の動作
CodeBuildプロジェクトを手動で実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/6a9da391-5f3c-46d9-99cb-1a099ff35ee0.png)

ビルドログに、テストFAILが表示されて、後続のデプロイが実行されていないことが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/5a5da286-0220-45a0-bbc4-ee83009648a8.png)

左ペイン「レポート履歴」から、テストのサマリーを確認できます。
※buildコマンド中で、単体テスト結果をレポート出力するようオプション指定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/c9d28943-ffa9-4e8b-ae64-05e3237d1c14.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/f881b699-962f-4d2b-822a-50b103ee3d31.png)

### テストがOKとなる場合の動作
単体テストがOKだった場合、ビルドログにLambda関数のデプロイ処理が出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/826178bf-8700-42b3-b080-d5200bbdaf5d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/7437b9e1-2255-43e2-9f5f-119b5bbc4866.png)

CloudFormationを確認すると、samアプリケーション用のスタックが作成され、Lambda関数、レイヤー関数、IAM Roleリソースが作成されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/2e1fbca5-4ce1-4d67-9000-2c15d8f23e96.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/f14fe941-aa96-4245-8558-0e5f0d61cfde.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/21d4769b-8154-4ba4-9231-8f6ed063db56.png)

テストとデプロイを行うCodeBuildプロジェクトが無事設定できました。

## 応用
今回は単一のアカウントでのPipeline化でしたが、マルチアカウント環境でも構成可能です。
* ブランチ毎に対応するアカウントへデプロイする構成
* CodeBuildeがS3へアクセスするためのクロスアカウントでの権限設定が必要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/7926c578-978c-456b-8f42-ff528182dae7.png)

## 最後に
今回やっと自分の理解が追いつき、AWS LambdaのCI/CD環境をCodePipelineとCodeBuildを用いたパターンで構成できました。私自身、今回の対応を実務に応用することでソースコードが管理しやすくなり、テストとデプロイの自動化によって作業時間の短縮とミスの軽減に繋がると期待しています。Lambdaレイヤーにも対応しましたので、ある程度複雑なアプリケーションの構成にも適用できると思われます。本記事が皆様のお役に立てれば幸いです。