Terraform CI/CDの改善してみた

## はじめに

こちらは LITALICO Engineers Advent Calendar 2023 の4日目の記事です。昨日は @nonikeno によるFlutterによる生体認証に関する記事でした。

LITAILCOのSREグループでマネージャーしております、 @tjinjin です。
最近マネジメント中心になってしまっておりますが、アドカレ駆動で改善したのでその辺りの話です。

## 前提
現状のSREグループでは様々なプロダクトを少人数で対応する手前、ステートレスな状況を作り上げることが重要です。リリース頻度も高い状況ではないため、指定するタイミングで変更を適用しなければいけないことがあります。

上記も踏まえて、現状は以下のようなフローで実現しておりました。

- TerraformのCI/CDはGitHub Actionsで実施
- PRでTerraform用のRoot directory以下の変更があった場合、実行ディレクトリ※(下記ディレクトリ構造のenv以下）を全部取ってきて、state単位で並列実行
- 差分が合ったものは、共通で管理しているcomposite-actions（自作）を使って、PRコメントを書く
- applyはPRがマージされたタイミングで任意のタイミングで実施

ディレクトリイメージのサンプルは下記です。moduleはあまり使わず、terragruntとproject-resourceと呼ばれる、プロダクト内の共通設定を使い回すような仕組みになっています。

```bash
├── README.md
├── env
│   ├── common
│   ├── dev
│   ├── prd
│   ├── shared    ※ 環境を跨いで利用するリソースをここに配置する。
│   ├── stg
│   └── test
│       ├── backend-config.yml         ※ terragruntで利用する環境設定ファイル。
│       ├── terragrunt.hcl -> ../../terragrunt/common/terragrunt.hcl
│       └── vpc        ※コンポーネント毎に実行ディレクトリを用意する。
│           ├── (backend.tf)
│           ├── (env_locals.tf)
│           ├── locals.tf -> ../../../project-resource/vpc/locals.tf
│           ├── main.tf -> ../../../project-resource/vpc/main.tf
│           ├── outputs.tf -> ../../../project-resource/vpc/outputs.tf
│           ├── xxx.tf       ※ 環境特有のリソースを記述する。
│           ├── xxx.tf    ※ 環境特有のoutputを記述する。
│           ├── (provider.tf)
│           ├── terragrunt.hcl -> ../../../terragrunt/component/terragrunt.hcl
│           └── (version.tf)
├── modules
├── project-resource
│   └── vpc
│       ├── locals.tf  ※ 変数を記述するtfファイル。map形式で各環境の変数を記載する。env/stage/componentにシンボリックリンクファイルを配置する。
│       ├── main.tf    ※ リソースを記述するtfファイル。env/stage/componentにシンボリックリンクファイルを配置する。
│       └── outputs.tf ※ outputを記述するtfファイル。env/stage/componentにシンボリックリンクファイルを配置する。
└── terragrunt
    ├── common
    │   └── terragrunt.hcl  ※ terragruntの実設定ファイル。env/stageにシンボリックリンクファイルを配置する。
    └── component
        └── terragrunt.hcl  ※ 上記を参照するためのファイル。env/stage/componentにシンボリックリンクファイルを配置する。

()で囲われているファイルはterragruntで作成されたファイルであることを示す。
```

コメントのイメージは下記です。

![plan_comment_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63145/539255fb-4327-ae57-b086-a9b63261fcbe.png)

直近余力がなかったのでこの運用で回していましたが、いろいろな問題が出てきました

- Renovateを途中から導入したため、GHAの実行回数が増大しコスト増加に
- Terraform関連のPRを出した際に対してすべてのstateに対してplanが実行される。意図してapplyを待っているものに対しても差分は検出されるため、PRとは関係ないものもPRコメントに出てしまってレビューしづらい
    - hide commentする謎作業が多発
- pushするごとにPRが走るので何度もコミットすると大変なことに :innocent: 
    - PRの出しづらさを感じさせる実装は利用者のコストが高い

## 実現したかったこと

上記のため以下の要件に沿って実装しました。

- GHAのコストを適正化したい
- PRの差分に関するもの以外はplanを実行しない
- stateとコードのずれは別の仕組みで担保する
- terraform planの結果はPR内で一つだけにしたい（plan実行のたびにコメントしたくない）

## 実装

以下のようになりました。

- PRではdiffで検知した対象のstateを検出し、それらに関連するplanのみを実行するためのshellを頑張って書きました。その結果をmatrixを使って実行しています
- 全体に対してterraform planを実行するworkflowを外出しし、 `tf_plan` というlabelが付与された場合にworkflowを実行する
- issueを作成し、tf_planラベルを実行することで、全体に対してのterraform planが実行され、その結果がissueコメントに起票される
- 差分のコメントは [tfcmt](https://github.com/suzuki-shunsuke/tfcmt) で行うように見直し。tfcmtでコメントの更新をサポートしていたのでその方針を採用
    - [tfcmt がコメントの更新をサポート](https://zenn.dev/shunsuke_suzuki/articles/tfcmt-plan-patch)
- Renovateと組み合わせて、packageRulesでterraform関連の変更があった場合は、addLabelsで `tf_plan` を付与させることで、バージョンアップ時にplanを実行させる


トリガーのイメージは下記です。

```.github/workflows/terraform_plan_all.yml
name: "terraform_plan_all"

on:
  issues:
    types:
      - labeled
  pull_request:
    types:
      - labeled

```

issueによるterraform planの実行イメージも貼っておきます。

![issue_plan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63145/e0792b89-9550-ce43-b736-73d09a07a0db.png)

![issue_plan_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63145/501330dd-2db4-a331-914b-c0b418e6c668.png)

まだ実装したてなので今後運用をしていきますが、実現したいことは達成できたかなと思います。

## 今後
- RenovateによるPRでAuto Mergeを入れられてないので導入したい
- terraform fmtで差分を検知したときに、commitやPRを作るのではなく、PRコメントで変更を指摘しauthorが自分で気づいて直せるようにしたい


## まとめ
こんな感じで少人数でマルチプロダクトを回すための仕組みづくりを頑張っていきたいと思います。SREは積極採用中です！

https://herp.careers/v1/litalico/requisition-groups/ea1f6b9d-88b7-4254-a06a-8cfdd83bd575