🚧

Terraform CI/CDを安全に:Planファイル活用とGitHub Actions

に公開

はじめに

Filmarksインフラチームの倉本と申します。

この記事では、Terraform CI/CDパイプラインにおけるPlanファイルの活用方法に焦点を当てています。そのため、Terraform自体の基本的な使い方(plan,apply,tfstate)やGitHub Actionsの詳細な説明は省略します。

Planファイルとは?

そもそもPlanファイルとは、outオプションで生成されるバイナリ形式の実行計画ファイルです。
要するに、Plan内容を凍結しておき、後のApplyに備えるという仕組みです。
https://developer.hashicorp.com/terraform/cli/commands/plan#other-options

-out=FILENAME - Writes the generated plan to the given filename in an opaque file format that you can later pass to terraform apply to execute the planned changes, and to some other Terraform commands that can work with saved plan files.

従来のTerraform運用

私たちのTerraformはモノレポ構成で、以下のようにシンプルなCI/CDで運用していました。

  • PR作成をトリガーにterraform planを自動実行(CI)
  • Plan結果はPRコメントに出力
  • Applyはworkflow_dispatchによる手動起動(CD)
  • 実行時にはマージ元ブランチ名を入力パラメータとして指定

手動起動CDのポイントとしては、Plan時には検知できないエラー(IAMロールとポリシーの依存関係など)がApply時には発生するケースがあるので、常にApply成功済みのクリーンな状態でマージしたい意図がありました。しかし、こちらの構成では、「レビューされた内容と、実際にApplyされる内容が異なるリスク」が内包されています。例えば...

  1. AさんがPRを作成 → CIでPlan実行 → 差分が表示される
  2. その間にBさんがCDを介して別のApplyを実行 → tfstateが更新される
  3. AさんがApplyすると、Plan結果と異なるリソース変更やエラーが発生

このような背景から、「Planファイルを保存し、Apply時は必ずそのPlanファイルを使う」というGitHub Actionsワークフローを再設計し始めました。

最新の運用

先ずは、実践上の重要ポイントを抑えておきます。

  • Planファイルはアーティファクト経由で渡す
  • 1つのPlanファイルにつき、1回のApply(=1回のワークフロー起動)
  • GitHub Actionsワークフローがまたがるため、ファイル取得元ワークフローのrun-idが必要

要するに、ワークフローの実行環境は一回限りの使い捨てで、CIとCDは別部署なので、Planを届けるには「アップロードして保存 → run-idを使って取り寄せる」仕組みが必要。run-idは言わば伝票番号、これがないと倉庫にアクセスできません。(by ChatGPT)

以上を踏まえて、Planファイルの具体的な運用方法を紹介します。

CI

ステップ1. Planファイルを出力する

- name: terraform plan
  working-directory: terraform/develop
  run: |
    terraform plan -input=false -out="plan.tfplan"

ステップ2. アーティファクトとして保存

- name: Upload plan file
  uses: actions/upload-artifact@v4
  with:
    path: terraform/develop/plan.tfplan

CIワークフローの実行ログからアーティファクトIDが出力されたら、保存成功です。

CD

ステップ1. 手動起動イベント

on:
  workflow_dispatch:
    inputs:
      ARTIFACT_NAME:
        required: true
        type: string
      ARTIFACT_ID:
        required: true
        type: string
env:
  AWS_REGION: ap-northeast-1
  ARTIFACT_ID: ${{ inputs.ARTIFACT_ID }}

ステップ2. アーティファクトIDからrun-idを取得

- name: Get run_id
  id: get-run-id
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    RESPONSE=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
      "https://api.github.com/repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID")

    # run.idを抽出
    RUN_ID=$(echo "$RESPONSE" | jq -r '.workflow_run.id')

    # run_idをoutputとして登録
    echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"

ステップ3. アーティファクトをダウンロードする

- name: Download artifact
  uses: actions/download-artifact@v4
  with:
    name: ${{inputs.ARTIFACT_NAME}}
    run-id: ${{ steps.get-run-id.outputs.run_id }}
    github-token: ${{ secrets.GITHUB_TOKEN }}
    path: terraform/develop

ステップ4. Apply実行

- name: Terraform Init
  working-directory: terraform/develop
  run: terraform init -input=false

- name: Terraform Apply
  working-directory: terraform/develop
  run: terraform apply plan.tfplan

まとめ

ここで紹介した構成はあくまで一例であり、すべてのチーム・すべてのプロジェクトにとっての万能解ではありません。Terraformの構成(ディレクトリ設計、モジュールの分割方針)や、チームの開発フローによって最適なパイプライン設計は異なってきます。

そして何より、最初から完璧な構成を目指さなくても大丈夫です。まずはシンプルに始めてみて、実際の運用の中で課題が見えてきたタイミングで設計を見直す、というサイクルも十分に有効です。

Filmarks Engineering Blog

Discussion