【AWS】CFn用CI/CD環境の作り方(単一リポジトリに複数ファイルが存在する場合)

## はじめに

CloudFormation をデプロイする際にどのように実行していますか？
私は CloudShell にテンプレファイルを格納してデプロイすることが多いです。

ただ、以下参考記事に記載されているように、
テンプレートをバージョン管理する前提であれば、
リポジトリにあるテンプレートと AWS インフラを同期させるためにパイプラインからの実行が必要になります。

本記事は複数ファイルが格納されているリポジトリである前提の元、
更新した特定ファイルをパイプラインから CloudFormation デプロイするための手順を記載した内容になります。

■ 参考先

https://dev.classmethod.jp/articles/aws-all-iac/#toc-28

## 結論
以下2パターンの方法で、
複数ファイルが混在するリポジトリのファイル更新を検知することが可能になる。
- パターン①：CodePipeline*Codebuildを利用
    - Codebuildを利用してリモートリポジトリの HEAD 情報の差分を取得。
更新有無を検知し特定ファイルのみ CloufFormation デプロイ処理を行うよう設定
- パターン②：CodePipeline統合CloudFormationを利用

それぞれのメリットとしては以下の通りである。
- パターン①
    - Codebuildを利用するため、テストツールの挿入が可能。
また、エラーが発生した場合にもCloudWatchLogsにログが出力できるため調査が容易
- パターン②
    - 設定が容易だが、テストルールなどのカスタマイズができない


## 構成の特徴/構成図

### 構成の特徴
- 以下のように複数ファイルが存在するリポジトリにて、
  特定ファイルが更新された時に更新ファイルのみCfnデプロイされること。

```
Repository(Github上のリモートリポジトリ)
├─index.html
├─style.css
├─README.md
├─buildspec.yml
├─00_img
├─src
└─template
  │─CfntemplateA.yaml
  │─CfntemplateB.yaml★本ファイルのみ更新対象としたい
  └─CfntemplateC.yaml
```
■GithubのGUI
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/6c6899cb-715d-0c55-7292-8c80f2948e25.png)

### 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/99fdc975-c111-5a91-301e-468a52f947bf.png)


## 実装方法

### パターン①：CodeBuild利用
#### 1.リモートリポジトリの作成

今回は Github のプライベートリポジトリの利用を想定しています。
リポジトリ作成方法は以下リンクを参考とするため、
本記事の記載からは割愛します。
[GitHub プライベートリポジトリの作成手順](https://qiita.com/jtFuruhata/items/0f28ebe5828005eae9b7)

#### 2.buildspec ファイル作成

buildspec ファイルとは、CodeBuild 実行時に実行するコマンドを記述したファイルとなります。
今回は更新されたファイルのみを検知できるよう以下記述になりました。
また、本ファイル作成後はリポジトリの最上位階層に格納する必要があります。

```yaml
version: 0.2

env:
  variables:
    AWS_REGION: ap-northeast-1
    CfntemplateA: CfntemplateA.yaml
    CfntemplateB: CfntemplateB.yaml
    CfntemplateC: CfntemplateC.yaml

phases:
  pre_build:
    commands:
      - echo "Checking for files SettingFalgs..."
      - CfntemplateA_UPDATE_FLAG=$(git diff --name-only HEAD~1 HEAD | grep "$CfntemplateA") || echo "$CfntemplateA_UPDATE_FLAG"
      - CfntemplateB_UPDATE_FLAG=$(git diff --name-only HEAD~1 HEAD | grep "$CfntemplateB") || echo "$CfntemplateB_UPDATE_FLAG"
      - CfntemplateC_UPDATE_FLAG=$(git diff --name-only HEAD~1 HEAD | grep "$CfntemplateC") || echo "$CfntemplateC_UPDATE_FLAG"
      - ls ./template/$CfntemplateA
  build:
    commands:
      - if [ -n "$CfntemplateA_UPDATE_FLAG" ]; then aws cloudformation deploy --template-file ./template/$CfntemplateA --stack-name CfntemplateA; echo " $CfntemplateA has been deployed."; fi
      - if [ -n "$CfntemplateB_UPDATE_FLAG" ]; then aws cloudformation deploy --template-file ./template/$CfntemplateB --stack-name CfntemplateB; echo " $CfntemplateB has been deployed."; fi
      - if [ -n "$CfntemplateC_UPDATE_FLAG" ]; then aws cloudformation deploy --template-file ./template/$CfntemplateC --stack-name CfntemplateC; echo " $CfntemplateC has been deployed."; fi

  post_build:
    commands:
      - echo "CloudFormation deployment completed."
```

`env`：buildspec.yml 内で共通で利用できる環境変数の設定
`pre_build`：ビルド実行前に実行するコマンド。主に依存パッケージのインストールやファイル取得など。
`CfntemplateA_UPDATE_FLAG=`：後段の処理を変数として格納
`git diff --name-only HEAD~1 HEAD`：「git diff」コマンドにて、コミット差分を検出する。コミットにはコミット単位でハッシュ値(コミットを表す一意の値)が付与される。HEAD は最新のハッシュ値を指定する記述であり、コミット対象となったファイルのみハッシュ値が更新されるため、ファイルが更新されない限り HEAD は同じ値となる。
※「HEAD~1」は最新コミットの 1 つ前のを指す。
※「--name-only」は差分として検出されたファイル名を抜き出す
`build`：ビルド時に実行するコマンド。
`if [ -n "$CfntemplateA_UPDATE_FLAG" ]`：対象変数に値が格納されている場合のみ後続の処理を実行
`CloudFormation deploy`：更新対象テンプレテートファイルを用いて、スタック更新


#### 3.CodePipeline の作成
AWS コンソールより CodePipeline の作成を選択する。
※以下一部設定項目のみピックアップして記載
- 必須設定はなく各値は任意で設定
- パイプラインタイプもv1,v2どちらでも良い

![2024-03-20_10h39_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/087c9600-f705-34a9-4f35-841f4f42bbeb.png)

##### 3-1.CodePipeline と GitHub リポジトリを紐づけ
以下設定を行い、「Githubに接続する」を選択
- ソースプロバイダー：Github(バージョン2)

![2024-03-20_10h42_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/df6f3271-ea7f-f6bb-3f81-6b767f73200e.png)

- 適当な接続名を入力し、「Githubに接続する」を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/f3e94081-0a57-e7b6-7c5f-bcf9933ef038.png)

- 「新しいアプリをインストールする」を選択する。

![2024-03-20_10h46_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/b83b8f6c-b1fb-ee7e-4ba2-fdbdd86b499b.png)

表示内容に従ってGithubへログイン。
その後リソース取得元のリポジトリを選択し、「Save」を選択する。
![2024-03-20_10h47_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/682a10e1-7847-2c99-acf5-5424033f4fc7.png)

githubとの連携が完了したので、
イベントのトリガーとなるリポジトリとブランチを選択する。
- 出力アーティファクト形式：完全クローン
    - 本オプションはリポジトリのクローンを後続のCodeBuildの実行環境に格納するオプションとなる。
後続のCodeBuildでgit Cloneコマンドを実行するためこちらを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/ec01acb1-d1ad-29e6-a8b4-98ed97d5091b.png)

##### 3-2.CodeBuild の設定
CodeBuildにてデプロイ対象の検知とデプロイを実施する設定を行う。
また、Pipeline設定の流れでCodeBuildの設定を実施。

- プロバイダーを構築する：AWS CodeBuild
- 「プロジェクトを作成する」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/96425287-32a4-9270-7a31-fb8d457b8f5b.png)

CodeBuildに付与するサービスロールは、
IAM関連の権限不足に伴うエラーが発生する可能性があるため、
必要に応じて権限を付与すること。
※最小特権ではないが「IAMFullAccess」を付与すると確実

- Buildspec：buildspec ファイルを使用する
    - Buildspec 名 - オプショナル：デフォルト(buildspec.yml)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/577ef974-e9bf-89d8-6e28-ceb8257560db.png)

なお、デプロイステージは導入不要につきスキップとする

##### 動作確認

リモートリポジトリへの Push をトリガーに、
対象ファイルのみ CloudFormatin デプロイが行われているか確認。

- `CfntemplateB.yaml`のみ更新し、リモートリポジトリへ Push

```
$ git diff
diff --git a/template/CfntemplateB.yaml b/template/CfntemplateB.yaml
       Tags:
         - Key: Name
-          Value: !Sub "${SystemName}-${EnviromentId}"
+          Value: !Sub "${SystemName}-${EnviromentId}-aaa"
⇒「CfntemplateB」のみ更新されていることを確認
$ git add .
$ git commit -m ‘[test]テンプレートファイル追加’ -m ‘テスト用テンプレート追加’
$ git push origin main
```

githubへのプッシュをトリガーにパイプラインの起動を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/a07b7229-a4d1-f42f-4756-4ef0b1f610b1.png)


- CloudForamtion の画面を確認し、`CfntemplateB.yaml`のみ更新されていることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/b8873b67-d17a-f2d4-219f-d2e07545ae4e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/22ba9d3b-79f2-19b0-c007-f1a4104f2857.png)


以上で特定ファイルのみデプロイされたことが確認できました。

### パターン②：CodePipeline統合CloudFormationを利用
CodePipeline 統合 CloudFormationとは？

CodePipeline では CodeBuild から CloudFormation をデプロイしなくても、
CodePipeline が CloudFormation をターゲットにしたデプロイ機能を備えている。

ただ、本機能ではデプロイ対象となるファイル/スタックの個別設定が必要になり、
条件(更新の有無)に応じた条件分けが行えない。

[AWS CloudFormation アクションをデプロイする](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/integrations-action-type.html#integrations-deploy-CloudFormation)

#### 1.リモートリポジトリの作成
パターン①と同様に割愛

#### 2.CodePipeline の作成
AWS コンソールより CodePipeline の作成を選択する。
※以下一部設定項目のみピックアップして記載
- 必須設定はなく各値は任意で設定
- パイプラインタイプもv1,v2どちらでも良い

![2024-03-20_10h39_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/087c9600-f705-34a9-4f35-841f4f42bbeb.png)

##### 2-1.CodePipeline と GitHub リポジトリを紐づけ
「出力アーティファクト形式：CodePipeline のデフォルト」を選択
パターン①と同様のため割愛
##### 2-2.CodePipeline統合CloudForamtionの設定
デプロイステージより以下の通り設定を行う
- デプロイプロバイダー：AWS CloudFormation
- アクションモード：スタックを作成または更新する
- スタック名：構築/更新したいスタックを選択
- ソース名：SourceArtifact
    - ソースステージでgitリポジトリをクローンする場合、「SourceArtifact」のルートディレクトリはgitリポジトリのルートディレクトリとなる
- ファイル名：対象テンプレートファイル名を記載
    - template/CfntemplateB.yaml
- ロール名：事前にCodePipelineにSTSできる(信頼関係を設定)ロールを用意
    - Cloudforamtionのアクション権限を付与

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/23315b4e-527f-ea83-ea91-0a276709b53a.png)

##### 2-3.CodePipeline統合CloudForamtionの設定複製
「アクションの追加」を選択して、デプロイしたいテンプレートの数だけ2-2を繰り返し実行する。
![2024-03-24_00h27_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/7ed65a56-6ea5-1f27-46b5-1be09176341a.png)

今回は「CfntemplateA.yaml」「CfntemplateB.yaml」「CfntemplateC.yaml」を対象に、
デプロイステージ設定を行った。

![2024-03-24_00h38_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/787f98c5-a9f9-3390-4175-577cbecf9bb6.png)


##### 動作確認
リモートリポジトリへの Push をトリガーに、
対象ファイルのみ CloudFormatin デプロイが行われているか確認。

- `CfntemplateB.yaml`のみ更新し、リモートリポジトリへ Push

```
$ git diff
diff --git a/template/CfntemplateB.yaml b/template/CfntemplateB.yaml
       Tags:
         - Key: Name
-          Value: !Sub "${SystemName}-${EnviromentId}"
+          Value: !Sub "${SystemName}-${EnviromentId}-aaa"
⇒「CfntemplateB」のみ更新されていることを確認
$ git add .
$ git commit -m ‘[test]テンプレートファイル追加’ -m ‘テスト用テンプレート追加’
$ git push origin main
```
githubへのプッシュをトリガーにパイプラインの起動を確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/ddc05b47-13d4-0ed4-be82-12e98264fd9a.png)

- CloudForamtion の画面を確認し、CfntemplateB.yamlのみ更新されていることを確認
![2024-03-24_00h23_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/4b68ba01-357f-9da5-e979-0fa09fe2e9a9.png)
![2024-03-24_00h24_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1764568/55684acd-9796-d024-8649-21c22fa82cc7.png)

## 終わりに
今回はリポジトリを更新したら、
自動的にテンプレートがデプロイされる仕組みを作ってみました。
実案件であれば変更セット作成までとして、了承は手動実行が安全だと思います。
本記事が誰かの助けになれば幸いです。

## 参考
https://blog.chick-p.work/til/aws-codebuild-sam/

https://aws.amazon.com/jp/blogs/news/build-a-continuous-delivery-pipeline-for-your-container-images-with-amazon-ecr-as-source/

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html