Terragrunt リポジトリでも Tfcmt で Plan 結果を Github 上に表示したい

皆さんは Terraform リポジトリを管理する際、CIを組んでいるでしょうか？

プルリク公開時に Terraform Plan を自動実行して、結果をGithub上で確認できると、変更箇所が伝わりやすくなりハッピーかと思います。

tfcmt はそんなTerraformのCIを組みたいときに活躍するツールで、terraform plan の結果を素敵な感じに整形して、プルリクにコメントしてくれます

(tfcmt 自体の使い方について知りたい場合、[こちらの記事](https://zenn.dev/shunsuke_suzuki/articles/improve-terraform-cicd-with-tfcmt)が参考になりました)

![1tfcmt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591507/cff3d47e-50b3-184f-10ea-1ffaed4ea1da.png)

ただ、Terragrunt のような複数の Statefile を管理するリポジトリの場合、プルリク上に大量にコメントが流れてしまい、かえって見づらくなることもあるかなと思います。

今回触っていた IaC リポジトリでも、6環境×モジュールが6~7個あったため、最大40個強のPlan結果がプルリクコメントに並んでしまうことになり、さすがにこれを運用すると辛そうだな...という印象でした。

↓は Bot が大量に PR にコメントを流している様子です。

![2tfcmt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591507/206d8cc6-a5c3-146f-8399-060dc6a9b99e.png)

そこで、Github Workflow の Summary に結果を見やすくまとめることで、上記の課題を解決しつつ、快適に terraform の CI を運用する方法を紹介したいと思います。

Terragrunt でなく生の Terraform を使っていても、State の分割管理を行っている場合、同様の課題感を持っていたりすると思うので、そういった方の参考にもなれば幸いです。

ついでに Github Enterprise 環境で tfcmt を構築している記事もあまり見かけなかったので、その解説もできたらと思っています。

## 結論

いろいろ試した結果、こんな形で出力させることにしてみました。

![image-6-1024x489.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591507/d31c4fb7-7fff-995f-7e8d-c03d5f9010bf.png)

環境ごとに Github Actions のワークフローを作成し、サマリー部分にモジュールごと実行した Plan 結果を見やすくまとめています。

逆にプルリクのラベル付けやコメントを書き込む機能などは、Statefile が大量にあるリポジトリの場合、逆に見づらくなってしまったため、明示的に無効化を行いました。

## 実装

### 全体のディレクトリ構成

Terraform リポジトリのディレクトリ構成としてはこんな感じになっています

```
.
├── .github
│   ├── workflows
│   │   ├── develop_terraform_plan.yml
│   │   ├── staging_terraform_plan.yml
│   │   └── ... (other files)
│   └── tfcmt
│       ├── tfcmt.yml
│       └── tfwrapper.sh
├── environments
│   ├── develop
│   │   └── ap-northeast-1
│   │       └── ... (other files)
│   └── ... (other environments)
├── modules
│   ├── ecs
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── ... (other files)
│   └── ... (other modules)
└── terragrunt.hcl
```

### tfcmt.yml の設定

tfcmt.yml という名前で config ファイルを書いておき、tfcmt コマンド実行時にオプションに指定してあげることで、plan 結果を整形するフォーマットをカスタマイズできます。

かなりカスタマイズしやすく、個人的に感動したポイントでした。

参考: https://suzuki-shunsuke.github.io/tfcmt/config#default-configuration

今回設定した tfcmt.yml の内容になります

```
embedded_var_names: []
terraform:
  plan:
    # github enterprise の時のみ設定が必要
    # 参考: https://suzuki-shunsuke.github.io/tfcmt/github-enterprise
    ghe_base_url: https://XXX.github.com
    ghe_graphql_endpoint: https://XXX.github.com/api/graphql
    # terragrunt だと statefile 毎ラベルが上書きされて逆に管理し辛かったので無効化
    disable_label: true
    template: |
      <details><summary><strong> :package: Target Module: {{.Vars.target}} </strong></summary>
        {{template "result" .}}
        {{template "updated_resources" .}}
        {{template "changed_result" .}}
        {{template "change_outside_terraform" .}}
        {{template "warning" .}}
        {{template "error_messages" .}}
      </details>
```

ポイントとしては以下になります
* Github Enterprise の場合は plan 実行時の 環境変数に ghe_base_url と ghe_graphql_endpoint を足してあげる必要があります
* tfcmt は自動で plan 結果に沿ったPRのラベル付けを行ってくれるのですが、前述の通り複数Statefileを管理している場合、見辛さが勝ってしまったので無効化しています

* `template:` に続くブロックで plan 結果をどんな感じで整形するか指定できるので、お好みでいじってみても良いかと思います。

### tfwrapper.sh の設定

Terragrunt の場合、tfcmt が terragrunt run-all plan コマンドをそのまま叩けないので wrapper のスクリプトをかませてあげる必要があります。

```
#!/bin/bash
set -euo pipefail

# コマンドの種類を取得(例: apply, plan, fmt...)
type=$(echo "$@" |  awk '{print $1}')

# カレントディレクトリを整形して、target の module 名を取得
base_dir=$(git rev-parse --show-toplevel)
target=${PWD#"$base_dir"/}
target=$(echo "$target" | sed 's|/\.terragrunt-cache/.*||')

if [ "$type" == "plan" ]; then
    tfcmt --config "$GITHUB_WORKSPACE/.github/tfcmt/tfcmt.yml" \
    --output "/tmp/tgplan.md" \
    -var "target:${target}" \
    plan -patch -- terraform "$@"
fi
```

ここが terragrunt run-all plan コマンドを実行した時の実体のスクリプトになるのですが、その際に　tfcmt の処理を噛ませてあげる流れとなっています

以下オプションの説明になります

* `--config オプション` : plan 結果の整形フォーマットなどを config ファイルに書くことができます。上のステップで作成した tfcmt.yml ファイルを config として指定します

* `--output オプション` : tfcmt のメインの使い方は、プルリクのコメントに plan 結果を出力することだと思いますが、今回はそれを無効化しローカルにファイルに吐き出しています

* `-var オプション` : tfcmt に変数を渡すことができます。今回は terraform のモジュール名を渡して、tfcmt が Plan 結果を整形する際に、モジュール名も含めて出力できるようにしています 

細かい仕様は 公式 doc を参照ください。

参考: https://suzuki-shunsuke.github.io/tfcmt/terragrunt-run-all


### Github Actions のワークフロー

```
name: Execute terragrunt plan
on:
  pull_request:
    branches:
      - 'main'
    paths:
      - 'environments/develop/**'
      - 'modules/**'
  workflow_dispatch:
env:
  Enviromnet: develop
  IAM_ROLE_ARN: XXX
jobs:
  plan-common:
    runs-on: XXX
    steps:
      - uses: actions/checkout@v3

     # plan 用の権限を assume する
      - name: assume ECS Role
 　　 　uses: aws-actions/configure-aws-credentials@v4
   　　　 with:
   　　　   aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      　　　aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      　　　aws-region: ap-northeast-1

      # tfcmt のインストール
      - name: setup tfcmt
        env:
          TFCMT_VERSION: v4.13.0
        run: |
          curl -L "https://github.com/suzuki-shunsuke/tfcmt/releases/download/${TFCMT_VERSION}/tfcmt_linux_amd64.tar.gz" -o /tmp/tfcmt.tar.gz
          tar xzf /tmp/tfcmt.tar.gz -C /tmp
          mv /tmp/tfcmt /usr/local/bin
          tfcmt --version

      # terragrunt plan の実行
      - name: Excute terragrunt plan
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          cd ./environments/${{ env.Enviromnet }}
          terragrunt run-all init
          chmod a+x $GITHUB_WORKSPACE/.github/tfcmt/tfwrapper.sh
          terragrunt run-all plan -no-color -input=false --terragrunt-tfpath $GITHUB_WORKSPACE/.github/tfcmt/tfwrapper.sh

      # actions の workflow summary に plan 結果の summary を通知する
      - name: Create Job Summary
        if: always()
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            const filePath = '/tmp/tgplan.md';
            const output = fs.readFileSync(filePath, 'utf8');
            await core.summary
              .addHeading('Terragrunt plan report')
              .addRaw(output)
              .write();
```

まず terragrunt plan を実行するための IAM 権限の Assume Role と、tfcmt をローカルにインストールします。

次にメインとなる terragrunt run-all plan を実行しています。

[ここで補足した通り](#tfwrappersh-の設定)、tfcmt が直接 terragrunt run-all plan を実行することができないので、--terragrunt-tfpath オプションで wrapper スクリプトを呼び出し、そのスクリプト内で tfcmt を実行させています。

その後、actions/github-script@v6 を使い、tfcmt が吐き出した md ファイルを Github の Summary 部分に書き込む処理を入れています。

結果、こんな感じで Summary に環境ごとの Plan 結果をまとめることができました！

![image-6-1024x489 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591507/7de90e33-0a23-25ac-c67b-de8ce8372e20.png)

## 改善ポイント

使い勝手自体の改善は満足したので、このまま他のいろんなIaCリポジトリに横展開していきたいと考えています。

横展開する際、毎回 tfcmt の Config やらを設定するのは面倒なので、共通部品化してモジュールを他の IaC リポジトリに配布できるといいなと考えています。機会があればチャレンジしてみようと思います。

tfcmt のおかげで Terraform の CI 体験が大分改善しそうで楽しみです！