CI/CD

TerraformのCI導入ガイド|fmt・validate・planを自動化するパイプライン設計とツールの選び方

TerraformのCI導入ガイド|fmt・validate・planを自動化するパイプライン設計とツールの選び方

TerraformのCIは、プルリクエストのたびに整形・構文・静的解析・planを自動で流し、レビューと適用の手順を個人の端末から切り離す仕組みです。本記事では2026年10月時点の最新版 Terraform 1.16.4 を実際に動かして終了コードやファイルの中身を確かめ、チェックを置く順序、CI環境で付けるべき設定、planファイルの扱い、同時実行の直列化、ドリフト検知、CIツールの選び方までを順に整理します。

AWSへのOIDC接続とapplyまでを含むGitHub Actionsの具体的な構築手順は、GitHub ActionsとTerraformでAWSのCI/CDを構築する手順(OIDC・S3ネイティブロック対応)で扱っています。本記事はクラウドやCIツールに依存しない設計の判断に絞ります。

まとめ:TerraformのCIで自動化する順序と設計の要点

  • 最初に入れるのは認証不要の静的チェック:terraform fmt -check・init -backend=false・validate・TFLintは、クラウド権限もstateも要らず1ジョブで完結します。
  • planは別ジョブにして権限をそこだけに渡す:planはクラウドAPIとstateを読みます。フォークからのプルリクエストでは自動実行しません。
  • CI用の設定は3つ:TF_INPUT=0(入力待ちで止めない)、TF_IN_AUTOMATION(出力の案内文を抑える)、-lockfile=readonly(ロックファイルをCIで書き換えない)。
  • 保存したplanファイルは機密情報:中身はzipで、stateと設定と変数が入っています。成果物として受け渡すなら保持期間と閲覧権限を絞ります。
  • 同じstateに触るジョブは直列化する:GitHub Actionsはconcurrency、GitLab CIはresource_groupを使います。
  • 定期planでドリフトを拾う:plan -detailed-exitcodeの終了コード2は「差分あり」です。コンソールでの直接変更はCIだけでは見つかりません。
  • CIツールはリポジトリのある場所で決める:TerraformとCIツールは代替関係ではありません。Terraform専用の実行基盤(HCP Terraform・Atlantis)が要るかは別に判断します。

CIで防げるTerraformの事故と、CIだけでは防げない変更

Terraformを手元の端末から実行していると、事故は「誰が・どの版で・どのコードを」適用したかが曖昧なところから起きます。未コミットの変更を含んだままapplyする、レビューで見たplanと実際のapplyの間にmainが進んでいる、担当者ごとにTerraformやプロバイダの版が違ってstateの形式が上がる、といった具合です。CIは実行場所を1か所に固定し、mainのコードを、固定した版で、ログの残る場所から実行することでこれらを潰します。

レビューの質も変わります。planの出力がプルリクエストに付けば、レビュアーはHCLの差分ではなく「何が作られ、何が置き換わるか」を見て判断できます。削除や置換を含むplanを機械的に止める仕組みはterraform planの差分の読み方|init・apply・destroyの安全手順で解説しています。

一方、CIが見るのはリポジトリに入った変更だけです。クラウドのコンソールやCLIで直接加えた変更(ドリフト)は、次に誰かがplanを流すまで表に出ません。CI導入と同時に、後述の定期planを置いてください。

TerraformのCIパイプラインに置くチェックと実行順

チェックは「速く、権限が要らないもの」から並べます。前段で落ちればクラウドAPIを呼ばずに済み、権限を渡すジョブも減ります。

段階 コマンド 失敗する条件 クラウド認証
整形 terraform fmt -check -recursive 要整形のファイルあり(終了コード3) 不要
構文 terraform init -backend=false→validate 構文・参照の誤り 不要
静的解析 tflint --recursive ルール違反 不要
テスト terraform test assertの不成立 mock次第
差分 terraform plan -out=tfplan エラー(差分ありは成功) 読み取り
適用 terraform apply tfplan エラー 書き込み

Terraform 1.16.4で確かめると、fmt -checkは整形が必要なファイル名を出力して終了コード3を返し、CIのステップはそこで失敗します。CIでfmtを修正モードで走らせて自動コミットする構成もありますが、レビュー中のブランチにCIが書き込むと差分の出どころが分かりにくくなります。CIは検査だけにし、修正はpre-commitで手元に寄せる方が運用は単純です。書式と命名のルールそのものはTerraformコーディング規約(スタイルガイド)|fmt・命名規則・ディレクトリ構成の統一ルールにまとめています。

認証なしで回せる静的チェックのジョブ

fmt・validate・TFLintはstateもクラウドAPIも使いません。initに-backend=falseを付けるとバックエンドへの接続を省けます(プロバイダのダウンロードは行われます)。GitHub Actionsで書くと次の形になり、actionlint 1.7.12でエラーが出ないことを確認しています。ブランチ保護の必須チェックに指定する前提なので、pathsによる絞り込みは付けていません。絞り込みでワークフローが起動しなかったプルリクエストでは、必須チェックが「Pending」のまま残りマージできなくなるためです。

name: terraform-ci
on:
  pull_request:
permissions:
  contents: read
env:
  TF_IN_AUTOMATION: "1"
  TF_INPUT: "0"
jobs:
  static:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: 1.16.4
          terraform_wrapper: false
      - run: terraform fmt -check -recursive
      - run: terraform init -backend=false -lockfile=readonly
      - run: terraform validate
      - uses: terraform-linters/setup-tflint@v6
        with:
          tflint_version: v0.64.0
      - run: tflint --init
      - run: tflint --recursive --format compact

hashicorp/setup-terraformは既定で、terraformコマンドの標準出力・標準エラー・終了コードをステップの出力として取り込むラッパーを入れます。終了コードをシェルで直接扱うジョブではterraform_wrapper: falseにしておくと挙動を追いやすくなります。TFLintのルールセット導入はTFLintとは?Terraform静的解析の導入・設定・CI組み込み【v0.64.0対応】、terraform testをどこまでCIに載せるかはterraform testの書き方|unit/integrationの分け方とCIコストで扱っています。

クラウド権限を要するplanジョブの分け方

planはリフレッシュのためにクラウドAPIを読み、stateを読み取ります(stateロックを取るため、バックエンドにはロック用の書き込みも発生します)。静的チェックと同じジョブに権限を渡すと、書式を直すだけのプルリクエストにもクラウドの読み取り権限が付きます。planは別ジョブにしてneedsで静的チェックの後に置き、OIDCのid-token: writeはそのジョブにだけ付けます。

公開リポジトリではフォークからのプルリクエストに注意してください。GitHub Actionsは、フォークから発火したワークフローにGITHUB_TOKEN以外のシークレットを渡しません。これを避けようとしてpull_request_targetでプルリクエストのコードをチェックアウトしplanを流すのは危険です。planの時点でdata "external"のように外部プログラムを実行するデータソースが評価されるため、第三者が書いたコードが特権付きのジョブで動きます。フォークのプルリクエストではplanを自動実行せず、静的チェックだけを回す設計にします。

CI環境で付けるTerraform固有の設定

入力待ちを止めるTF_INPUTと出力を整えるTF_IN_AUTOMATION

CIのジョブには入力端末がありません。値の決まっていない変数があると、Terraformは入力を求める文を出したうえで、標準入力の状態しだいで即座に失敗したり、入力待ちのまま止まったりします。-input=falseを付けるか、環境変数TF_INPUT=0を設定すると、コマンドは入力を求めなくなります。値が足りない場合だけ「No value for required variable」で終了し、ログから原因が分かります。

TF_IN_AUTOMATIONは空でない値を入れると、「次はこのコマンドを実行してください」といった案内を出力から外します。公式ドキュメントは「purely cosmetic」(見た目だけの変更)と説明しており、動作は変わりません。CIのログを読む人が案内どおりに手元でapplyしてしまう事故を避ける目的で設定します。

.terraform.lock.hclのプラットフォーム別ハッシュと-lockfile=readonly

CIではterraform init -lockfile=readonlyを使います。ロックファイルに記録されたチェックサムで検証はしつつ、ファイル自体は書き換えません。プロバイダの更新はプルリクエストで人が行い、CIが勝手に版を上げる余地をなくせます。なお、この指定は-upgradeと併用できず、1.16.4では「The -upgrade flag conflicts with -lockfile=readonly.」というエラーになります。

注意したいのはプラットフォーム別のハッシュです。initは、実行した環境(たとえば開発者のmacOS)のパッケージに対するh1:ハッシュだけを追記します。Linuxで動くCIのためのハッシュが無いと、検証自体はレジストリのzh:ハッシュで通るものの、プロバイダのキャッシュが効きません。1.16.4で確かめると、ロックファイルに実行環境のh1:が無い状態ではプラグインキャッシュを使わずに毎回ダウンロードし、-lockfile=readonlyなのでハッシュも追記されないままでした。CIの実行環境を含めてハッシュを揃えておきます。

terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64 \
  -platform=darwin_amd64 \
  -platform=windows_amd64

プロバイダのダウンロードを減らすプラグインキャッシュ

AWSプロバイダのように大きなパッケージを毎回ダウンロードすると、initだけで時間を使います。環境変数TF_PLUGIN_CACHE_DIRにディレクトリを指定し(Terraformはこのディレクトリを作らないため、init前にmkdir -pで作っておきます)、CIのキャッシュ機能でそのディレクトリを保存すれば、2回目以降は「from the shared cache directory」として再利用されます。上のproviders lockでCI環境のh1:が揃っていることが前提です。plugin_cache_may_break_dependency_lock_fileという設定でロックファイルとの照合を緩める方法もありますが、名前のとおりロックファイルが不完全になり得るため、CIではハッシュを揃える方を選びます。

保存したplanファイルをCIで受け渡すときの制約と機密情報

レビューで見たplanと同じ内容を適用するには、terraform plan -out=tfplanで保存したファイルをterraform apply tfplanに渡します。planジョブとapplyジョブを分けるなら、このファイルを成果物(artifact)として受け渡すことになります。

1.16.4で作ったplanファイルはzip形式で、中にはtfplanのほか、tfstate・tfstate-prev・tfconfig/配下の設定ファイル・.terraform.lock.hclが入っていました。公式の自動化ガイドも、planファイルには設定とstateと変数の完全なコピーが含まれると明記しています。stateにはデータベースのパスワードなどが平文で入るため、planファイルの扱いはstateと同じ水準にします。

  • 成果物の保持期間を短くし、閲覧できる人をstateの閲覧者と揃える。
  • プロバイダの認証情報は、Terraformの入力変数(TF_VAR_で渡すものを含む)ではなく、プロバイダが読む認証用の環境変数やOIDCで渡す(planファイルに入らない)。
  • applyは同じOS・CPUアーキテクチャ、同じTerraformとプロバイダの版で実行する。版が違うとapplyはエラーになります。
  • applyジョブでもリポジトリをチェックアウトしてinitし直す。planファイルには設定外で参照するファイルやプロバイダ本体は入らず、子モジュールの絶対パスが記録されることもあるため、作業ディレクトリのパスも揃えます。
  • planの後に別のapplyでstateが進んだ場合、古いplanは「Saved plan is stale」で拒否されます。この場合はplanからやり直します。

最後の挙動は安全側の仕組みですが、プルリクエストを複数続けてマージする運用では頻繁に起きます。その場合はmainの最新でplanを作り直し、その保存planを承認(GitHubのEnvironmentsのRequired reviewersなど)にかけてから適用します。承認を挟まずに作り直したplanをそのまま適用すると、レビューした内容と適用内容がずれます。次の直列化と組み合わせて設計してください。

同じstateに触るジョブの直列化とstateロック

stateロック(S3のuse_lockfileなど)は、同時に走ったapplyの片方をエラーで止めます。データの破損は防げますが、止まったジョブは失敗として残ります。CI側でも同じstateに触るジョブを1本ずつ流すようにしておくと、ロック待ちの失敗を減らせます。S3バックエンドのロック設定はTerraform stateとは?tfstateの構造とS3バックエンド・移動削除の安全手順を参照してください。

GitHub Actionsのconcurrencyと待ち行列の上限

GitHub Actionsではconcurrency.groupを同じ値にしたジョブが同時に1本しか走りません。ただし既定では待機できるのも1本だけで、新しいジョブが来ると待機中のジョブは取り消されます。mainへのマージが3回続くと、2回目のapplyジョブは実行されずに取り消されます。3回目のapplyがmainの最新でplanし直す構成なら2回目の変更もそこに含まれますが、プルリクエストごとの保存planを適用する構成では、2回目の変更は誰かがplanし直すまで適用されないまま残ります。

2026年5月7日の更新でqueue: maxが加わり、同じグループで最大100件まで待機させて順番に処理できるようになりました。cancel-in-progress: trueとは併用できません(ワークフローの検証エラーになります)。追加が新しいため、actionlint 1.7.12はqueueを未知のキーとしてエラーにしました。リンターを使っている場合は版の対応を確認してください。

concurrency:
  group: terraform-prod
  queue: max

applyのジョブを途中で取り消すとstateのロックが残ったり、作りかけのリソースが残ったりします。Terraformのジョブではcancel-in-progressを有効にしないでください。

GitLab CIのresource_groupによる直列化と実行順

GitLab CIは既定でパイプラインを並行に実行します。applyのジョブにresource_group: productionのように指定すると、同じリソースグループのジョブは1本ずつ実行されます。実行順はprocess_modeで選べ、既定のunorderedは準備できたジョブから処理します。oldest_firstに変えると、パイプラインIDの小さい順に処理します(変更はAPIで行います)。mainへのマージごとに作られるパイプラインでapplyする構成なら、これでマージ順に近い順序になります。

定期実行のplanでドリフトを検知する方法

terraform plan -detailed-exitcodeは、差分なしで0、エラーで1、差分ありで2を返します。1.16.4で確かめても、変更を入れた設定では2、apply直後では0でした。これを平日朝などの定期実行に載せ、2が返ったら通知すれば、コンソールでの直接変更に気付けます。ただし2は「適用すべき差分がある」という意味で、外部変更だけを示す値ではありません。mainにマージ済みで未適用のコードも2になり、ignore_changesで無視している属性の変化は通知されません。コードの差分を除いて実環境とstateのずれだけを見たいときは-refresh-only付きのplanを使います。

name: terraform-drift
on:
  schedule:
    - cron: "0 0 * * 1-5"
  workflow_dispatch:
permissions:
  contents: read
  id-token: write
  issues: write
concurrency:
  group: terraform-prod
  queue: max
env:
  TF_IN_AUTOMATION: "1"
  TF_INPUT: "0"
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: hashicorp/setup-terraform@v4
        with:
          terraform_version: 1.16.4
          terraform_wrapper: false
      # ここにクラウド認証(OIDC)のステップを置く
      - run: terraform init -lockfile=readonly
      - id: plan
        run: |
          set +e
          terraform plan -no-color -lock-timeout=5m -detailed-exitcode > plan.txt
          ec=$?
          set -e
          if [ "$ec" -ne 0 ] && [ "$ec" -ne 2 ]; then exit "$ec"; fi
          echo "exitcode=$ec" >> "$GITHUB_OUTPUT"
      - if: steps.plan.outputs.exitcode == '2'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          grep -E '^  # |^Plan:|^Changes to Outputs:' plan.txt > summary.txt || [ $? -eq 1 ]
          [ -s summary.txt ] || echo "差分あり(詳細はジョブのログを確認)" > summary.txt
          gh issue create --title "Terraform drift $(date -u +%F)" --body-file summary.txt

GitHub Actionsのrunは既定でbash -e相当のため、終了コード2で即座にステップが失敗します。set +eでいったん受け止め、0と2以外(エラーや強制終了)はそのまま失敗させています。出力値(output)だけが変わった場合も終了コードは2ですが、リソースの行が無いためgrepが一致なしの1を返します。1.16.4で確かめるとplan出力には「Changes to Outputs:」の行が出るので、これも抽出対象に入れ、一致なしは失敗扱いにしていません。cronはUTCなので0 0 * * 1-5は日本時間の平日9時になります。Issueに載せるのはリソースのアドレス行とPlan:の集計行、出力値の見出し行だけにしています。plan全文には属性値が出るため、公開リポジトリのIssueにそのまま貼らないでください。concurrencyのグループをapplyと揃えておくと、適用中にドリフト検知が走って誤検知することもありません。apply側と同じくqueue: maxを付けているのは、既定の動作だと後から来たドリフト検知が待機中のapplyを取り消してしまうためです(このキーは前述のとおりactionlint 1.7.12ではエラーになります)。

TerraformのCIに使うツールの選び方

「CircleCI vs Terraform」「GitLab CI vs Terraform」のように比べられることがありますが、両者は代替関係ではありません。Terraformはインフラを定義して適用するツールで、CIツールはそのコマンドをいつ・どこで実行するかを決める仕組みです。選ぶべきは「どのCIツールでTerraformを動かすか」と「Terraform専用の実行基盤を使うか」の2点です。

選択肢 Terraformの導入方法(2026年10月時点) 向く条件
GitHub Actions hashicorp/setup-terraform@v4(v4.0.1) GitHubでコード管理
GitLab CI/CD Terraform用テンプレートは18.0で削除 GitLabでコード管理
CircleCI circleci/terraform orb(3.7.0) 既にCircleCIを運用
Atlantis セルフホスト(v0.48.0) PRコメントで操作したい
HCP Terraform SaaS(管理リソース500まで無料) stateと実行を任せたい

まずはリポジトリのある場所のCIを使います。GitHubならGitHub Actions、GitLabならGitLab CI/CDです。別のCIを足すと、認証の設定とシークレットの管理場所が2か所に増えます。

GitLabは事情が変わっています。Terraform.gitlab-ci.ymlなどのTerraform用CI/CDテンプレートはGitLab 16.9で非推奨が告知され、18.0で削除されました。BSLライセンスに移ったTerraformのバイナリをジョブ用イメージ(terraform-images)で更新できないことが理由で、現在の公式ドキュメントは代わりにOpenTofu用のCI/CDコンポーネントを案内しています。Terraformを使い続けるなら、Terraformのバイナリを含むイメージを自分で用意して.gitlab-ci.ymlに書きます。OpenTofuへ移る判断材料はOpenTofuとは?Terraformとのライセンス・機能差と移行判断を実装視点で解説にまとめています。

CircleCIのcircleci/terraform orbは、init・plan・applyのジョブ定義をまとめて読み込めます。ただし最新の3.7.0は2025年8月11日の公開で(CircleCIのOrb APIで確認)、それ以降の版は出ていません。orbが固定するTerraformの版を確認し、新しい版が要るならhashicorp/terraformのDockerイメージで自前のジョブを書く方が版を管理しやすくなります。

Terraform専用の実行基盤は、CIツールでは持ちにくい機能を補うものです。プルリクエストのコメントでplanとapplyを操作したいならAtlantis(AtlantisでTerraformをPR運用する構成と権限設計|採用条件を解説)、stateの保管・実行環境・承認フローをまとめて任せたいならHCP Terraform(HCP Terraformとは?旧Terraform Cloudの料金・無料枠とCLI接続の手順)が候補です。どちらもCIツールと併用できます。

CIの導入を進める順番と、applyの自動化を見送る条件

一度にapplyまで自動化すると、権限設計とstateの移行が同時に動いて切り分けが難しくなります。次の3段階で進めると、各段階で戻れる状態を保てます。

  1. 静的チェックだけをプルリクエストに入れる:クラウドの権限設定が要りません。ブランチ保護で必須チェックに指定します(この場合はpathsの絞り込みを外します)。
  2. planをプルリクエストで自動実行し、結果を共有する:stateをリモートバックエンドへ移し、CIからOIDCで引き受けるロールを用意します。権限は管理対象のインフラの読み取りに加え、バックエンドのロック操作(S3のuse_lockfileならロックファイルのs3:PutObject・s3:DeleteObject)が要ります。applyはまだ手元から行います。
  3. mainへのマージでapplyする:書き込み権限をapplyジョブだけに付け、本番には承認(GitHubのEnvironmentsのRequired reviewersなど)を挟みます。

3段階目のapplyの自動化は、全員に必要なわけではありません。管理者が1〜2人でリソースも少なく、適用が月に数回という規模なら、2段階目で止めてapplyは手元から実行する方が、強い権限をCIに置かずに済みます。逆に、本番環境に対して承認なしでapply -auto-approveを流す構成は採りません。公式の自動化ガイドも、破壊的な変更が起こり得る環境では人によるplanの確認を推奨しています。

よくある質問

TerraformのCIとは何をすることですか?

プルリクエストやmainへのマージをきっかけに、terraform fmt -check・validate・TFLint・planを自動で実行し、問題があればマージ前に止めることです。applyまで含めて自動化した場合はCI/CDと呼ばれます。最初は認証の要らない静的チェックから始めるのが安全です。

TerraformのCIはGitHub ActionsとGitLab CIのどちらがよいですか?

コードを置いている側を使います。機能差より、認証とシークレットの管理場所を1か所に保てることの方が効きます。GitLabではTerraform用テンプレートがGitLab 18.0で削除されたため、Terraformのイメージを自分で用意するか、OpenTofuのCI/CDコンポーネントを使います。

CircleCIとTerraformは何が違いますか?

役割が違います。Terraformはインフラの構成をコードで定義して適用するツールで、CircleCIはそのTerraformコマンドを実行する場所とタイミングを決めるCIサービスです。CircleCIでTerraformを動かすにはcircleci/terraform orbを使うか、hashicorp/terraformのDockerイメージでジョブを書きます。

planの結果をプルリクエストにコメントするにはどうしますか?

planを-no-color付きで実行して出力をファイルに保存し、GitHub Actionsならgh pr commentやactions/github-scriptで投稿します。出力が長いとコメントの文字数上限を超えるため、Plan:の集計行とリソースのアドレス行だけを抜き出すか、全文は成果物に置いてリンクを張ります。AWSでの具体例はGitHub ActionsとTerraformでAWSのCI/CDを構築する手順(OIDC・S3ネイティブロック対応)にあります。

CIで使うTerraformの版はどう固定しますか?

設定のrequired_versionで許す範囲を決め、CIのセットアップではterraform_version: 1.16.4のように具体的な版を指定します。プロバイダは.terraform.lock.hclをコミットし、CIでは-lockfile=readonlyで固定します。最新版と更新手順はTerraformの最新バージョンは1.16|terraform versionでの確認とアップグレード手順・新機能で確認できます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!不正アクセスでサービス停止:再開未定の現状と運営側の停止判断・証拠保全・段階的再開
  2. 2025.01.09 コラム アドレサブルCM(Addressable CM)とは?フジテレビで出る表示の意味・仕組みと視聴データの扱い
  3. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  4. 2025.12.10 コラム 今の50代は何世代?バブル世代・団塊ジュニア・氷河期の境目と特徴
  5. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:パスワード変更・対象確認と再開時期(10月23日第2報まで)

RELATED POSTS 関連記事

目次