GitHub ActionsからTerraform Planするお話

この記事は[IaC Meetup【Finatext × YUMEMI.grow】](https://yumemi.connpass.com/event/349633/)の登壇で発表した内容になります。

# 目次

1. CIでPlanするメリット
1. CIでPlanするための設定
1. Plan実行の流れ
1. 追記

# Planするメリット

* 実行環境の再現性を向上させられる
* インフラの一貫性維持とドリフト検知が
できる
* 依存関係のアップデートがしやすくなる

## 実行環境の再現性を向上させられる

### たまにある問題: Aさんの環境ではPlanできるのに、Bさんの環境ではPlanできない

* 認証の問題
* 権限の問題
* 環境の問題
* variablesの設定値の問題
* etc…

![tf-auth-fail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/67416d1c-e893-48cf-8c0e-fade02517a70.png)

上の図ではGitHubの認証エラーで弾かれている

### Planするために必要な環境が明示されるようになる

* CI上でPlanするために必要な準備を全て行う必要がある
* そのためにはPlanするための認証、権限、環境をすべて用意し設定しているはず

実際に動く環境として、実行環境の明示がなされる。CIで設定している内容はPlanするために必須であり、個人の環境から実施する際にも必要になる。

## インフラの一貫性維持とドリフト検知ができる

Terraformの内容と実際のリソースの状態が一致していることを確認する機会が増やせる

* 一致していないとApplyで設定が巻き戻ってしまう可能性
    * 手動で変更していたのであれば必要な変更では?
* この状態では新規に開発することが難しい

![tf-plan-changes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/b52932aa-8866-4600-a49e-61e75f7604b1.png)

スケジューラーが無効になっているがこれは有効にしてもOK? それとも無効のままがよい?
これを判断できないと迂闊にApplyできない
Terraform側を修正して現状維持するようにしてもいいが、間違ったインフラ変更だったという場合もある

## 依存関係のアップデートがしやすくなる

主な依存関係のProviderを更新させ続けられる

* Providerの更新があった際に、対応が必要かそのまま適用できるか判断できる
    * PlanでNo changesであれば適用して問題ない場合が多い
    * Changeがあれば仕様が変わっている可能性がある
* RenovateやDependabotで自動マージまで持っていくことも可能

![tf-renovate-merge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/4e9d54e9-d386-43e5-9f04-696a243b3d28.png)

renovate-approveを使用して依存関係の更新を完全自動化している。Providerの更新に対しplanで差分が検出されなければ自動でマージする設定になっている。

# CIでPlanするための設定

* 実行環境の設定
* 認証の設定
* 権限の設定

## 実行環境の設定

### Terraform

* `hashicorp/setup-terraform` のActionsでインストール可能
* バージョンの指定はお好みで…
* detailed-exitcodeのオプションとの相性が悪いようなのでterraform_wrapperは `false` に

```yaml
- uses: hashicorp/setup-terraform@v3
  with:
    terraform_wrapper: 'false'
```

### AWS

* `aws-actions/configure-aws-credentials` のActionを使用しOIDCによる認証を使用
* Workflowにおけるpermissionsに、`id-token: write` を設定すること
* `mask-aws-account-id` をtrueにするとアカウント番号を隠してくれる

```yaml
- uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-region: 'us-east-1'
    role-to-assume: ${{ secrets.AWS_TERRAFORM_PLANNER_ROLE }}
    role-session-name: 'TerraformCheck'
    mask-aws-account-id: true
    retry-max-attempts: 3
```

### Google Cloud

* `google-github-actions/auth` のActionを使用しOIDCによる認証を使用
* Workflowにおけるpermissionsに、id-token: writeを設定すること
* projectIdを隠したい場合はGitHub ActionsのSecretsに登録の上、 Workflowの中でSecretを参照する必要がある
* Checkoutした直下に認証用のファイルを作成するので注意 ( gha-creds-40c829577b183395.json のようなファイル)
    * このファイルが漏れても直ちに影響があるわけではない
    * コミット指定なファイルが増えるのでgit diffなどで引っかかることがある

```yaml
- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: ${{secrets.GOOGLE_CLOUD_IDENTITY_PROVIDER}}
    service_account: ${{secrets.GOOGLE_CLOUD_TERRAFORM_PLANNER_SERVICE_ACCOUNT}}
```

### GitHub

* `actions/create-github-app-token` のActionを使用しAppIdと秘密鍵でTokenを作成
* RepositoryやOrganizationの設定をしたい際に使用する
    * ActionsのSecretやEnvironmentなど
* Terraform側のProviderの設定でAppを使う設定にもできるが、instalation idが必要なので注意

```yaml
- id: generate-token
  uses: actions/create-github-app-token@v2
  with:
    app-id: ${{ steps.get-secrets.outputs.app_id }} # from Secret Manager
    private-key: ${{ steps.get-secrets.outputs.pem }} # from Secret Manager
    owner: ${{ github.repository_owner }}
```

### その他

* サービスによって異なるが、API Keyなどを設定
    * APIKeyはActionsのSecretや、クラウドのSecret Managerなどから持ってくることになりそう
* AWSに保存していれば、ephemeralblockで引っ張ってきて、Providerの設定に使用することもできる
    * GoogleCloudのSecret Managerはephemeralresourceの実装が現状ない

```terraform
ephemeral "aws_ssm_parameter" "cloudflare_token" {
  arn = local.cloudflare_token_arn
}
provider "cloudflare" {
  api_token = ephemeral.aws_ssm_parameter.cloudflare_token.value
}
```

## 権限の設定

### 必要な権限

* Stateファイルの読み込み/書き込みの権限
    * Applyするなら書き込みが必須。Planだけなら読み込みでOK
    * KMSで暗号化しているならその関連も必要
* Lockに関する権限
    * Plan時にLockしない(`-lock=false`)なら不要
    * 書き込みの権限であることが多い
        * AWSならS3かDynamoDB
        * GoogleCloudならCloud Storage
* Planするリソースに関する権限

### Planするリソースに関する権限

(最小権限の原則を守れるのであればそれが一番)

* マネージドな権限を設定するのが簡単+ アクセスさせたくないものはDenyするなど
    * AWSならReadOnlyAccessのポリシー
        * SSM Parameterの内容が読めるので注意
    * Google Cloudなら閲覧者のロール
    * どちらもSecret Managerの内容は読めない
* 実際に使う際にはよく調査してから

![aws_readonly_policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/0065efe5-26cb-4ee2-8671-f28226a5cc96.png)
![google_cloud_viewer_role.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/9852eefb-12fb-486e-b9f9-a62ead4e7c86.png)

# Plan実行の流れ

1. 認証の準備
1. `terraform init`
1. `terraform plan`

## 認証の準備

これまでで設定した認証を使用していく

![auth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/ba4719c0-9f52-4a2a-9876-39a39d5ff5e0.png)

## `terraform init`

![init.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/37b25aaa-c0ea-4cd4-a1c0-6fe1712d21a9.png)

これだけで1GB弱のProviderが設置されるので注意。AWS Providerが600MB以上を占める。Google CLoudやCloudflareは130MB未満くらい。

* Providerをダウンロードしていく
    * 数百MBあるので、通信量には気を付ける
        * 特にSelf Hosted Runnerを使用している場合など
    * Providerをキャッシュするとダウンロード回数が減る
        * `TF_PLUGIN_CACHE_DIR` の指定で使用可能

![init-cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/c30ab3e8-7784-4e6e-9435-85e588b80873.png)

> from shared cache directory

とあるのでキャッシュが使われている。

## `terraform plan`

```bash
terraform plan -detailed-exitcode -lock=false
```

* `-detailed-exitcode` で、差分がある場合に終了コードを2にしてくれる
    * https://developer.hashicorp.com/terraform/cli/commands/plan#detailed-exitcode
* ファイルに書き出す場合は-no-color もつけるのがおすすめ
    * privateやinternalのリポジトリでは不要だが、publicなリポジトリでやっている場合にはファイルに出力するなど簡単にはterraformの実行結果を見られないようにした方が安全
* lockする設定だと、Renovateなどで一斉にProviderが更新された際に並列でplanされ、lockの取得に失敗する

![plan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3355242/8875e913-d269-4f18-b73c-b3fafa4b5d1f.png)

# まとめ

CI組まないとRenovateやDependabotでオートマージさせられないのでPlanしよう。

# 追記

質問のあったことを記載します

## Applyはどうしているのか

自分は手元でやっています。CIではやっていません。

適切な権限を割り当てればCIでも `terraform apply -auto-approve` できるはずです。

## Planの結果を見るのにサードパーティのActionsなどは使用しているか

例として以下のActionsが挙げられていました。

https://github.com/suzuki-shunsuke/tfaction

自分のユースケースとしては不要なため使っていないです。
もともとPublic RepositoryでPlanしていたため、Planの結果を不特定多数が見られる箇所には記載しないようにしていました。そのためPlan結果はファイルに書き出して個人のCloud Storageにアップロードしています。

チーム開発などでPlanの結果を見せることがあるといった場合には非常に便利かと思います。