CloudFormationのGit同期を自動で作成する（複数のAWSアカウントにも対応）

CloudFormation(CFn)の[Git同期](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/git-sync.html)とは、GitリポジトリにPushしたらスタックが自動で更新される便利な機能です。プルリクエストに変更内容がコメントされる点も素晴らしいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/f193a78a-368f-c8b4-a0a6-4d07f46b8cef.png)


ただし、公式では「マネジメントコンソールでスタックごとにGit同期を設定する方法」しか紹介されておらず、スタックが多い場合はかなり絶望的でした。

そこで本記事では「**Gitリポジトリにファイルを追加したら、自動でスタックとGit同期が作成される状態を作る方法**」を解説します。

# TL;DR

先にゴールを紹介します。この記事では最終的に

```bash
aws cloudformation create-stack \
  --stack-name "スタック名" \
  --template-body "file://初期化用ファイル.yaml" # 何かファイルがないとスタックは作れない
```

で空のスタックを作成し、そのあと

```bash
aws codeconnections create-sync-configuration \
  --branch "ブランチ名" \
  --resource-name "スタック名" \
  --config-file "デプロイファイル.yaml" \
  --repository-link-id "REPOSITORY_LINK_ID" \
  --role-arn "IAM RoleのARN" \
  --pull-request-comment ENABLED \
  --sync-type CFN_STACK_SYNC
```

でGit同期を作成します。

さらにGitHub Actionsを用いて、新規追加されたファイルに対して上記コマンドを実行します。




# 事前準備 GitHubとAWSを連携する

:::note info
これだけはどうしても自動化できません :bow:
:::

まずはGitHubとAWSを接続します（GitLabやBitbucketも接続できます）

AWSマネジメントコンソールで CodeBuild > [接続](https://ap-northeast-1.console.aws.amazon.com/codesuite/settings/connections) > 接続を作成

![接続の一覧画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/1fe46e81-0b2c-d85c-ba40-addb5daeddc6.png)

画面に従ってGitHub Appをインストールします。接続名はGitHubのユーザー名（またはOrganization名）をおすすめします。

![接続名を入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/0b2b0b5c-9e78-5cd6-6cd5-0dc0253784ba.png)

![GitHub Appのインストール](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/8fa5b371-12c3-d629-4dec-253287bc6afd.png)

完了後に表示されるARNはこのあとすぐ使います。

![接続の作成が完了](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/cd809904-30de-64c3-cf3c-302cc19a3950.png)

次にAWS CLIでリポジトリリンクを作成します（マネジメントコンソールからは作れません[^1]）

[^1]: 正確には、CloudFormationのGit同期を作成するときの画面では作ることができます。ただしこの段階ではスタックを作りたくないのでAWS CLIで作業しています

```bash:リポジトリリンクを作成する
aws codeconnections create-repository-link \
  --connection-arn "今作った接続のARN" \
  --owner-id "GitHubのユーザー名（またはOrganization名）" \
  --repository-name "GitHubのリポジトリ名"
```

出力された**リポジトリリンクID**は手元に保存してください。

```txt: リポジトリリンクIDの例
f919f952-e050-4b24-XXXX-XXXX494d49d6
```

準備は以上です。

なお、今回は違いますが、Gitリポジトリが2つ以上ある場合は次のようにします。

- GitHub Appのインストール時に全てのリポジトリを指定する
  - GitHub > Settings > [Applications](https://github.com/settings/installations) であとから追加することも可能
  - リポジトリを制限せずにインストールするのもOK
- 1つのリポジトリにつき1つのリポジトリリンクを作成する
  - `aws codeconnections list-repository-links`で一覧を見られる

# 1. テンプレートとデプロイファイルを作る

:::note info
Git同期を使う場合、いつもの**テンプレート**に加えて**デプロイファイル**を作ります
:::

まずは「焼肉プロジェクト」の本番環境のIAMユーザーのファイルを作ります。

```yaml: デプロイファイル yakiniku-iam.yaml
template-file-path: iam.yaml # テンプレートのパス

parameters:
  env: prd
  project: yakiniku
```

```yaml: テンプレート iam.yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  env:
    Type: String
  project:
    Type: String

Resources:
  IAMUser:
    Type: AWS::IAM::User
    Properties:
      UserName: !Sub ${env}-${project}-user

```

次にテンプレートを使いまわして「寿司プロジェクト」のIAMユーザーのファイルを作ります。

```yaml: デプロイファイル sushi-iam.yaml
template-file-path: iam.yaml

parameters:
  env: prd
  project: sushi # ここだけ変えました！
```

今回は本番環境(`prd`)と事前環境(`stg`)の2つのAWSアカウントがある想定で、次のようなディレクトリ構成にしてみました。

```bash: ディレクトリ構成
├── cfn-deployments # デプロイファイル
│   ├── prd
│   │   ├── sushi-iam.yaml
│   │   ├── sushi-log.yaml
│   │   ├── yakiniku-iam.yaml
│   │   └── yakiniku-log.yaml
│   └── stg
│       ├── yakiniku-iam.yaml
│       └── yakiniku-log.yaml
└── cfn-templates
    ├── iam.yaml # IAMユーザーのテンプレート
    └── log.yaml # CloudWatchロググループのテンプレート
```

これらのファイルはこのあと使います。

# 2. 初期化用のCloudFormationテンプレートを作る

Git同期を設定するには先にスタックを作る必要があります[^2]

[^2]: [公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/git-sync-status.html#git-sync-status-stack-state)を参照。変更セットだけを作成した状態(`REVIEW_IN_PROGRESS`)では設定できない点に注意してください

空のスタックを作るために、次のようなテンプレートを準備しておきます（[参考](https://stackoverflow.com/questions/62990653/create-cloudformation-stack-without-resources)）

```yaml: initial-template.yaml
AWSTemplateFormatVersion: 2010-09-09

Resources:
  NullResource:
    Type: AWS::CloudFormation::WaitConditionHandle
```

# 3. スクリプトを作る



まずは「Gitリポジトリに新規追加されたファイルを一覧にする処理」が必要です。

作成だけに対応する場合（削除はしない場合）は次のように実装できます。

```bash: create-git-sync.sh（一部抜粋）
DIR="対象のディレクトリ"

# リモートのスタック一覧（削除済みは除く）（数が多い場合はページングが必要）
remote="$(aws cloudformation list-stacks --query 'StackSummaries[?StackStatus!=`DELETE_COMPLETE`].[StackName]' --output text | sort)"

# ローカルのファイル一覧
local="$(find "$DIR" -name '*.yaml' -exec basename {} .yaml \; | sort)"

# 差分（＝新規作成されたファイル一覧）
diff="$(join -v2 <(echo "$remote") <(echo "$local"))"
```

これをfor文で回し、スタックとGit同期を作成します。なお、デプロイファイルのファイル名をスタック名にしています。

```bash: create-git-sync.sh（一部抜粋）
for stack_name in $diff; do

  echo "NEW STACK: $stack_name"

  # スタックの作成
  aws cloudformation create-stack \
    --stack-name "$stack_name" \
    --template-body file://initial-template.yaml

  # Git同期の作成
  aws codeconnections create-sync-configuration \
    --branch "ブランチ名" \
    --resource-name "$stack_name" \
    --config-file "$DIR/$stack_name.yaml" \
    --repository-link-id "事前準備で作成したリポジトリリンクID" \
    --role-arn "CloudFormationで使用するIAMロールのARN" \
    --pull-request-comment ENABLED \
    --sync-type CFN_STACK_SYNC

done
```

スクリプトの全体はGitHubでご確認ください。

https://github.com/tippy3/aws-cfn-git-sync/blob/main/create-git-sync.sh

# 4. スクリプトをGitHub Actionsで動かす

作成したスクリプトをGitHub Actions(GHA)で動かします。

今回は`prd`と`stg`の2つのAWSアカウントを使うので、Jobは2並列で動かします。

```yaml: .github/workflows/main.yaml（一部抜粋）
jobs:
  create-git-sync:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        conf:
          - account: AWS_ACCOUNT_ID_PRD
            branch: main
            dir: cfn-deployments/prd # デプロイファイルのディレクトリ(prd)
            link-id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
          - account: AWS_ACCOUNT_ID_STG
            branch: main
            dir: cfn-deployments/stg # デプロイファイルのディレクトリ(stg)
            link-id: YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
```

次のようなステップを作り、先ほどのスクリプトを動かします。

```yaml: .github/workflows/main.yaml（一部抜粋）
      - name: create-git-sync
        env:
          BRANCH: ${{ matrix.conf.branch }}
          DIR: ${{ matrix.conf.dir }}
          LINK_ID: ${{ matrix.conf.link-id }}
          ROLE_ARN: arn:aws:iam::${{ secrets[matrix.conf.account] }}:role/my-cloudformation-role
        run: |
          ./create-git-sync.sh "$BRANCH" "$DIR" "$LINK_ID" "$ROLE_ARN"
```

ワークフローの全体はGitHubでご確認ください。

https://github.com/tippy3/aws-cfn-git-sync/blob/main/.github/workflows/main.yaml

これで実装は完了です！

# 結果

リポジトリにファイルを追加すると、自動でスタックとGit同期が作られるようになりました:tada:

![作成されたスタック一覧](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/6193fcaf-2c29-22cd-a79b-d065244aaab3.png)

また、プルリクエストを作るとGitHub Appがスタックの変更内容をコメントしてくれます！（Git同期の作成時に`--pull-request-comment ENABLED`を付けたからです）

![コメント](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/5de9cf8b-4ba2-41db-1cec-4bf5841ac040.png)

# ソースコード

作成したファイルは下記で公開しています。

https://github.com/tippy3/aws-cfn-git-sync/tree/main

最後までお読みいただきありがとうございました。よろしければ「いいね」もお願いします！