Terraform 運用 #1 チームワークフロー — PR で plan レビュー、GitHub Actions で apply

読了 4分

実践シリーズ 10 本でインフラは完成しましたが、運用のやり方はまだ「各自のノート PC で apply」のままです。この方式はチームでは長く持ちません。強い AWS 権限を個人のノート PC ごとに置く必要があり、レビューなしで適用が起き、誰が何をいつ apply したのか記録が残りません。運用シリーズ 7 本の最初のテーマは、この構造を変えることです。目標は 人はコードと plan をレビューし、apply は CI だけが実行する という一文に要約できます。

ワークフローの形 — PR に plan、マージに apply #

Git コラボレーションの関門が PR なので、Terraform の関門も PR に合わせます。

ワークフローの流れ
ブランチでコードを修正 → PR 作成
  → CI: fmt チェック、validate、plan 実行
  → plan の結果が PR コメントとして登録される
  → レビュアーがコードと plan をまとめて承認
main にマージ
  → CI: マージされたコードで apply

観点の転換として重要なのは、レビュー対象がコードだけでなく plan でもあるという点です。コードの diff は意図を見せ、plan は結果を見せます。基礎 #2で -/+(置き換え)の記号を必ず確認するように言いましたが、その確認が個人の習慣ではなくチームのレビュー手順になる瞬間です。

認証情報 — OIDC ロールの回収 #

CI が AWS を操作するには認証情報が必要ですが、実践 #7で作っておいた GitHub OIDC ロールは、まさにこの用途のためのものでした。そのロールにデプロイ権限(管理対象リソースと state バケットへのアクセス)を付け、ワークフローではアクセスキーなしでロールを引き受けます。保存されたシークレットがないので、漏えいするシークレットもありません。

GitHub Actions のワークフロー #

2 つのファイルで構成します。まず PR 側です。

.github/workflows/terraform-plan.yml
# .github/workflows/terraform-plan.yml
name: terraform plan
on:
  pull_request:
    paths: ["envs/**", "modules/**"]

permissions:
  id-token: write      # OIDC トークンの発行
  contents: read
  pull-requests: write # plan コメントの登録

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.TF_PLAN_ROLE_ARN }}
          aws-region: ap-northeast-2
      - uses: hashicorp/setup-terraform@v3
      - run: terraform fmt -check -recursive
      - run: terraform -chdir=envs/dev init
      - run: terraform -chdir=envs/dev validate
      - run: terraform -chdir=envs/dev plan -no-color -out=tfplan
      # 続けて plan 出力を PR コメントとして登録するステップ

plan 出力を PR コメントに載せる部分は、公開アクションか gh pr comment を使う短いスクリプトで処理します。次はマージ側です。

.github/workflows/terraform-apply.yml
# .github/workflows/terraform-apply.yml
name: terraform apply
on:
  push:
    branches: [main]
    paths: ["envs/**", "modules/**"]

permissions:
  id-token: write
  contents: read

concurrency:
  group: terraform-dev
  cancel-in-progress: false

jobs:
  apply:
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.TF_APPLY_ROLE_ARN }}
          aws-region: ap-northeast-2
      - uses: hashicorp/setup-terraform@v3
      - run: terraform -chdir=envs/dev init
      - run: terraform -chdir=envs/dev apply -auto-approve

設定の 1 つ 1 つが運用上の安全装置です。

  • concurrency: マージが連続しても apply は一度に 1 つだけ動きます。基礎 #9の state ロックが最後の防衛線だとすれば、これは最初の防衛線です。cancel-in-progress: false により、進行中の apply を中断しません(apply の中断はリソースを中途半端な状態のまま残します)。
  • environment: dev: GitHub の環境機能に承認者を設定すると、apply の直前に人の承認ステップを入れられます。prod のワークフローで特に有用です。
  • paths フィルター: インフラのコードが変わったときだけ動くので、ドキュメント修正のようなコミットに CI を浪費しません。
  • ロールの分離: plan 用ロールは読み取り中心にして、apply 用ロールだけに書き込み権限を持たせれば、PR 段階の権限を最小化できます。

信頼境界がこう変わります #

このワークフローが定着すると、権限の構造が変わります。人には読み取りと plan の権限だけを残し、書き込み権限は CI のロールにだけあります。緊急時にローカル apply が必要になればできなくはありませんが、それは例外手順であり、例外が起きたという事実そのものが監査記録に残ります。「誰がいつ何を適用したのか」の答えが、Git の履歴と Actions のログで完結する構造です。

prod まで広げても、構造は同じで値だけが違います。envs/prod 用のワークフローに prod のロールと承認必須の environment を掛ければ、実践 #9で作った「dev が先、検証してから prod」の流れが CI の上で回ります。

まとめ #

今回扱った内容です。

  • ローカル apply は権限の集中、レビューの不在、記録の不在という問題を抱えます。原則は「人はレビュー、apply は CI」です
  • PR では fmt、validate、plan が動き、plan の出力がコメントとして残るので、コードと結果をまとめてレビューします
  • CI の認証情報は実践 #7 の OIDC ロールです。plan 用と apply 用にロールを分けて、PR 段階の権限を最小化します
  • concurrency で同時 apply を防ぎ、environment の承認で prod の前に人の関門を置きます
  • 例外的なローカル apply は可能ですが、記録に残る例外になります

次回(#2 drift と state の手術)では、このワークフローの外で起きる変更を扱います。コンソールでの手動変更の検知、import ブロックと removed ブロックによる state への編入と除外です。

X