CTS-KB

Terraform IaC 実践ガイド:CI/CD パイプライン編 — GitLab 動的 child pipeline と WIF キーレス認証

⏱ 約 12 分で読めます
#Terraform #IaC #GitLab CI #CI/CD #child pipeline #動的パイプライン #monorepo #WIF #キーレス認証 #GCP #AWS

⚙️ なぜ Terraform を CI/CD で動かすのか

本記事はシリーズ 「Terraform IaC 実践ガイド」第 4 回(CI/CD パイプライン編) です。第 1 回 概要 と第 2 回 準備編 で扱った設計判断を、 「Git にマージされたコードだけが本番に適用される」自動化 に落とし込みます。

ローカルから terraform apply を叩く運用には、以下の致命的な弱点があります。

  • レビュー不能:誰が・いつ・何を変えたかが残らない
  • 資格情報の偏在:本番権限を個々人の端末に持たせざるを得ない
  • State drift の温床:手元の state と本番が常にズレるリスク
  • 属人化:「あのスタックは○○さんしか触れない」状態を生む

CI/CD で運用すれば、これらは構造的に解消できます。

🎯 目指すパイプラインの全体像

🎯 目指すパイプラインの全体像

approve

deploy

MR 作成

変更スタック検出

plan ジョブを動的生成

plan 結果を MR にコメント

レビュー & 承認

main へ merge

apply ジョブを動的生成

手動承認

terraform apply

結果を Slack / GitLab 通知

本パイプラインの設計原則は 3 つに集約されます。

  1. plan と apply を分離する:MR 段階では plan のみ、merge 後に apply
  2. 変更があったスタックだけを実行する:差分検出による動的ジョブ生成
  3. 資格情報を CI に置かないWIF(Workload Identity Federation) でキーレス認証

🔐 認証はキーレス(WIF)一択

第 2 回 準備編 で述べたとおり、Terraform 用のサービスアカウントキーは GitLab Variables にも置きません 。GitLab CI の id_tokens キーワードで OIDC ID トークンを取得し、GCP の WIF Pool が短命の access token に交換します。

.terraform_base:
  image:
    name: hashicorp/terraform:1.10
    entrypoint: [""]
  id_tokens:
    GCP_ID_TOKEN:
      aud: https://iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/terraform-pool/providers/gitlab-saas
  before_script:
    - echo "$GCP_ID_TOKEN" > /tmp/id-token.jwt
    - |
      cat <<EOF > /tmp/cred.json
      {
        "type": "external_account",
        "audience": "//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/terraform-pool/providers/gitlab-saas",
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "token_url": "https://sts.googleapis.com/v1/token",
        "credential_source": { "file": "/tmp/id-token.jwt" }
      }
      EOF
    - export GOOGLE_APPLICATION_CREDENTIALS=/tmp/cred.json

WIF Pool の作成手順・attribute-condition の最小権限設計(namespace_id で自社グループ限定、refs/heads/main でブランチ限定 等)は、独立記事 WIF でキーレス認証 で詳しく扱っています。 本記事はその上に組み上げる「IaC 自動化レイヤ」の設計図 です。

impersonation あり / なし — 重要な設計分岐

WIF の認証経路には、中継となるサービスアカウント(Principal SA)を経由するか否かという重要な設計分岐があります。これが社内で言う サービスアカウント impersonation の「あり / なし」です(どちらも対象 SA のトークンを借りる点は同じで、違いは中継 SA を挟むか)。本基盤の bootstrap 認証スタック認証は、この使い分けの上に成り立っています。

方式経路使いどころ
impersonation なし(基本)WIF Provider → 目的の SA を直接借りる(1 段)単一プロジェクト内で完結。経路が短く最小権限・監査が容易。まずこちらを選ぶ
impersonation ありWIF Provider → 中継 SA → 目的の SA(2 段)複数プロジェクトを横断して権限を借りる必要があるとき

選定指針: 原則は impersonation なし(中継 SA なしの 1 段)。中継を挟むほど権限委譲の経路が伸び、追跡と最小権限の維持が難しくなります。impersonation あり(2 段)は「1 つの入口から複数プロジェクトの SA を使い分けたい」など、横断が本当に必要な場合に限定します。bootstrap 段(設定取り出し)とスタック段(Terraform 実行)で、どちらを採るかを意図的に分けるのが設計のキモです(経路の実装はスクリプトに閉じます)。

マルチクラウド時の認証

GCP と AWS の両方を 1 本のパイプラインで管理する場合、 GCP のサービスアカウントが発行する OIDC トークン で AWS の IAM Role を AssumeRole します。GitLab → GCP → AWS と直列で連鎖させることで、 AWS アクセスキーも CI に置かない 構成が成立します。

GitLab Runner
  └→ GCP WIF(OIDC)→ Google Cloud に認証
      └→ GCP SA の OIDC トークン
          → AWS STS AssumeRoleWithWebIdentity
              → AWS リソースに認証

🧩 動的 child pipeline 生成 — モジュール管理 Infra CI/CD の核心

本パイプラインで最大の革新がここです。スタック数が増えても、変更があったスタックだけを自動検出して plan / apply する仕組みを、GitLab の child pipeline(子パイプライン) の動的生成で実現します。

あなたの .gitlab-ci.yml、「秘伝のタレ」になっていませんか?

その .gitlab-ci.yml、いつの間にか数千行〜数万行に育っていませんか?

動的生成を使わない場合、新しいクライアント・スタックを追加するたびに、.gitlab-ci.ymlrules: の条件分岐を足していく運用になります。これ自体は素直なやり方で、スタックが少ないうちは十分機能します。ただしスタックが数十〜数百規模に育つと、いくつかの課題が効いてきます。

  • スタックが増えるほど .gitlab-ci.yml が条件分岐で線形に長くなり、誰も全体を追いきれない「秘伝のタレ」状態に近づく
  • 全スタック走査だと CI 時間が線形に伸び、無関係な plan の差分が MR レビューのノイズになる
  • 設定の維持が特定メンバーに依存しやすくなる(属人化)

これは「書いた人が悪い」のではなく、静的記述というアプローチがスケールで頭打ちになるだけの話です。動的生成は、この「スケール時の保守性」を構造的に解決する手段になります。

GitLab 公式も「動的 child pipeline」を位置づけている

これは奇策ではありません。GitLab 公式ドキュメントが、ジョブ内で生成した YAML から子パイプラインをトリガーする手法を正式に位置づけています。

“You can trigger a child pipeline from a YAML file generated in a job, instead of a static file saved in your project.” “This can be very powerful for generating pipelines targeting content that changed …”GitLab Docs: Dynamic child pipelines

公式は Jsonnet 等のテンプレート言語による生成例を示しています。本基盤はこれを 「変更検出 → 子パイプライン生成」を Python スクリプトで実装する形に発展させ、.gitlab-ci.yml 本体は薄いまま、無限にスタックを足せる構成にしています。新規スタックを追加しても .gitlab-ci.yml には一切手を入れません。

変更検出のロジック

変更検出は、main との差分からどのスタックが影響を受けたかを判定するスクリプトで行います。判定方針(=設計のキモ)は次の 2 点です。

  • environments/<env>/<stack>/ 単位 でジョブを切る(State の単位とそろえる)
  • modules/ 配下が変わった場合は全スタック対象 に切り替える(影響範囲が読めないため安全側に倒す)

この「差分 → 対象スタック集合」を求める判定ロジックをスクリプト化し、次ステップのジョブ生成に渡します(スクリプトの実装そのものはプロジェクト内に閉じます)。

parent-child pipelines で動的にジョブを発行

GitLab CI の trigger:include: を使って、変更スタック分の YAML を動的生成し、子パイプラインとして実行します。

generate-pipeline:
  stage: prepare
  script:
    - ./scripts/generate-pipeline.sh > generated-pipeline.yml
  artifacts:
    paths: [generated-pipeline.yml]

trigger-children:
  stage: pipeline
  needs: [generate-pipeline]
  trigger:
    include:
      - artifact: generated-pipeline.yml
        job: generate-pipeline
    strategy: depend

子パイプライン側には、変更されたスタック数だけ plan / apply ジョブが動的に生成されます。

strategy: depend の意味: GitLab 公式仕様では、これは「子パイプラインの artifact reports(test report 等)を親パイプラインの MR widget に表示する 」ためのオプションです。指定しないと、子で生成した plan サマリ等の成果物が親 MR の差分ビューに反映されません。「親が子の完了を待つ」副次効果も付随します。

🧱 この基盤を支える 4 つのコンポーネント

本パイプラインの実体は、役割を絞った 4 つの小さなスクリプトの組み合わせです。巨大な .gitlab-ci.yml の代わりに、この 4 つが疎結合に連携します(各スクリプトの実装はプロジェクト内に閉じ、ここでは役割のみ示します)。

コンポーネント役割効果
bootstrap 認証CI の最初の入口。bootstrap の WIF で Secret Manager にだけアクセスし、各スタックの設定を取り出す鍵ゼロで「設定の取り出し口」を一本化
スタック認証スタックごとの WIF で、その環境の Terraform 実行権限へ昇格スタック単位の最小権限・キーレス
クロスクラウド認証GCP の認証を起点に AWS STS へ連鎖し、AWS リソースも鍵なしで操作GCP/AWS をまたいでも長寿命キーゼロ
動的パイプライン生成変更スタックを検出し、子パイプラインの YAML を生成して発行スタックが増えても .gitlab-ci.yml は薄いまま

この 4 点が噛み合うことで、「単一リポジトリ・無数のスタック・マルチクラウド・キーレス・.gitlab-ci.yml は最小」 が同時に成立します。新しいクライアントやサービスを追加するときに触るのは モジュールと environments/ のディレクトリだけ。CI 設定本体は不変です。

導入を検討する CI/CD 担当者へ: この構成の価値は、機能の派手さではなく 「スケールしても CI 設定が肥大化しない」 一点に集約されます。スタック数が 3 桁になっても、パイプラインの保守コストはほぼ一定に保てます。.gitlab-ci.yml の維持が重くなってきたなら、動的 child pipeline + キーレス認証の 4 コンポーネント構成が一つの選択肢になります。

🛠️ plan / apply の分離

ステージトリガージョブ失敗時の挙動
planMR 作成・更新時terraform plan -out=plan.tfplanMR にコメントして失敗扱い
applymain へ merge 後terraform apply plan.tfplanパイプライン停止、Slack 通知
plan:
  extends: .terraform_base
  stage: plan
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - cd terraform/environments/${ENV}/${STACK}
    - terraform init
    - terraform plan -out=plan.tfplan -detailed-exitcode
  artifacts:
    paths:
      - terraform/environments/${ENV}/${STACK}/plan.tfplan
    expire_in: 1 day

apply:
  extends: .terraform_base
  stage: apply
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  when: manual
  environment:
    name: ${ENV}/${STACK}
  script:
    - cd terraform/environments/${ENV}/${STACK}
    - terraform init
    - terraform apply -auto-approve plan.tfplan
  needs:
    - job: plan
      artifacts: true

-detailed-exitcode の活用

terraform plan -detailed-exitcode は次の 3 値を返します。

終了コード意味パイプライン挙動
0差分なしapply を skip
1エラーパイプライン失敗
2差分ありapply 候補

これを使うと 「差分が無いのに apply される」事故 を構造的に防げます。

🛡️ 安全装置

機構目的実装
state lock同時 apply の防止GCS バックエンドのデフォルト機能で自動
手動承認本番 apply のヒューマンゲートwhen: manual + environment:
環境ごとの保護ブランチmain 直 push 禁止GitLab Project Settings
レビュー必須化単独 merge を禁止MR Approval Rules(最低 1 名)
plan artifact の TTL古い plan の流用防止expire_in: 1 day
CODEOWNERS重要スタックの自動レビュアー指定terraform/environments/prod/* @sre-team

同時 apply を防ぐ理由

GCS バックエンドは state lock をネイティブサポートしているため、複数ジョブが同じ state に対して apply するとロック競合で 1 本目以外が即失敗します。これに加えて GitLab の resource_group: を併用すると、 「ロック取得失敗による無駄なリトライ」 を CI レベルで抑止できます。

apply:
  resource_group: ${ENV}/${STACK}  # 同一スタックの apply は直列化

環境保護

environment:
  name: prod/my-app
  deployment_tier: production

deployment_tier: production を宣言しておくと、GitLab UI 上で本番デプロイを視覚的に区別でき、 誤って staging を本番として扱うミス を減らせます。

🔄 マルチスタック・マルチクラウドのパイプライン

GCP・AWS のスタックを 1 本のパイプラインに同居させる際は、 State バックエンドを GCS に統合した上で準備編 参照)、ジョブ単位で必要なクラウドの認証だけをロードする構成が運用しやすくなります。

AWS スタック向けのジョブでは、前掲の クロスクラウド認証コンポーネントが働きます。GCP の認証で得た OIDC トークンを起点に AWS STS へ連鎖させ、短命の AWS 一時クレデンシャルだけを取得してから Terraform を実行します(連鎖の実装はスクリプトに閉じます)。

要点:

  • AWS 単独スタックは AWS 認証を組み込んだジョブテンプレートを extends: する
  • AWS アクセスキーを CI Variables に持たない(取得するのは短命の一時クレデンシャルのみ)
  • セッション名に パイプライン ID・スタック名を含めることで CloudTrail 追跡が容易

🚫 ありがちな失敗

失敗何が起きるか対策
terraform applyscript: に直書きレビューなしで本番反映される事故必ず plan / apply を分離し、apply は when: manual
state ファイルをローカルに置くチーム間で State が分散リモートバックエンド(GCS / S3)必須
local-execgcloud を呼ぶプロビジョナがべき等性を失い、再実行不能リソースは Terraform Provider で表現
count でスタックを切り替える環境差分が tfstate に残り State migration 不能環境はディレクトリで分け、environments/<env> 単位の State
同時 apply の競合state lock 取得失敗で CI が大量失敗resource_group: で直列化
terraform fmt / validate を CI に入れないフォーマット崩れと簡易ミスが merge されるpre-commit + CI の両方で実行
plan 結果を MR にコメントしないレビュアーが影響範囲を把握できないgitlab-terraform 等で plan サマリを自動コメント

📝 CTS-EC での運用

CTS-EC では、本構成を以下のように運用しています。

  • GitLab CI 1 本 で GCP・AWS の全スタックを管理
  • 認証は WIF(GitLab → GCP → AWS)の連鎖、長寿命キーゼロ
  • staging は MR merge で自動 apply、 prod のみ when: manual
  • resource_group: でスタック単位の apply を直列化
  • terraform plan の結果を MR コメント Bot で自動投稿し、レビュアーが差分を即時確認

これにより、 「インフラ変更が PR 上で完結」「本番反映は人間ゲート」「鍵漏洩リスクゼロ」 の 3 点が成立しています。

🎯 まとめ

やめるべき運用置き換え先
ローカルから terraform applyGitLab CI で plan / apply を分離
サービスアカウントキーWIF(Workload Identity Federation)
数万行に育つ .gitlab-ci.yml(秘伝のタレ化)動的 child pipeline で CI 設定を薄く保つ
全スタック走査動的パイプラインで変更分のみ実行
自動 apply(無条件)本番のみ when: manual のヒューマンゲート
State の各クラウド散在GCS バックエンドへ統合

Terraform は「書く」より「運用する」フェーズの設計が事故を分けます。CI/CD パイプラインを キーレス・動的・分離 の 3 原則で組み上げれば、IaC は安全な日常運用に乗ります。とりわけ 動的 child pipeline 生成は、スタックが何百に増えても CI 設定を肥大化させない——スケールしても保守コストが一定という、モジュール管理 Infra CI/CD の決定的な差になります。

📚 シリーズ記事

#タイトル内容
1概要マルチクラウド構成の全体像と AI 駆動運用
2準備編アカウント設計・WIF 認証・bootstrap・State 管理
3モジュール設計編再利用可能なモジュールの設計原則
4CI/CD パイプライン編(本記事)動的 child pipeline・WIF キーレス認証・GCP↔AWS
5運用編State drift 解消・障害対応・AI 運用ループ

関連記事

関連用語

外部資料