CodePipeline×CodeBuildでTerraform CI/CD — マルチ環境・承認・OIDC

目次

AWS CodePipeline×CodeBuildで構築するTerraform CI/CD — エンタープライズ運用パターン

AWS×Terraform 複数人開発シリーズ

関連シリーズ(前提知識):

Git/GitHub × Terraform 実践シリーズ(全5弾)も合わせてどうぞ:
Git入門 /
GitHub入門 /
ブランチ戦略 /
セキュリティ /
Terraform実践

1. この記事について

1-1. 本シリーズの位置付け

本シリーズ「AWS×Terraform 複数人開発運用編」は、チームでTerraformを安全・効率的に運用するための3つの柱を段階的に構築してきた。

テーマ主な内容対象
第1弾(公開済み)state管理・lock・drift対策S3+DynamoDBバックエンド・ロック体験・drift検知インフラ・DevOpsエンジニア全般
第2弾(公開済み)PR駆動CI/CD(GitHub Actions)GitHub Actions+OIDC・Plan/Apply Role分離・PRコメント自動投稿GitHub中心のチーム
第3弾(本記事)AWSネイティブCI/CD(CodePipeline)CodePipeline×CodeBuild・マニュアル承認・マルチ環境・監査証跡エンタープライズ・AWSネイティブ派

第2弾ではGitHub Actionsを使ったCI/CDを構築した。これはGitHubを中心に開発しているチームにとって導入ハードルが低く、柔軟性も高い。一方で、企業規模が大きくなるほど「すべての操作をAWS上で管理・監査したい」「GitHub以外のソース管理ツールを使っている」「マルチアカウントで厳密な権限制御が必要」というニーズが生まれてくる。

本記事では AWS CodePipeline × CodeBuild というAWSネイティブな組み合わせでTerraform CI/CDを構築する。CodePipelineはAWSのマネージドCI/CDサービスであり、CloudTrailとの深い連携、マニュアル承認ステージ、マルチアカウント対応など、エンタープライズ要件に応えやすい特性を持つ。

なぜCodePipelineを選ぶのか、3つの理由を示す。

  1. AWSネイティブな監査証跡: すべてのパイプライン操作がCloudTrailに記録され、「誰が・いつ・何をApproveしたか」をAWS上で一元管理できる
  2. マニュアル承認の標準機能: Manual Approval ステージが組み込み機能として提供されており、applyの前に人間の承認を挟む設計が容易
  3. AWS IAMによる細粒度の権限制御: CodePipelineのIAMロール・リソースポリシーで、各ステージの実行権限をAWS標準の方法で管理できる

1-2. GitHub Actions方式 vs CodePipeline方式 — 選択基準

どちらの方式を選ぶべきかは、チームの規模・技術スタック・セキュリティ要件によって異なる。以下の比較表と判断フローチャートを参考に選択してほしい。

比較項目GitHub Actions方式(第2弾)CodePipeline方式(本記事)
管理場所GitHub(ワークフローYAML)AWS(コンソール / Terraform)
ソース管理ツールGitHub専用GitHub / CodeCommit / S3 / Bitbucket等
セットアップ難度低(YAMLファイルを追加するだけ)やや高(CodePipeline/CodeBuild/IAMの設定が必要)
マニュアル承認GitHub Environments(設定が必要)標準機能として組み込み済み
監査証跡GitHub Actions履歴 + CloudTrail(部分的)CloudTrailで全操作を一元記録
IAM制御の細かさGitHub Actionsの権限 + AWS IAM RoleAWS IAMのみで完結(一元管理)
コストGitHub Actionsの実行時間料金CodePipeline実行回数 + CodeBuild実行時間
マルチアカウント対応可能(Cross-account Role設定が必要)可能(Cross-account Action標準サポート)
マルチ環境構成ワークフローの分岐で対応ステージ追加で対応(GUI/Terraform両方)
Slack/SNS通知GitHub Actions + Slack ActionSNS + Lambda / EventBridge標準連携
障害時の再実行ワークフローの「Re-run」ボタンパイプラインの「Retry」ボタン
GitHub依存強い(GitHubがダウンするとCI/CDも停止)弱い(ソース取得のみGitHub依存)
学習コストGitHubユーザーなら低いAWSサービス理解が必要

どちらを選ぶべきか — 判断フローチャート

スタートここから │ ▼Q1: ソース管理はGitHubのみを使うか? │  NO ──┴── YES  │ │  ▼ ▼CodePipeline  Q2: チーム規模は10人以上か、または  セキュリティ監査要件があるか?│NO ──┴── YES│  │▼  ▼ Q3: AWS IAMで CodePipeline 全権限を一元管理  (エンタープライズ向け) したいか?│NO ──┴── YES│  │▼  ▼ GitHub Actions  CodePipeline (シンプル・  (AWSネイティブ  スピード重視)権限管理)

推奨パターンまとめ

GitHub Actions を選ぶ場合:  ✅ スタートアップ・小規模チーム(〜10人)  ✅ GitHub中心の開発フロー  ✅ 素早く導入したい  ✅ Terraform以外のCI/CDも同じ基盤で管理したいCodePipeline を選ぶ場合:  ✅ エンタープライズ・大規模組織(10人〜)  ✅ AWSセキュリティ監査・コンプライアンス要件がある  ✅ マルチアカウント構成で運用している  ✅ GitHubに依存したくない  ✅ マニュアル承認を組み込みたい  ✅ すべての操作をCloudTrailで追跡したい

1-3. 対象読者・前提知識

対象読者

  • エンタープライズ向けTerraform CI/CDを構築したいインフラ・DevOpsエンジニア
  • AWSのマネージドサービスでCI/CDを完結させたいチーム
  • 第2弾のGitHub Actions方式を使っているが、よりAWSネイティブな方式に移行を検討しているエンジニア
  • セキュリティ・監査要件からAWS上での権限管理を一元化したい組織

前提知識

本記事は以下の知識を前提としている。未習得の場合は先にリンク先を参照してほしい。

分野必要な知識参照先
Terraforminit / plan / apply の実行経験・リモートstate・moduleTerraform実践
state管理S3+DynamoDBバックエンド・ロックの仕組み第1弾
OIDC基礎IAM Role・信頼ポリシー・OIDC認証フロー第2弾
AWS CodePipelineパイプライン・ステージ・アクションの概念AWS公式ドキュメント
AWS CodeBuildbuildspec.yml・ビルドプロジェクトの概念AWS公式ドキュメント
AWS IAMRole・Policy・信頼ポリシー・最小権限AWS公式ドキュメント

1-4. この記事で学べること

本記事を通じて、以下のスキルと知識を習得できる。

  • CodePipeline基本構成: Source → Plan → Manual Approval → Apply の4ステージパイプラインを構築する
  • CodeBuildでのTerraform実行: buildspec.ymlでTerraformをインストール・実行する方法
  • マニュアル承認の組み込み: apply 前に人間の承認を必須にするManual Approvalステージの設定
  • マルチ環境構成(dev/stg/prod): 環境ごとにパイプラインを分離し、段階的なデプロイを実現する
  • マルチアカウント対応: Cross-account RoleによるマルチAWSアカウントへのTerraform apply
  • OIDC role assume chain: CodeBuildからAssumeRoleでターゲットアカウントのRoleを引き受ける設計
  • 監査ログ: CodePipelineの全操作をCloudTrailで記録し、承認者・実行者を追跡する
  • SNS通知: パイプラインの状態変化をSNSでSlack/Emailに通知する設定

1-5. 必要なもの

ハンズオンを進めるにあたり、以下の環境を用意しておくこと。

AWSアカウント

  • AWSアカウント(以下のサービスの作成・管理権限を持つユーザー)
  • CodePipeline / CodeBuild / CodeStar Connections
  • IAM Role / Policy / OIDC Provider
  • S3 / DynamoDB / SNS
  • アカウントID: 本記事では 123456789012 を使用(実際のアカウントIDに読み替えること)

ローカル環境

# バージョン確認terraform version# → Terraform v1.5.0 以上を推奨aws --version# → aws-cli/2.x 以上

GitHubアカウントとリポジトリ

  • GitHubアカウントとリポジトリ(本記事では myorg/terraform-repo を使用)
  • mainブランチへのpushがパイプラインのトリガーになる

Terraform backend(S3+DynamoDB)

第1弾で構築したバックエンドをそのまま使用できる。

S3バケット(state): myorg-terraform-stateDynamoDB テーブル:terraform-state-lockS3バケット(artifact): myorg-pipeline-artifacts  ← 本記事で新規作成

1-6. ハンズオン全体アーキテクチャ

本記事で構築するCI/CDパイプラインの全体像を以下に示す。

┌──────────────────────────────────────────────────────────────────────┐│GitHub Repository  ││(myorg/terraform-repo)││ ││  feature*"  # アーティファクトのディスカード前にキャッシュする(オプション)  discard-paths: nocache:  # .terraform ディレクトリをキャッシュしてプロバイダのダウンロードを省略  paths: - ".terraform*" - "/root/.terraform.d/plugin-cache*"reports:  # テストレポートが存在する場合(terraform test等)  TerraformTestReport: files:- "test-results*.xml" base-directory: "." discard-paths: no

各フェーズの詳細説明

フェーズ実行内容失敗時の動作
installTerraform本体・TFLintをインストールインストール失敗 → ビルド失敗
pre_buildinit・fmt check・validateフォーマット違反・構文エラー → ビルド失敗
buildterraform plan実行・JSON保存planエラー(exit=1) → ビルド失敗。変更あり(exit=2)は正常継続
post_buildplan結果のサマリー表示失敗しても実行される

-detailed-exitcode の使い方: terraform plan -detailed-exitcode は終了コード 2 を「変更あり(正常)」として返す。シェルでは $?2 でも失敗と見なさないよう PIPESTATUS で明示的に制御する必要がある。exit 1(エラー)のみ失敗として扱い、exit 2(変更あり)は正常終了させる点がポイントだ。

4-2. Plan用CodeBuildプロジェクトの作成

AWSコンソール手順

  1. CodeBuild → 「ビルドプロジェクトを作成する」
  2. プロジェクト名: terraform-plan-project
  3. 説明: 「Terraform plan ステージ用 CodeBuild プロジェクト」
  4. ソース
  5. ソースプロバイダー: 「AWS CodePipeline」(CodePipelineから呼ばれる場合はこれを選択)
  6. 環境
  7. 環境イメージ: 「マネージドイメージ」
  8. オペレーティングシステム: 「Amazon Linux」
  9. ランタイム: 「Standard」
  10. イメージ: 「aws/codebuild/amazonlinux2-x86_64-standard:5.0」(最新を選択)
  11. イメージのバージョン: 「このランタイムバージョンの最新イメージを常に使用する」
  12. 環境タイプ: 「Linux EC2」
  13. 特権モード: オフ(Dockerビルドが不要な場合)
  14. サービスロール: 「既存のサービスロール」→ codebuild-plan-role を選択
  15. Buildspec
  16. 「buildspecファイルを使用する」を選択
  17. Buildspec名: buildspec-plan.yml
  18. アーティファクト
  19. タイプ: 「Amazon S3」
  20. バケット名: myorg-pipeline-artifacts
  21. 名前: plan-artifacts
  22. パスの削除: オフ
  23. ログ
  24. CloudWatch Logs: 有効
  25. グループ名: /aws/codebuild/terraform-plan-project

Terraform コード(完全版)

resource "aws_codebuild_project" "terraform_plan" {  name = "terraform-plan-project"  description= "Terraform plan ステージ用 CodeBuild プロジェクト"  build_timeout = 60  # 分  service_role = aws_iam_role.codebuild_plan_role.arn  artifacts { type = "CODEPIPELINE"  }  cache { type  = "LOCAL" modes = ["LOCAL_CUSTOM_CACHE", "LOCAL_SOURCE_CACHE"]  }  environment { compute_type = "BUILD_GENERAL1_SMALL" image  = "aws/codebuild/amazonlinux2-x86_64-standard:5.0" type= "LINUX_CONTAINER" image_pull_credentials_type = "CODEBUILD" privileged_mode = false environment_variable {name  = "TF_VERSION"value = "1.9.0" } environment_variable {name  = "TF_BACKEND_BUCKET"value = "myorg-terraform-state" } environment_variable {name  = "TF_BACKEND_KEY"value = "terraform.tfstate" } environment_variable {name  = "TF_BACKEND_REGION"value = "ap-northeast-1" } environment_variable {name  = "TF_BACKEND_DYNAMODB_TABLE"value = "terraform-state-lock" } # シークレット値はParameter Store経由で渡す # environment_variable { #name  = "TF_VAR_db_password" #value = "/terraform/prod/db_password" #type  = "PARAMETER_STORE" # }  }  source { type= "CODEPIPELINE" buildspec = "buildspec-plan.yml"  }  logs_config { cloudwatch_logs {group_name  = "/aws/codebuild/terraform-plan-project"stream_name = "build-log" }  }  tags = { ManagedBy = "terraform" Purpose= "terraform-plan"  }}

compute_type の選択基準: BUILD_GENERAL1_SMALL(3GB RAM, 2vCPU)でほとんどのTerraform planは十分。大規模なモジュール(リソース数200以上)では BUILD_GENERAL1_MEDIUM(7GB RAM, 4vCPU)への変更を検討する。

4-3. CodePipelineの作成(SourceステージとPlanステージ)

AWSコンソール手順

  1. CodePipeline → 「パイプラインを作成する」
  2. パイプライン設定
  3. パイプライン名: terraform-cicd-pipeline
  4. パイプラインタイプ: V2(変数・条件・フィルタリングをサポート)
  5. 実行モード: 「キュー」(前の実行を待ってから次を開始)
  6. サービスロール: 「既存のサービスロール」→ codepipeline-role を選択
  7. アドバンスト設定
  8. アーティファクトストア: 「カスタムロケーション」→ myorg-pipeline-artifacts
  9. 暗号化キー: 「デフォルトAWSマネージドキー」
  10. Sourceステージ
  11. ソースプロバイダー: 「GitHub(GitHub App)」※CodeStar Connectionsを使用
  12. 接続: 先ほど作成した github-myorg-connection
  13. リポジトリ名: myorg/terraform-repo
  14. ブランチ名: main
  15. パイプライントリガー: 「特定のブランチへのプッシュ」(デフォルト)
  16. 出力アーティファクト形式: 「CodePipeline形式」
  17. ビルドステージ(Plan)
  18. ビルドプロバイダー: 「AWS CodeBuild」
  19. リージョン: ap-northeast-1
  20. プロジェクト名: terraform-plan-project
  21. ビルドタイプ: 「単一ビルド」
  22. 入力アーティファクト: SourceArtifact
  23. 出力アーティファクト: PlanArtifact
  24. 「パイプラインを作成する」をクリック

パイプラインタイプ V2 vs V1: V2は2023年末に導入された新しいパイプライン仕様で、変数・条件付きステージ・GitTagトリガーなどをサポートする。新規作成は必ずV2を選ぶこと。V1(旧仕様)は機能拡張が停止している。

Terraform コード(SourceステージとPlanステージ含む完全版)

resource "aws_codepipeline" "terraform_cicd" {  name  = "terraform-cicd-pipeline"  pipeline_type  = "V2"  role_arn = aws_iam_role.codepipeline_role.arn  execution_mode = "QUEUED"  artifact_store { location = aws_s3_bucket.pipeline_artifacts.bucket type  = "S3"  }  # ===== Sourceステージ =====  stage { name = "Source" action {name = "GitHubSource"category= "Source"owner= "AWS"provider= "CodeStarSourceConnection"version = "1"output_artifacts = ["SourceArtifact"]configuration = {  ConnectionArn  = aws_codestarconnections_connection.github.arn  FullRepositoryId  = "myorg/terraform-repo"  BranchName  = "main"  OutputArtifactFormat = "CODE_ZIP"  # mainブランチへのPRマージのみトリガー(直pushは除外したい場合)  # DetectChanges  = "true"} }  }  # ===== Planステージ =====  stage { name = "Plan" action {name = "TerraformPlan"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["SourceArtifact"]output_artifacts = ["PlanArtifact"]configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name} }  }  # ===== Manual Approvalステージ(Section 5で詳述)=====  stage { name = "ManualApproval" action {name  = "ApproveTerraformApply"category = "Approval"owner = "AWS"provider = "Manual"version  = "1"configuration = {  NotificationArn = aws_sns_topic.pipeline_notifications.arn  CustomData= "terraform planの結果を確認し、Applyを承認してください。"  ExternalEntityLink = "https://ap-northeast-1.console.aws.amazon.com/codesuite/codebuild/projects/terraform-plan-project/history"} }  }  # ===== Applyステージ(Section 6で詳述)=====  stage { name = "Apply" action {name= "TerraformApply"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["PlanArtifact"]configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name} }  }  tags = { ManagedBy = "terraform"  }}

execution_mode = "QUEUED" の重要性: デフォルトの SUPERSEDED だと、新しいコミットがpushされると実行中のパイプラインが上書きキャンセルされる。インフラ変更では途中キャンセルが危険なため、QUEUED(前の実行が終わるまで待機)が安全だ。

4-4. Planステージの動作確認

Planステージが正常に動作することを確認するための手順と、よくあるエラーの対処法を示す。

動作確認の流れ

# 1. テスト用の変更をGitHubのmainブランチにpushgit checkout maingit pull origin main# 空コミット(テスト用)git commit --allow-empty -m "test: trigger CodePipeline"git push origin main
2. AWSコンソールでパイプラインの実行を確認→ CodePipeline → terraform-cicd-pipeline→ 「実行の詳細」で各ステージのステータスを確認3. Sourceステージ→ ステータス: 成功 ✅ → GitHubからコードの取得に成功4. Planステージ→ ステータス: 進行中 → CodeBuildが起動してterraform planを実行中→ ステータス: 成功 ✅ → plan完了、PlanArtifactが生成された5. ManualApprovalステージ→ ステータス: 待機中 → 承認を待っている状態で一時停止

CodeBuildのビルドログ確認

CodeBuild → terraform-plan-project → ビルド履歴→ 最新のビルド → ビルドログを確認期待される出力:  === Installing Terraform 1.9.0 ===  Terraform v1.9.0 on linux_amd64  === Pre-build: Init, Format Check, Validate ===  Initializing the backend...  Successfully configured the backend "s3"!  Success! The configuration is valid.  === Build: terraform plan ===  Plan: 3 to add, 0 to change, 0 to destroy.  Plan exit code: 2  ← 変更あり(正常)  === Post-build: Plan Complete ===

よくあるエラーと対処法

エラー原因対処法
Error: error configuring S3 Backend: no valid credential sourcesCodeBuildのIAMロールにS3アクセス権限がないcodebuild-plan-role にstateバケットへのGetObject/ListBucket権限を追加
Error: Failed to install providerプロバイダーのダウンロードに失敗NAT Gatewayまたはインターネットアクセスを確認(プライベートサブネットで実行している場合)
The action failed because no branch named main existsSourceステージでブランチ名が不一致GitHubのデフォルトブランチ名(main vs master)を確認
Error: Connection 'github-myorg-connection' is not AVAILABLECodeStarConnectionsのOAuth認証が未完了AWSコンソールのConnections画面でOAuth認証を完了させる
╷ Error: Reference to undeclared resourceTerraformコードの構文エラーterraform validate をローカルで実行して事前確認
Process completed with exit code 1 (fmt check)terraform fmt -check でフォーマット違反を検出terraform fmt -recursive をローカルで実行してからpush
AccessDenied: User: arn:aws:sts::...codebuild-plan-role is not authorizedIAMロールの権限が不足不足しているアクション・リソースをポリシーに追加

パイプラインの手動再実行

# AWS CLIでパイプラインを手動起動aws codepipeline start-pipeline-execution \  --name terraform-cicd-pipeline \  --region ap-northeast-1# パイプラインの現在の状態を確認aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --region ap-northeast-1 \  --query 'stageStates[*].{Stage:stageName,Status:latestExecution.status}' \  --output table

Section 4 まとめ

設定内容リソース/ファイル確認ポイント
buildspec-plan.ymlGitリポジトリinit・fmt・validate・plan が順に実行される
terraform-plan-projectCodeBuildプロジェクトIAMロールがcodebuild-plan-roleに設定されている
terraform-cicd-pipelineCodePipelineV2・QUEUED・アーティファクトストアが正しい
SourceステージCodeStarConnections接続ステータスがAVAILABLEである
PlanステージCodeBuildplan出力がPlanArtifactとして保存される

SourceステージとPlanステージの確認が取れたら、次はManualApprovalとApplyステージの構築に進む。Section 5ではManualApproval(人間によるterraform plan確認・承認)を、Section 6ではApplyステージ(terraform apply実行)を構築する。


5. マニュアル承認ステージ

5-1. マニュアル承認の設計思想

CodePipelineのManual Approvalアクションは、terraform planの結果を人間が確認してからapplyを実行するための安全弁です。

なぜapply前に人間の確認が必要か

Terraformのplanは「これから行う変更の差分」を出力しますが、以下のリスクが残ります:

  • 破壊的変更の見落とし: destroyを含むリソース差分
  • 意図しない依存関係の連鎖: あるリソース変更が他リソースのrecreateを引き起こす
  • 本番データへの影響: RDS/S3などのステートフルリソースへの変更
  • コスト急増: 高額インスタンスタイプへの意図しない変更

GitHubのPRレビューとの違い

確認タイミング確認内容確認者
GitHubのPRレビューTerraformのコード変更(.tf ファイル)チームメンバー全員
ManualApprovalterraform planの実行結果(実際の変更差分)承認権限を持つメンバー

PRレビューはコードの意図を確認しますが、ManualApprovalは実行環境での実際の変更を確認します。環境ドリフト(手動変更やリソース追加)が発生している場合、planで想定外の変更が表示されることがあり、それを承認前にキャッチできます。

承認者の選定と権限設計

推奨構成:  PRレビュー  → チーム全員(コードレビュー)  ManualApproval → TechLead / SREチーム(インフラ変更最終確認)  prod apply  → 特定のIAMユーザーのみApproveアクション可能

承認権限はIAMポリシーで制御します:

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Action": [  "codepipeline:GetPipeline",  "codepipeline:GetPipelineState",  "codepipeline:GetPipelineExecution",  "codepipeline:PutApprovalResult"],"Resource": "arn:aws:codepipeline:ap-northeast-1:123456789012:terraform-cicd-pipeline" }  ]}

5-2. マニュアル承認アクションの設定

AWSコンソール手順

  1. CodePipeline → terraform-cicd-pipeline を選択
  2. 「パイプラインを編集」をクリック
  3. Planステージの後ろの「+」ボタンをクリック → 「ステージを追加」
  4. ステージ名: Approve
  5. 「アクショングループを追加」をクリック

アクション設定:
| 項目 | 値 |
|—|—|
| アクション名 | ManualApproval |
| アクションプロバイダー | Manual |
| SNS トピック ARN | arn:aws:sns:ap-northeast-1:123456789012:terraform-pipeline-notifications |
| URL(任意) | CodePipelineコンソールURL |
| コメント(任意) | terraform plan結果を確認してApproveしてください |

  1. 「完了」→「保存」をクリック

Terraform コード(ManualApprovalステージ追加版)

resource "aws_codepipeline" "terraform_cicd" {  name  = "terraform-cicd-pipeline"  role_arn = aws_iam_role.codepipeline_role.arn  artifact_store { location = aws_s3_bucket.pipeline_artifacts.id type  = "S3"  }  stage { name = "Source" action {name = "Source"category= "Source"owner= "AWS"provider= "CodeStarSourceConnection"version = "1"output_artifacts = ["source_output"]configuration = {  ConnectionArn = aws_codestarconnections_connection.github.arn  FullRepositoryId = "myorg/terraform-repo"  BranchName = "main"} }  }  stage { name = "Plan" action {name = "TerraformPlan"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["source_output"]output_artifacts = ["plan_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name} }  }  stage { name = "Approve" action {name  = "ManualApproval"category = "Approval"owner = "AWS"provider = "Manual"version  = "1"configuration = {  NotificationArn = aws_sns_topic.notifications.arn  CustomData= "Terraform plan完了。CodePipelineコンソールでplan結果を確認し、承認または拒否してください。"  ExternalEntityLink = "https://console.aws.amazon.com/codesuite/codepipeline/pipelines/terraform-cicd-pipeline/view"} }  }  stage { name = "Apply" action {name= "TerraformApply"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["plan_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name} }  }}

SNSトピックのTerraformコード:

resource "aws_sns_topic" "notifications" {  name = "terraform-pipeline-notifications"}resource "aws_sns_topic_subscription" "email" {  topic_arn = aws_sns_topic.notifications.arn  protocol  = "email"  endpoint  = "infra-team@example.com"}

5-3. plan結果の確認方法(承認者向け)

承認メールが届いたら、以下の手順でplan結果を確認します。

方法1: CodePipelineコンソールで確認

  1. AWSコンソール → CodePipeline → terraform-cicd-pipeline
  2. Planステージの「詳細」をクリック
  3. CodeBuildの実行ログ → 「terraform plan」出力を確認
# コンソールで確認できるplan出力例Plan: 2 to add, 1 to change, 0 to destroy.+ aws_instance.web+ aws_security_group.web~ aws_iam_role.app_role

方法2: S3アーティファクトからplan.jsonをダウンロード

# PlanArtifactを含むS3バケットを確認aws codepipeline get-pipeline \  --name terraform-cicd-pipeline \  --query "pipeline.artifactStore"# 最新の実行IDを取得EXECUTION_ID=$(aws codepipeline list-pipeline-executions \  --pipeline-name terraform-cicd-pipeline \  --query "pipelineExecutionSummaries[0].pipelineExecutionId" \  --output text)# S3からplan出力をダウンロードaws s3 cp s3://myorg-pipeline-artifacts/terraform-cicd-pipeline/plan_outp/ ./plan-artifact/ \  --recursive# plan.jsonをterraform showで読みやすく表示terraform show -json plan.json | jq '.resource_changes[] | select(.change.actions[] | . != "no-op")'

確認すべきポイント

確認項目アクションリスク
destroy が含まれる要注意 — 意図的か確認データ損失・サービス停止
RDS/S3への変更慎重に確認ステートフルリソース影響
forces replacementreplace = destroy + createダウンタイム発生
コスト増加リソースコスト試算と一致するか確認予算超過
IAMポリシー変更セキュリティレビュー権限昇格リスク

承認・拒否の操作手順

コンソールから承認:
1. CodePipeline → Approveステージ → 「レビュー」クリック
2. コメント入力(任意)
3. 「承認」または「拒否」をクリック

AWS CLIから承認:

# パイプラインの実行状態を確認aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query "stageStates[?stageName=='Approve'].actionStates"# 承認トークンを取得TOKEN=$(aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query "stageStates[?stageName=='Approve'].actionStates[0].latestExecution.token" \  --output text)# 承認aws codepipeline put-approval-result \  --pipeline-name terraform-cicd-pipeline \  --stage-name Approve \  --action-name ManualApproval \  --result "summary=Plan confirmed,status=Approved" \  --token "$TOKEN"# 拒否aws codepipeline put-approval-result \  --pipeline-name terraform-cicd-pipeline \  --stage-name Approve \  --action-name ManualApproval \  --result "summary=Unexpected destroy detected,status=Rejected" \  --token "$TOKEN"

5-4. 承認タイムアウトと期限切れの対処

デフォルトのタイムアウト

ManualApprovalアクションはデフォルトで7日間で自動的にタイムアウトします。タイムアウトするとパイプラインはFAILED状態になります。

タイムアウト変更はTerraformでは直接設定できないため、パイプラインを再起動します:

# タイムアウト後の再実行aws codepipeline start-pipeline-execution \  --name terraform-cicd-pipeline

定期的なリマインダー通知(EventBridgeで実現)

承認待ちが長引く場合に定期リマインダーを送る設計:

# 毎日朝9時にパイプライン状態を確認するLambda(設計パターン)resource "aws_cloudwatch_event_rule" "approval_reminder" {  name = "pipeline-approval-reminder"  schedule_expression = "cron(0 0 * * ? *)"  # UTC 00:00 = JST 09:00}

緊急時の承認スキップ(権限設計で制御)

緊急デプロイが必要な場合でも、ManualApprovalをコードでスキップすることはできません。代わりに以下の設計で対応します:

緊急対応パターン:  1. 承認者をSlack/電話で直接連絡して即時承認  2. 緊急用パイプライン(terraform-cicd-emergency)を用意(承認ステージなし)  → 通常時は無効化、緊急時のみIAMで有効化  3. 直接terraform apply(BreakGlass手順)を整備  → 実行はCloudTrailに記録必須

5-5. マニュアル承認ハンズオン

以下の手順でManualApprovalの動作を確認します。

Step 1: GitHubにpushしてパイプラインを起動

# テスト用の変更をpushgit checkout -b feature/test-approvalecho "# test" >> README.mdgit add README.mdgit commit -m "test: trigger pipeline for approval test"git push origin feature/test-approval# PRをmainにマージ# → mainへのpushでパイプライン自動起動

Step 2: Planステージ完了後、Approveステージで停止することを確認

# パイプラインの状態をポーリングwatch -n 10 aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query "stageStates[*].{Stage:stageName,Status:latestExecution.status}"

期待する出力:

StageStatusSource  SucceededPlan SucceededApprove InProgress  ← ここで停止Apply-

Step 3: SNSメール通知が届くことを確認

SNSサブスクリプションのメールアドレスに以下のようなメールが届きます:

Subject: APPROVAL NEEDED: AWS CodePipeline terraform-cicd-pipelinePipeline: terraform-cicd-pipelineStage: ApproveAction: ManualApprovalCustomData: Terraform plan完了。CodePipelineコンソールでplan結果を確認し...To review and approve/reject, please visit:https://console.aws.amazon.com/codesuite/codepipeline/...

Step 4: CodePipelineコンソールから承認操作

  1. コンソール → Approveステージ → 「レビュー」
  2. コメント: plan確認OK — EC2追加のみ、destroyなし
  3. 「承認」をクリック

Step 5: パイプラインが次ステージ(Apply)に進むことを確認

# 承認後のステータス確認aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query "stageStates[*].{Stage:stageName,Status:latestExecution.status}"

期待する出力:

StageStatusSource  SucceededPlan SucceededApprove Succeeded  ← 承認完了ApplyInProgress ← apply実行中

Section 5 まとめ

設定内容確認ポイント
ManualApprovalアクションApproveステージに追加されているか
SNS通知承認者のメールアドレスに届くか
IAM権限承認権限をもつユーザーのみApprove可能か
タイムアウト7日で自動失敗(運用ルールで対処)

ManualApprovalが機能したら、次はApplyステージの構築に進みます。


6. Apply BuildステージとCodeBuild設定

6-1. Apply用 buildspec.yml の作成

Apply用のbuildspecは、Planステージで生成したtfplanバイナリを受け取り、terraform applyを実行します。

# buildspec-apply.ymlversion: 0.2env:  variables: TF_VERSION: "1.7.0" TF_DIR: "."  parameter-store: TF_BACKEND_BUCKET: "/terraform/backend/bucket" TF_BACKEND_KEY: "/terraform/backend/key" TF_BACKEND_REGION: "/terraform/backend/region" TF_BACKEND_LOCK_TABLE: "/terraform/backend/lock_table"phases:  install: runtime-versions:python: 3.11 commands:- echo "=== Terraform Install ==="- wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip- unzip -q terraform_${TF_VERSION}_linux_amd64.zip- mv terraform /usr/local/bin/- terraform version  pre_build: commands:- echo "=== Terraform Init ==="- cd ${TF_DIR}- |  terraform init \ -backend-config="bucket=${TF_BACKEND_BUCKET}" \ -backend-config="key=${TF_BACKEND_KEY}" \ -backend-config="region=${TF_BACKEND_REGION}" \ -backend-config="dynamodb_table=${TF_BACKEND_LOCK_TABLE}" \ -no-color- echo "=== Extracting Plan Artifact ==="- ls -la ${CODEBUILD_SRC_DIR_plan_output}/- cp ${CODEBUILD_SRC_DIR_plan_output}/tfplan ./tfplan  build: commands:- echo "=== Terraform Apply ==="- terraform apply -no-color -auto-approve tfplan- echo "Apply exit code: $?"  post_build: commands:- echo "=== Terraform Output ==="- terraform output -json > outputs.json || echo "No outputs defined"- echo "Apply completed at $(date)"artifacts:  files: - outputs.json  name: apply_output  discard-paths: yescache:  paths: - '.terraform/providers*'

buildspec-apply.yml のポイント

設定説明
CODEBUILD_SRC_DIR_plan_outputPlanステージのアーティファクトをApplyステージが受け取る環境変数
-auto-approve tfplanユーザー入力なしで保存済みplanを適用
terraform output -jsonapply後のリソース出力をアーティファクトとして保存
キャッシュ設定プロバイダーキャッシュでビルド時間を短縮

6-2. Apply用CodeBuildプロジェクトの作成

AWSコンソール手順

  1. CodeBuild → 「ビルドプロジェクトを作成」
設定項目
プロジェクト名terraform-apply-project
ソースNo source(CodePipelineから受け取る)
環境イメージマネージドイメージ / Amazon Linux 2023
ランタイムStandard
イメージaws/codebuild/standard:7.0
サービスロールcodebuild-apply-role
Buildspecbuildspecファイルを使用 → buildspec-apply.yml
ログCloudWatch Logs: /aws/codebuild/terraform-apply
  1. 「ビルドプロジェクトを作成」をクリック

Terraform コード(完全版)

resource "aws_codebuild_project" "terraform_apply" {  name = "terraform-apply-project"  description= "Terraform apply stage for CI/CD pipeline"  build_timeout = 60  service_role  = aws_iam_role.codebuild_apply_role.arn  artifacts { type = "CODEPIPELINE"  }  cache { type  = "LOCAL" modes = ["LOCAL_DOCKER_LAYER_CACHE", "LOCAL_SOURCE_CACHE"]  }  environment { compute_type = "BUILD_GENERAL1_SMALL" image  = "aws/codebuild/standard:7.0" type= "LINUX_CONTAINER" image_pull_credentials_type = "CODEBUILD" environment_variable {name  = "TF_VERSION"value = "1.7.0" } environment_variable {name  = "TF_DIR"value = "." }  }  source { type= "CODEPIPELINE" buildspec = "buildspec-apply.yml"  }  logs_config { cloudwatch_logs {group_name  = "/aws/codebuild/terraform-apply"stream_name = "" }  }  tags = { Project = "terraform-cicd" Stage= "apply"  }}

Apply用IAMロールのポリシー(Plan用より権限が広い):

resource "aws_iam_role_policy" "codebuild_apply_policy" {  name = "codebuild-apply-policy"  role = aws_iam_role.codebuild_apply_role.id  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect = "Allow"  Action = [ # S3 アーティファクト / state 操作 "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation", # DynamoDB ロック "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable", # CodeBuild ログ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", # パラメータストア "ssm:GetParameter", "ssm:GetParameters", # KMS "kms:GenerateDataKey", "kms:Decrypt", # リソース操作(管理対象リソースに応じて追加) "ec2:*", "rds:*", "iam:PassRole"  ]  Resource = "*"} ]  })}

6-3. CodePipelineにApplyステージを追加(完全版)

Section 5のManualApprovalと組み合わせた、4ステージ完全版のパイプラインコードです。

resource "aws_codepipeline" "terraform_cicd" {  name = "terraform-cicd-pipeline"  role_arn= aws_iam_role.codepipeline_role.arn  pipeline_type = "V2"  execution_mode = "QUEUED"  artifact_store { location = aws_s3_bucket.pipeline_artifacts.id type  = "S3" encryption_key {id= aws_kms_key.pipeline.arntype = "KMS" }  }  # ステージ1: Source  stage { name = "Source" action {name = "Source"category= "Source"owner= "AWS"provider= "CodeStarSourceConnection"version = "1"output_artifacts = ["source_output"]run_order  = 1configuration = {  ConnectionArn = aws_codestarconnections_connection.github.arn  FullRepositoryId = "myorg/terraform-repo"  BranchName = "main"  DetectChanges = "true"} }  }  # ステージ2: Plan  stage { name = "Plan" action {name = "TerraformPlan"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["source_output"]output_artifacts = ["plan_output"]run_order  = 1configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name} }  }  # ステージ3: Approve  stage { name = "Approve" action {name= "ManualApproval"category  = "Approval"owner  = "AWS"provider  = "Manual"version= "1"run_order = 1configuration = {  NotificationArn = aws_sns_topic.notifications.arn  CustomData= "terraform plan完了。plan結果を確認し、承認または拒否してください。"  ExternalEntityLink = "https://console.aws.amazon.com/codesuite/codepipeline/pipelines/terraform-cicd-pipeline/view"} }  }  # ステージ4: Apply  stage { name = "Apply" action {name= "TerraformApply"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["plan_output"]run_order = 1configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name} }  }}

6-4. apply失敗時のロールバック戦略

apply失敗の確認

# パイプラインの最新実行ステータスを確認aws codepipeline list-pipeline-executions \  --pipeline-name terraform-cicd-pipeline \  --max-results 3# 失敗したapplyのビルドログを確認aws logs tail /aws/codebuild/terraform-apply --follow

部分的apply失敗時の対処

Terraformはapply途中で失敗すると、適用済みのリソースはstateに記録されます。

# 現在のstateを確認terraform state list# 問題のあるリソースをstateから除外(再インポートで対処)terraform state rm aws_instance.web_server# 手動修正後、再インポートterraform import aws_instance.web_server i-1234567890abcdef0# 修正コードで再applyterraform apply

よくある失敗パターンと対処法

エラー原因対処
Error: access deniedIAMロールに権限不足codebuild-apply-roleのポリシーを確認
Error: resource already exists手動作成済みリソースとのコンフリクトterraform importでstateに取り込む
Error: timeoutリソース作成に時間がかかりすぎtimeoutsブロックで延長
Error: lock timeout別のterraform操作が進行中DynamoDBのlockアイテムを確認・削除
plan artifact期限切れplanとapplyの間が72時間以上パイプラインを再起動してplanからやり直し

リトライ方針

# 失敗したパイプラインを特定のステージから再起動(Apply失敗時、Planからやり直す)aws codepipeline retry-stage-execution \  --pipeline-name terraform-cicd-pipeline \  --stage-name Apply \  --pipeline-execution-id <execution-id> \  --retry-mode ALL_ACTIONS

6-5. apply完了後の通知設定

EventBridge → SNS通知

# パイプライン成功/失敗通知ルールresource "aws_cloudwatch_event_rule" "pipeline_state_change" {  name  = "terraform-pipeline-state-change"  description = "Notify on CodePipeline state changes"  event_pattern = jsonencode({ source= ["aws.codepipeline"] detail-type = ["CodePipeline Pipeline Execution State Change"] detail = {pipeline = ["terraform-cicd-pipeline"]state = ["SUCCEEDED", "FAILED"] }  })}resource "aws_cloudwatch_event_target" "pipeline_sns" {  rule= aws_cloudwatch_event_rule.pipeline_state_change.name  target_id = "pipeline-notification"  arn = aws_sns_topic.notifications.arn  input_transformer { input_paths = {pipeline  = "$.detail.pipeline"state  = "$.detail.state"execution = "$.detail.execution-id" } input_template = "\"Pipeline <pipeline> <state>. Execution ID: <execution>\""  }}# EventBridgeがSNSにPublishする権限resource "aws_sns_topic_policy" "pipeline_events" {  arn = aws_sns_topic.notifications.arn  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect = "Allow"  Principal = { Service = "events.amazonaws.com" }  Action = "SNS:Publish"  Resource  = aws_sns_topic.notifications.arn} ]  })}

通知フォーマット例

成功通知:

Subject: [SUCCEEDED] CodePipeline terraform-cicd-pipelinePipeline terraform-cicd-pipeline SUCCEEDED.Execution ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890

失敗通知:

Subject: [FAILED] CodePipeline terraform-cicd-pipelinePipeline terraform-cicd-pipeline FAILED.Execution ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890確認先: https://console.aws.amazon.com/codesuite/codepipeline/pipelines/terraform-cicd-pipeline/view

6-6. エンドツーエンドハンズオン(全4ステージ)

実際に全4ステージが動作することを確認します。

Step 1: テスト用リソースを追加してpush

# main.tfに追加するテスト用EC2resource "aws_instance" "web" {  ami  = "ami-0d52744d6551d851e"  # Amazon Linux 2023  instance_type = "t3.micro"  tags = { Name = "cicd-test-web" Project = "terraform-cicd"  }}
git add main.tfgit commit -m "feat: add web EC2 instance for CI/CD test"git push origin main

Step 2: Source → Plan と進みplan結果確認

# パイプラインの進行をモニタリングwatch -n 15 "aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query 'stageStates[*].{Stage:stageName,Status:latestExecution.status}' \  --output table"

Planステージ完了後、CodeBuildのログで確認:

Plan: 1 to add, 0 to change, 0 to destroy.

Step 3: 承認操作

SNSメールを確認し、コンソールから承認:
1. Approveステージ → 「レビュー」
2. コメント: plan確認OK — EC2 1台追加のみ
3. 「承認」

Step 4: Apply実行 → AWSコンソールでEC2作成確認

# Apply完了を確認aws codepipeline get-pipeline-state \  --name terraform-cicd-pipeline \  --query "stageStates[?stageName=='Apply'].latestExecution.status"# EC2インスタンスが作成されたことを確認aws ec2 describe-instances \  --filters "Name=tag:Name,Values=cicd-test-web" \  --query "Reservations[].Instances[].{ID:InstanceId,State:State.Name,IP:PublicIpAddress}"

Step 5: SNS通知でapply完了メール確認

Subject: [SUCCEEDED] CodePipeline terraform-cicd-pipelinePipeline terraform-cicd-pipeline SUCCEEDED.

Step 6: terraform destroyで後片付け

# main.tfからEC2リソースを削除(コメントアウトまたは削除)# resource "aws_instance" "web" { ... }  ← 削除
git add main.tfgit commit -m "cleanup: remove test EC2 instance"git push origin main# → パイプラインが自動起動 → Plan: 0 to add, 0 to change, 1 to destroy# → 承認後にdestroy実行

Section 6 まとめ

コンポーネントファイル/リソース確認ポイント
buildspec-apply.ymlGitリポジトリtfplanを受け取り、apply -auto-approve で実行
terraform-apply-projectCodeBuildプロジェクトcodebuild-apply-role が設定されている
Apply ステージCodePipelineplan_output を入力アーティファクトとして受け取る
EventBridge通知CloudWatch EventsSUCCEEDED/FAILED でSNS通知が届く

4ステージパイプラインの完成後は、Section 7でマルチ環境構成(dev/stg/prod)に拡張します。


7. マルチ環境構成(dev/stg/prod)

マルチ環境・マルチアカウント構成図

7-1. マルチ環境設計の考え方

単一のAWSアカウントで dev/stg/prod を管理する場合でも、Terraformの状態(state)と設定値は環境ごとに完全に分離する必要があります。それを怠ると、dev環境での実験的な変更がprod環境に誤って適用される「環境汚染」が発生します。

環境分離の2つのアプローチ

アプローチA: Terraform Workspace

terraform workspace new devterraform workspace new stgterraform workspace new prodterraform workspace select devterraform apply
メリット: コードの重複が少ないデメリット:  - workspace名の付け間違いで別環境に適用する事故が起きる  - stateファイルのパスが env:/dev/terraform.tfstate と読みにくい  - 環境ごとに設定を大きく変えたい場合に分岐が増えて複雑になる

アプローチB: ディレクトリ分離(本記事採用)

terraform-repo/├── environments/│├── dev/││├── main.tf││├── variables.tf││├── terraform.tfvars││└── backend.tf│├── stg/││├── main.tf││├── variables.tf││├── terraform.tfvars││└── backend.tf│└── prod/│ ├── main.tf│ ├── variables.tf│ ├── terraform.tfvars│ └── backend.tf└── modules/ ├── ec2/ │├── main.tf │├── variables.tf │└── outputs.tf └── vpc/  ├── main.tf  ├── variables.tf  └── outputs.tf
メリット:  - 各環境が独立したTerraformルートモジュールとして扱われる  - 誤って別環境に適用するリスクが低い(ディレクトリが物理的に分かれている)  - 環境ごとのstateファイルパスが直感的  - dev/prodで全く異なるリソース構成も扱いやすいデメリット: environments/ 間でコードが重複しやすい → modules/ で共通化する

本記事でディレクトリ分離を採用する理由: CodePipelineのステージごとに異なるディレクトリを cd して実行するのが自然であり、環境の切り間違いリスクが低いため。

各環境の backend.tf 設定

# environments/dev/backend.tfterraform {  backend "s3" { bucket= "myorg-terraform-state" key= "dev/terraform.tfstate" region= "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt  = true  }}
# environments/stg/backend.tfterraform {  backend "s3" { bucket= "myorg-terraform-state" key= "stg/terraform.tfstate" region= "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt  = true  }}
# environments/prod/backend.tfterraform {  backend "s3" { bucket= "myorg-terraform-state" key= "prod/terraform.tfstate" region= "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt  = true  }}

S3のバケット内は以下のように分離されます。

myorg-terraform-state/├── dev/terraform.tfstate├── stg/terraform.tfstate└── prod/terraform.tfstate

7-2. 環境ごとのパイプライン設計

マルチ環境のCodePipelineには2つの設計パターンがあります。

選択肢A: 1パイプライン × 複数ステージ(本記事採用)

┌──────────────────────────────────────────────────────────────────┐│  terraform-cicd-pipeline││  ││  Source → Plan-Dev → Approve-Dev → Apply-Dev││  ↓ ││Plan-Stg → Approve-Stg → Apply-Stg  ││ ↓  ││ Plan-Prod → Approve-Prod → Apply-Prod │└──────────────────────────────────────────────────────────────────┘
メリット:  - パイプラインの実行フローが1画面で確認できる  - dev→stg→prodの昇格ルールをCodePipelineが強制する  - Approve(手動承認)を組み込みやすいデメリット:  - パイプラインが長くなり管理が複雑になる  - 1ステージが失敗すると後続の環境にも影響する

選択肢B: 環境ごとに独立したパイプライン

pipeline-dev:Source → Plan-Dev → Apply-Devpipeline-stg:Source → Plan-Stg → Approve-Stg → Apply-Stgpipeline-prod:  Source → Plan-Prod → Approve-Prod → Apply-Prod
メリット:  - 各環境のパイプラインを独立して実行・管理できる  - devとprodのリリースサイクルを完全に分離できるデメリット:  - dev→stg→prodの順序を自動強制できない  - パイプラインが3本になり、コード・コスト・管理コストが増える

本記事での選択: シンプルさを優先し、選択肢A(1パイプライン×複数ステージ)を採用します。


7-3. 環境別パイプラインの実装

以下は dev→stg→prod を順次実行する完全なCodePipeline Terraformコードです。

# pipeline.tf — マルチ環境対応 CodePipelineresource "aws_codepipeline" "terraform_cicd" {  name  = "terraform-cicd-pipeline"  role_arn = aws_iam_role.codepipeline_role.arn  artifact_store { location = aws_s3_bucket.pipeline_artifacts.bucket type  = "S3"  }  # ─── Stage 1: Source ─────────────────────────────────────────────  stage { name = "Source" action {name = "Source"category= "Source"owner= "ThirdParty"provider= "GitHub"version = "2"output_artifacts = ["source_output"]configuration = {  Owner= "myorg"  Repo = "terraform-repo"  Branch  = "main"  OAuthToken = var.github_oauth_token} }  }  # ─── Stage 2: Plan-Dev ───────────────────────────────────────────  stage { name = "Plan-Dev" action {name = "TerraformPlan-Dev"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["source_output"]output_artifacts = ["plan_dev_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "dev" }, { name = "TF_DIR", value = "environments/dev" }  ])} }  }  # ─── Stage 3: Approve-Dev ────────────────────────────────────────  stage { name = "Approve-Dev" action {name  = "ManualApproval-Dev"category = "Approval"owner = "AWS"provider = "Manual"version  = "1"configuration = {  NotificationArn = aws_sns_topic.pipeline_approval.arn  CustomData= "dev環境へのapplyを承認してください。planの結果を確認の上、承認・却下を選択してください。"} }  }  # ─── Stage 4: Apply-Dev ──────────────────────────────────────────  stage { name = "Apply-Dev" action {name= "TerraformApply-Dev"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["source_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "dev" }, { name = "TF_DIR", value = "environments/dev" }  ])} }  }  # ─── Stage 5: Plan-Stg ───────────────────────────────────────────  stage { name = "Plan-Stg" action {name = "TerraformPlan-Stg"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["source_output"]output_artifacts = ["plan_stg_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "stg" }, { name = "TF_DIR", value = "environments/stg" }  ])} }  }  # ─── Stage 6: Approve-Stg ────────────────────────────────────────  stage { name = "Approve-Stg" action {name  = "ManualApproval-Stg"category = "Approval"owner = "AWS"provider = "Manual"version  = "1"configuration = {  NotificationArn = aws_sns_topic.pipeline_approval.arn  CustomData= "stg環境へのapplyを承認してください。"} }  }  # ─── Stage 7: Apply-Stg ──────────────────────────────────────────  stage { name = "Apply-Stg" action {name= "TerraformApply-Stg"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["source_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "stg" }, { name = "TF_DIR", value = "environments/stg" }  ])} }  }  # ─── Stage 8: Plan-Prod ──────────────────────────────────────────  stage { name = "Plan-Prod" action {name = "TerraformPlan-Prod"category= "Build"owner= "AWS"provider= "CodeBuild"version = "1"input_artifacts  = ["source_output"]output_artifacts = ["plan_prod_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_plan.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "prod" }, { name = "TF_DIR", value = "environments/prod" }  ])} }  }  # ─── Stage 9: Approve-Prod ───────────────────────────────────────  stage { name = "Approve-Prod" action {name  = "ManualApproval-Prod"category = "Approval"owner = "AWS"provider = "Manual"version  = "1"configuration = {  NotificationArn = aws_sns_topic.pipeline_approval.arn  CustomData= "【本番環境】prod環境へのapplyを承認してください。影響範囲を十分に確認した上で承認してください。"} }  }  # ─── Stage 10: Apply-Prod ────────────────────────────────────────  stage { name = "Apply-Prod" action {name= "TerraformApply-Prod"category  = "Build"owner  = "AWS"provider  = "CodeBuild"version= "1"input_artifacts = ["source_output"]configuration = {  ProjectName = aws_codebuild_project.terraform_apply.name  EnvironmentVariables = jsonencode([ { name = "TF_ENV", value = "prod" }, { name = "TF_DIR", value = "environments/prod" }  ])} }  }}

ポイント: 全ステージで同一の CodeBuild プロジェクト(terraform_plan / terraform_apply)を使用しています。環境の切り替えは TF_ENVTF_DIR という環境変数で行います。これにより、buildspec.yml を1本書けば全環境に使い回せます


7-4. 環境別 buildspec.yml の分岐

CodeBuild の buildspec.yml で TF_ENVTF_DIR を参照し、環境ごとにbackend設定とtfvarsを切り替えます。

terraform-plan の buildspec.yml

# buildspec-plan.ymlversion: 0.2phases:  install: commands:- TF_VERSION=1.7.5- curl -fsSL "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" -o terraform.zip- unzip terraform.zip -d /usr/local/bin/- terraform version  pre_build: commands:- echo "=== Environment: $TF_ENV ==="- cd $CODEBUILD_SRC_DIR/$TF_DIR- |  terraform init \ -backend-config="bucket=myorg-terraform-state" \ -backend-config="key=${TF_ENV}/terraform.tfstate" \ -backend-config="region=ap-northeast-1" \ -backend-config="dynamodb_table=terraform-state-lock"  build: commands:- cd $CODEBUILD_SRC_DIR/$TF_DIR- echo "=== terraform plan ($TF_ENV) ==="- |  terraform plan \ -var-file="terraform.tfvars" \ -out=tfplan-${TF_ENV} \ -no-color \ 2>&1 | tee /tmp/plan_output.txt- |  echo "## Terraform Plan - $TF_ENV" >> /tmp/summary.md  echo '```' >> /tmp/summary.md  tail -20 /tmp/plan_output.txt >> /tmp/summary.md  echo '```' >> /tmp/summary.mdartifacts:  files: - "**/*"  base-directory: $TF_DIR

terraform-apply の buildspec.yml

# buildspec-apply.ymlversion: 0.2phases:  install: commands:- TF_VERSION=1.7.5- curl -fsSL "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" -o terraform.zip- unzip terraform.zip -d /usr/local/bin/- terraform version  pre_build: commands:- echo "=== Apply Environment: $TF_ENV ==="- cd $CODEBUILD_SRC_DIR/$TF_DIR- |  terraform init \ -backend-config="bucket=myorg-terraform-state" \ -backend-config="key=${TF_ENV}/terraform.tfstate" \ -backend-config="region=ap-northeast-1" \ -backend-config="dynamodb_table=terraform-state-lock"  build: commands:- cd $CODEBUILD_SRC_DIR/$TF_DIR- terraform plan -var-file="terraform.tfvars" -out=tfplan-apply -no-color- terraform apply -auto-approve -no-color tfplan-apply  post_build: commands:- |  if [ "$CODEBUILD_BUILD_SUCCEEDING" = "1" ]; then echo "Apply succeeded for $TF_ENV"  else echo "Apply failed for $TF_ENV"  fi

環境別 terraform.tfvars の例

# environments/dev/terraform.tfvarsenvironment = "dev"instance_type  = "t3.micro"desired_count  = 1enable_deletion_protection = false
# environments/stg/terraform.tfvarsenvironment = "stg"instance_type  = "t3.small"desired_count  = 2enable_deletion_protection = true
# environments/prod/terraform.tfvarsenvironment = "prod"instance_type  = "t3.medium"desired_count  = 4enable_deletion_protection = true

7-5. マルチ環境ハンズオン

実際にマルチ環境パイプラインを動かす手順を step-by-step で確認します。

Step 1: feature ブランチで変更を作成して push

git checkout -b feature/add-s3-bucket
# environments/dev/main.tf に追記resource "aws_s3_bucket" "app_data" {  bucket = "myorg-app-data-${var.environment}"  tags = { Environment = var.environment ManagedBy= "terraform"  }}
git add environments/dev/main.tfgit commit -m "feat: add app-data S3 bucket"git push origin feature/add-s3-bucket

PR を作成して main にマージします。マージ後、CodePipeline が自動起動します。

Step 2: dev 環境に apply

AWS コンソールで CodePipeline > terraform-cicd-pipeline を開きます。

確認の流れ:1. Source→ 成功(最新コミットを取得)2. Plan-Dev → 成功(CodeBuildログで「1 to add」を確認)3. Approve-Dev → SNS通知が届く  → CodePipelineコンソールで「Review」→「Approve」を選択4. Apply-Dev→ 成功
# dev 環境のリソースを確認aws s3 ls | grep myorg-app-data-dev

Step 3: dev 承認後、stg に自動昇格

Apply-Dev が完了すると自動的に Plan-Stg が実行されます。

Plan-Stg の確認:  - stg/terraform.tfstate を参照していること  - myorg-app-data-stg が plan に表示されること  - 「1 to add, 0 to destroy」であること

SNS 通知を受け取ったら Approve-Stg を承認します。

aws s3 ls | grep myorg-app-data-stg

Step 4: stg 承認後、prod 昇格

prod の承認ゲートで確認すること:  ✅ plan で destroy が発生していないこと  ✅ 変更対象のリソースが意図したものだけであること  ✅ stg 環境での動作確認が完了していること  ✅ 本番リリースの時間帯として適切か
aws s3 ls | grep myorg-app-data-prod

Step 5: 全環境で terraform destroy

cd environments/prod && terraform destroy -var-file="terraform.tfvars"cd environments/stg  && terraform destroy -var-file="terraform.tfvars"cd environments/dev  && terraform destroy -var-file="terraform.tfvars"

または Terraform コードからリソースブロックを削除して PR→マージ→パイプライン実行でも destroy できます(コードdriven destroy)。


8. マルチアカウント構成

8-1. なぜマルチアカウントか

多くのエンタープライズ企業では、AWSアカウントを環境ごとに分けています。

シングルアカウント構成(小規模チーム向け):  AWS Account: 123456789012  ├── dev リソース  ├── stg リソース  └── prod リソース  問題: dev の実験的操作が prod を誤って変更するリスクがあるマルチアカウント構成(エンタープライズ向け):  管理アカウント: 123456789012  ← CodePipeline/CodeBuild が存在  ├── dev アカウント:  111111111111  └── prod アカウント: 222222222222

マルチアカウントの3大メリット

メリット内容
セキュリティ分離prod アカウントへの IAM 権限を持つ人間・ロールを最小限にできる
請求分離環境ごとのコストをアカウントレベルで分離・可視化できる
リソース制限の独立dev の Service Quotas 上限超過が prod に影響しない

AWS Organizations の概要

Organizations 管理アカウント (123456789012)├── OU: Infrastructure│├── dev アカウント (111111111111)│└── prod アカウント (222222222222)└── OU: Management └── 管理ツール用アカウント(本記事のCodePipeline)

本記事では、管理アカウントにCodePipeline/CodeBuildを配置し、各環境アカウントへクロスアカウントでIAMロールをAssumeしてTerraformを実行する設計を採用します。


8-2. クロスアカウント IAM ロール設計

全体の認証フロー

CodeBuild (管理アカウント: 123456789012) │ │ sts:AssumeRole ▼TerraformApplyRole-Dev (dev アカウント: 111111111111) │ │ terraform apply ▼dev アカウントの AWS リソース

dev アカウント側の設定(信頼ポリシー)

dev アカウント(111111111111)に TerraformApplyRole-Dev を作成し、管理アカウントの CodeBuild ロールからのみ AssumeRole を許可します。

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Principal": {  "AWS": "arn:aws:iam::123456789012:role/codebuild-apply-role"},"Action": "sts:AssumeRole","Condition": {  "StringEquals": { "sts:ExternalId": "terraform-cicd-dev"  }} }  ]}

ExternalId を設定する理由: 管理アカウントの CodeBuild ロールが複数システムで共有される場合、ExternalId なしでは意図しない AssumeRole が発生するリスクがあります。ExternalId を設定することで「terraform-cicd パイプラインからの要求のみ受け付ける」という制約を追加できます。

prod アカウント(222222222222)の信頼ポリシー:

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Principal": {  "AWS": "arn:aws:iam::123456789012:role/codebuild-apply-role"},"Action": "sts:AssumeRole","Condition": {  "StringEquals": { "sts:ExternalId": "terraform-cicd-prod"  }} }  ]}

dev アカウントの TerraformApplyRole-Dev に付与するポリシー

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Action": ["ec2:*", "s3:*", "iam:*", "cloudwatch:*"],"Resource": "*" }, {"Effect": "Allow","Action": [  "s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:GetBucketVersioning"],"Resource": [  "arn:aws:s3:::myorg-terraform-state",  "arn:aws:s3:::myorg-terraform-state/*"] }, {"Effect": "Allow","Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"],"Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/terraform-state-lock" }  ]}

注意: DynamoDB のロックテーブルは管理アカウントにある前提です。dev/prod アカウントからクロスアカウントアクセスできるよう、管理アカウントの DynamoDB テーブルへのアクセスを許可します。

管理アカウントの CodeBuild ロールへの追加ポリシー

{  "Version": "2012-10-17",  "Statement": [ {"Effect": "Allow","Action": "sts:AssumeRole","Resource": [  "arn:aws:iam::111111111111:role/TerraformApplyRole-Dev",  "arn:aws:iam::222222222222:role/TerraformApplyRole-Prod"] }  ]}

8-3. buildspec.yml でのクロスアカウント AssumeRole

CodeBuild の buildspec.yml 内で STS の AssumeRole を行い、dev/prod アカウントの認証情報を一時的に取得してから Terraform を実行します。

# buildspec-apply-crossaccount.ymlversion: 0.2phases:  install: commands:- TF_VERSION=1.7.5- curl -fsSL "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" -o terraform.zip- unzip terraform.zip -d /usr/local/bin/- terraform version- apt-get install -y jq  pre_build: commands:- echo "=== Cross-Account Apply: $TF_ENV (Account: $TARGET_ACCOUNT_ID) ==="# STS AssumeRole でターゲットアカウントの認証情報を取得- |  CREDS=$(aws sts assume-role \ --role-arn "arn:aws:iam::${TARGET_ACCOUNT_ID}:role/TerraformApplyRole-${TF_ENV_CAPITALIZE}" \ --role-session-name "terraform-apply-${TF_ENV}-${CODEBUILD_BUILD_ID}" \ --external-id "terraform-cicd-${TF_ENV}" \ --duration-seconds 3600 \ --output json)# 取得した認証情報を環境変数にセット- export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId')- export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')- export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken')# 認証確認(ARNのみ表示)- aws sts get-caller-identity --query 'Arn' --output text- cd $CODEBUILD_SRC_DIR/$TF_DIR- |  terraform init \ -backend-config="bucket=myorg-terraform-state-${TF_ENV}" \ -backend-config="key=${TF_ENV}/terraform.tfstate" \ -backend-config="region=ap-northeast-1" \ -backend-config="dynamodb_table=terraform-state-lock" \ -reconfigure  build: commands:- cd $CODEBUILD_SRC_DIR/$TF_DIR- terraform plan -var-file="terraform.tfvars" -out=tfplan -no-color- terraform apply -auto-approve -no-color tfplan  post_build: commands:- unset AWS_ACCESS_KEY_ID- unset AWS_SECRET_ACCESS_KEY- unset AWS_SESSION_TOKEN- |  if [ "$CODEBUILD_BUILD_SUCCEEDING" = "1" ]; then echo "Cross-account apply succeeded: $TF_ENV ($TARGET_ACCOUNT_ID)"  else echo "Cross-account apply failed: $TF_ENV ($TARGET_ACCOUNT_ID)"  fi

TF_ENV_CAPITALIZE について: devDevprodProd のように頭文字を大文字にした値です。IAM ロール名(TerraformApplyRole-Dev)との一致が必要なため、buildspec 内で変換します。

# buildspec 内での変換例- TF_ENV_CAPITALIZE=$(echo $TF_ENV | python3 -c "import sys; print(sys.stdin.read().strip().capitalize())")

8-4. Terraform での OIDC role assume chain

provider "aws" ブロックで assume_role を使うと、Terraform 実行中に自動でクロスアカウント AssumeRole を行えます。buildspec での STS 操作が不要になるため、よりシンプルな構成です。

# environments/dev/providers.tfprovider "aws" {  region = "ap-northeast-1"  assume_role { role_arn  = "arn:aws:iam::111111111111:role/TerraformApplyRole-Dev" session_name = "terraform-dev-pipeline" external_id  = "terraform-cicd-dev"  }}
# environments/prod/providers.tfprovider "aws" {  region = "ap-northeast-1"  assume_role { role_arn  = "arn:aws:iam::222222222222:role/TerraformApplyRole-Prod" session_name = "terraform-prod-pipeline" external_id  = "terraform-cicd-prod"  }}

provider assume_role と buildspec AssumeRole の比較

方法メリットデメリット
provider assume_roleTerraformコードで宣言的に設定provider.tf が環境ごとに異なるコードになる
buildspec AssumeRolebuildspec を1本に集約できるjq インストールが必要・セッション管理が複雑

小規模なマルチアカウント構成では provider assume_role が推奨です。

role_arn を変数で切り替える設計

# environments/dev/providers.tf(変数方式)variable "target_role_arn" {  description = "Cross-account IAM role ARN for Terraform"  type  = string}provider "aws" {  region = "ap-northeast-1"  assume_role { role_arn  = var.target_role_arn session_name = "terraform-${var.environment}" external_id  = "terraform-cicd-${var.environment}"  }}
# environments/dev/terraform.tfvarstarget_role_arn = "arn:aws:iam::111111111111:role/TerraformApplyRole-Dev"environment  = "dev"

8-5. マルチアカウント構成の Terraform コード(完全版)

管理アカウント側:CodeBuild ロールとクロスアカウント設定

# management/codebuild_role.tfresource "aws_iam_role" "codebuild_apply" {  name = "codebuild-apply-role"  assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{Effect = "Allow"Principal = { Service = "codebuild.amazonaws.com" }Action = "sts:AssumeRole" }]  })}resource "aws_iam_role_policy" "codebuild_crossaccount" {  name = "crossaccount-assume-role"  role = aws_iam_role.codebuild_apply.id  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect= "Allow"  Action= "sts:AssumeRole"  Resource = [ "arn:aws:iam::${var.dev_account_id}:role/TerraformApplyRole-Dev", "arn:aws:iam::${var.prod_account_id}:role/TerraformApplyRole-Prod"  ]},{  Effect = "Allow"  Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"  ]  Resource = "*"},{  Effect = "Allow"  Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]  Resource = [ "arn:aws:s3:::${var.pipeline_artifact_bucket}", "arn:aws:s3:::${var.pipeline_artifact_bucket}/*"  ]} ]  })}variable "dev_account_id" {  default = "111111111111"}variable "prod_account_id" {  default = "222222222222"}variable "pipeline_artifact_bucket" {  default = "myorg-codepipeline-artifacts"}

各環境アカウント側:信頼ポリシーと実行ポリシー

# dev-account/terraform_role.tf# このコードは dev アカウント (111111111111) で terraform apply するvariable "management_account_id" {  default = "123456789012"}resource "aws_iam_role" "terraform_apply_dev" {  name = "TerraformApplyRole-Dev"  assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{Effect = "Allow"Principal = {  AWS = "arn:aws:iam::${var.management_account_id}:role/codebuild-apply-role"}Action = "sts:AssumeRole"Condition = {  StringEquals = { "sts:ExternalId" = "terraform-cicd-dev"  }} }]  })}resource "aws_iam_role_policy" "terraform_apply_dev_policy" {  name = "terraform-apply-dev-policy"  role = aws_iam_role.terraform_apply_dev.id  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect= "Allow"  Action= ["ec2:*", "s3:*", "rds:*", "iam:*", "cloudwatch:*", "logs:*"]  Resource = "*"},{  Effect = "Allow"  Action = [ "s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject", "s3:GetBucketVersioning"  ]  Resource = [ "arn:aws:s3:::myorg-terraform-state-dev", "arn:aws:s3:::myorg-terraform-state-dev/*"  ]},{  Effect = "Allow"  Action = [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable"  ]  Resource = "arn:aws:dynamodb:ap-northeast-1:111111111111:table/terraform-state-lock-dev"} ]  })}
# prod-account/terraform_role.tf# このコードは prod アカウント (222222222222) で terraform apply するvariable "management_account_id" {  default = "123456789012"}resource "aws_iam_role" "terraform_apply_prod" {  name = "TerraformApplyRole-Prod"  assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{Effect = "Allow"Principal = {  AWS = "arn:aws:iam::${var.management_account_id}:role/codebuild-apply-role"}Action = "sts:AssumeRole"Condition = {  StringEquals = { "sts:ExternalId" = "terraform-cicd-prod"  }} }]  })}resource "aws_iam_role_policy" "terraform_apply_prod_policy" {  name = "terraform-apply-prod-policy"  role = aws_iam_role.terraform_apply_prod.id  policy = jsonencode({ Version = "2012-10-17" Statement = [{  Effect= "Allow"  Action= ["ec2:*", "s3:*", "rds:*", "iam:*", "cloudwatch:*", "logs:*"]  Resource = "*"},{  Effect = "Allow"  Action = [ "s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject", "s3:GetBucketVersioning"  ]  Resource = [ "arn:aws:s3:::myorg-terraform-state-prod", "arn:aws:s3:::myorg-terraform-state-prod/*"  ]},{  Effect = "Allow"  Action = [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable"  ]  Resource = "arn:aws:dynamodb:ap-northeast-1:222222222222:table/terraform-state-lock-prod"} ]  })}

Section 8 まとめ

設定箇所アカウント内容
CodeBuild 実行ロール管理アカウント (123456789012)sts:AssumeRole を dev/prod ロールに許可
TerraformApplyRole-Devdev アカウント (111111111111)管理アカウントの CodeBuild からの AssumeRole を許可
TerraformApplyRole-Prodprod アカウント (222222222222)管理アカウントの CodeBuild からの AssumeRole を許可(ExternalId 必須)
buildspec.yml管理アカウントSTS AssumeRole → Terraform init/plan/apply を実行
provider.tf(代替案)各環境の Terraform コードassume_role ブロックで宣言的に設定

マルチアカウント構成を採用することで、prod アカウントへの直接アクセス権限を持つ人間をゼロに近づけ、全ての変更を CodePipeline 経由の自動化フローに集約できます。これがエンタープライズ環境でのTerraform CI/CDの到達点です。


9. 運用・モニタリング・セキュリティ強化

9-1. CloudTrailによる監査証跡

CodePipeline/CodeBuildの操作はすべてAWS CloudTrailに記録されます。インフラ変更の監査証跡として活用します。

CloudTrailで記録される主要イベント

EventNameサービス意味
StartPipelineExecutionCodePipelineパイプライン手動起動
PutApprovalResultCodePipelineManualApproval の承認/拒否
StopPipelineExecutionCodePipelineパイプラインの手動停止
UpdatePipelineCodePipelineパイプライン設定変更
StartBuildCodeBuildビルド起動
BatchDeleteBuildsCodeBuildビルド履歴削除

CloudTrail Lakeでの監査クエリ

-- 過去30日のManualApproval操作一覧SELECT  eventTime,  userIdentity.arn AS approver,  requestParameters.result.status AS action,  requestParameters.result.summary AS commentFROM  aws_cloudtrail_logsWHERE  eventSource = 'codepipeline.amazonaws.com'  AND eventName = 'PutApprovalResult'  AND eventTime > DATE_ADD('day', -30, NOW())ORDER BY  eventTime DESC;-- 過去7日でprod applyを実行したIAM EntitySELECT  eventTime,  userIdentity.arn AS executor,  requestParameters.projectName AS projectFROM  aws_cloudtrail_logsWHERE  eventSource = 'codebuild.amazonaws.com'  AND eventName = 'StartBuild'  AND requestParameters.projectName LIKE '%apply%'  AND eventTime > DATE_ADD('day', -7, NOW());

CloudWatch Logsへのエクスポート設定

resource "aws_cloudtrail" "pipeline_audit" {  name  = "terraform-pipeline-audit"  s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id  include_global_service_events = true  is_multi_region_trail= true  enable_log_file_validation = true  event_selector { read_write_type  = "WriteOnly" include_management_events = true data_resource {type= "AWS::CodePipeline::Pipeline"values = ["arn:aws:codepipeline:ap-northeast-1:123456789012:terraform-cicd-pipeline"] }  }}

9-2. CodePipeline EventBridgeアラート

パイプライン失敗時の自動通知

# パイプライン失敗通知ルールresource "aws_cloudwatch_event_rule" "pipeline_failed" {  name  = "terraform-pipeline-failed"  description = "Alert when CodePipeline execution fails"  event_pattern = jsonencode({ source= ["aws.codepipeline"] detail-type = ["CodePipeline Pipeline Execution State Change"] detail = {pipeline = ["terraform-cicd-pipeline"]state = ["FAILED"] }  })}resource "aws_cloudwatch_event_target" "pipeline_failed_sns" {  rule= aws_cloudwatch_event_rule.pipeline_failed.name  target_id = "pipeline-failed-notification"  arn = aws_sns_topic.notifications.arn  input_transformer { input_paths = {pipeline  = "$.detail.pipeline"state  = "$.detail.state"execution = "$.detail.execution-id"time= "$.time" } input_template = "\"[ALERT] Pipeline <pipeline> <state> at <time>. Execution: <execution>. 確認: https://console.aws.amazon.com/codesuite/codepipeline/pipelines/terraform-cicd-pipeline/view\""  }}# ステージレベルの失敗通知resource "aws_cloudwatch_event_rule" "stage_failed" {  name = "terraform-stage-failed"  event_pattern = jsonencode({ source= ["aws.codepipeline"] detail-type = ["CodePipeline Stage Execution State Change"] detail = {pipeline = ["terraform-cicd-pipeline"]state = ["FAILED"] }  })}

Slack連携(Lambda経由)の設計パターン

設計パターン:  EventBridge → Lambda → Slack WebhookLambda関数(Python)の概要:  1. EventBridgeイベントを受け取る  2. パイプライン名・ステージ・ステータスを取得  3. Slack Webhookにメッセージを送信メッセージフォーマット:  🚨 [FAILED] terraform-cicd-pipeline / Apply stage  Execution ID: a1b2c3d4...  確認: https://console.aws.amazon.com/codesuite/...
# EventBridge → Lambda(Slack通知)resource "aws_cloudwatch_event_target" "pipeline_slack" {  rule= aws_cloudwatch_event_rule.pipeline_failed.name  target_id = "pipeline-slack-notification"  arn = aws_lambda_function.slack_notifier.arn}resource "aws_lambda_permission" "eventbridge_invoke" {  statement_id  = "AllowEventBridgeInvoke"  action  = "lambda:InvokeFunction"  function_name = aws_lambda_function.slack_notifier.function_name  principal  = "events.amazonaws.com"  source_arn = aws_cloudwatch_event_rule.pipeline_failed.arn}

9-3. CodeBuildのログ管理

CloudWatch Logsへのbuildspec出力

resource "aws_cloudwatch_log_group" "codebuild_plan" {  name  = "/aws/codebuild/terraform-plan"  retention_in_days = 30  tags = { Project = "terraform-cicd"  }}resource "aws_cloudwatch_log_group" "codebuild_apply" {  name  = "/aws/codebuild/terraform-apply"  retention_in_days = 90  # apply履歴は長めに保持  tags = { Project = "terraform-cicd"  }}

ログ検索クエリ(CloudWatch Logs Insights)

# apply失敗の原因を検索fields @timestamp, @message| filter @message like /Error:|error:|FAILED/| sort @timestamp desc| limit 50# terraform planで破壊的変更を検出fields @timestamp, @message| filter @message like /destroy/| sort @timestamp desc# apply実行時間の統計stats avg(@duration), max(@duration), min(@duration)by bin(1d)

S3へのビルドレポート保存

resource "aws_codebuild_project" "terraform_apply" {  # ... 既存設定 ...  logs_config { cloudwatch_logs {group_name  = "/aws/codebuild/terraform-apply"stream_name = "" } s3_logs {status= "ENABLED"location = "${aws_s3_bucket.pipeline_artifacts.id}/build-reports/apply"encryption_disabled = false }  }}

9-4. パイプライン実行コストの管理

CodePipelineの課金モデル

課金対象単価(2024年時点)備考
アクティブパイプライン$1.00/月/パイプラインV1: 最初の1本無料。V2は別料金体系
アクション実行V2: $0.002/アクションV2パイプライン使用時

CodeBuildの課金モデル

コンピューティングタイプ料金(ap-northeast-1)
BUILD_GENERAL1_SMALL (3.5GB/2 vCPU)$0.005/分
BUILD_GENERAL1_MEDIUM (7GB/4 vCPU)$0.01/分
BUILD_GENERAL1_LARGE (15GB/8 vCPU)$0.02/分

月次コスト試算

小規模チーム(1パイプライン、1日5回push):

CodePipeline: $1.00/月CodeBuild Plan: 5回/日 × 5分 × 30日 = 750分 × $0.005 = $3.75CodeBuild Apply: 5回/日 × 3分 × 20日(承認あり) = 300分 × $0.005 = $1.50合計: 約 $6.25/月

エンタープライズ(5パイプライン、1日20回push):

CodePipeline: $5.00/月CodeBuild Plan: 20回/日 × 5分 × 30日 × 5環境 = 15,000分 × $0.005 = $75.00CodeBuild Apply: 10回/日 × 5分 × 20日 × 5環境 = 5,000分 × $0.005 = $25.00合計: 約 $105/月

コスト削減のベストプラクティス

1. ビルド時間の短縮- Terraformプロバイダーをキャッシュ(LOCAL_DOCKER_LAYER_CACHE)- 変更のないモジュールはtarget指定でplan範囲を限定2. 不要なパイプライン実行の防止- BranchFilterでmainブランチのみトリガー- ファイルパスフィルター(V2パイプライン機能)で.tfファイル変更時のみ実行3. 環境別パイプラインの最適化- dev環境はManualApprovalを省略してコスト削減- stg/prodのみApprovalあり構成に

9-5. セキュリティ強化チェックリスト

以下の項目を定期的に確認し、エンタープライズグレードのセキュリティを維持します。

IAM・権限設計

  • [ ] CodePipelineロールに最小権限が付与されているか(ワイルドカード * の使用を最小化)
  • [ ] CodeBuildのPlan用とApply用ロールを分離しているか(Plan: ReadOnly、Apply: Write)
  • [ ] ManualApprovalのPutApprovalResult権限が承認者のみに付与されているか
  • [ ] クロスアカウント構成でExternalIdが設定されているか(混乱した代理問題対策)
  • [ ] S3バケットポリシーでCodePipeline/CodeBuildのサービスロールのみアクセス可か

ネットワーク・データ保護

  • [ ] S3アーティファクトバケットでSSE-KMS暗号化が有効か
  • [ ] S3バケットへのパブリックアクセスがブロックされているか
  • [ ] S3アクセスログが有効か(誰がplan結果を参照したか追跡)
  • [ ] CodeBuildをVPC内で実行しているか(VPCエンドポイント経由でS3/SSMにアクセス)
  • [ ] CloudTrailでの証跡収集が有効か(マルチリージョン)

パイプライン設計

  • [ ] Approvalなしでprod applyができない構成になっているか
  • [ ] 緊急時のBreakGlass手順が文書化されているか
  • [ ] パイプラインの変更履歴がCloudTrailで追跡可能か
  • [ ] S3のアーティファクトに保持期限(TTL)が設定されているか

定期監査

# IAMポリシーの過剰権限を確認(Access Advisor)aws iam generate-service-last-accessed-details \  --arn arn:aws:iam::123456789012:role/codebuild-apply-role# S3バケットのパブリックアクセスブロック設定を確認aws s3api get-public-access-block \  --bucket myorg-pipeline-artifacts# CloudTrailが有効か確認aws cloudtrail describe-trails \  --query "trailList[?HomeRegion=='ap-northeast-1'].{Name:Name,S3Bucket:S3BucketName,IsLogging:IsLogging}"

Section 9 まとめ

施策効果
CloudTrail監査証跡誰がいつapplyしたか完全記録
EventBridge通知失敗の即時検知・Slack連携
CloudWatch Logsへのビルドログapply失敗原因の迅速な特定
コスト管理不要なビルド実行の削減
セキュリティチェックリスト定期的な権限・設定の棚卸し

10. まとめとシリーズ完結

10-1. 本記事のまとめ

本記事では、AWS CodePipelineとCodeBuildを組み合わせてTerraformのCI/CDパイプラインを構築しました。単純な4ステージパイプラインから始まり、エンタープライズ運用に必要なすべての要素を実装しました。

構築したCodePipelineの全体サマリー

パイプライン構成(全4ステージ):  GitHub (main push) ↓  [Source] CodeStarConnections → S3アーティファクト ↓  [Plan] CodeBuild (terraform plan) → PlanArtifact保存 ↓  [Approve] ManualApproval → SNS通知 → 人間が確認・承認 ↓  [Apply] CodeBuild (terraform apply) → SNS完了通知

マルチ環境・マルチアカウントでの拡張パターン

規模構成追加コスト
単一アカウント・単一環境本記事の基本構成
単一アカウント・dev/stg/prod環境別パイプライン × 3本~$3/月
マルチアカウント管理アカウント + 各環境アカウント+ クロスアカウントIAM設計
エンタープライズOrganizations + Control Tower+ ガードレール・SCP設計

GitHub Actions方式(第2弾)との使い分け最終まとめ

観点GitHub Actions (第2弾)CodePipeline (本記事)
セットアップコスト低(YAMLファイルのみ)高(AWSリソース一式)
AWSサービスとの統合OIDC経由(疎結合)ネイティブ統合(密結合)
ManualApprovalPR承認フローで代替専用UIとSNS通知
コストGitHub Actionsの無料枠CodeBuild従量課金
監査・コンプライアンスGitHubの監査ログCloudTrail完全統合
推奨シーンスタートアップ・スモールチームエンタープライズ・金融・官公庁

10-2. シリーズ全体の振り返り

本シリーズ全3弾を通じて、Terraform複数人開発の完全像を構築しました。

第1弾: state管理・lockで複数人開発の基盤を整えた

  • S3 + DynamoDBによるstate管理とロック機構を導入
  • state divergence(ドリフト)の検出と修復
  • 複数人がterraform applyを同時実行しても安全な環境

第2弾: GitHub Actions + OIDCでPR駆動CI/CDを実現した

  • OIDCによる認証情報不要のAWS連携
  • PRトリガーでterraform planを自動実行
  • Atlantis的なPRレビューとapplyの統合フロー

第3弾(本記事): CodePipeline + CodeBuildでエンタープライズグレードのCI/CDを構築した

  • AWS完全マネージドなCI/CDパイプライン
  • ManualApprovalによる人間の確認ゲート
  • マルチ環境・マルチアカウントへのスケールアウト
  • CloudTrailによる完全な監査証跡

3弾を通して得られたTerraform複数人開発の完全像

開発フロー(完成形):  1. 開発者がfeatureブランチで変更  2. PRを作成 → GitHub Actions で terraform plan 自動実行  3. コードレビュー + plan結果レビュー → マージ  4. mainマージ → CodePipeline起動  5. Planステージ → ManualApproval → Applyステージ  6. CloudTrailに全操作を記録  7. SNS/Slackで完了通知

10-3. 次のステップ(読者へ)

本シリーズで構築した基盤をさらに発展させるための方向性を紹介します。

Atlantis(Terraform専用CI/CD)との比較

AtlantisはTerraform専用のCI/CDツールで、GitHubのPRコメントからplanとapplyを操作できます。

比較軸AtlantisCodePipeline
セットアップEC2/EKSへの自己ホストフルマネージド
PR連携PRコメントで操作(atlantis plan/apply)別途GitHub Actionsと組み合わせ
AWSネイティブなし完全統合
適合シーンTerraform専用チーム・OSS重視AWSオールイン環境

Terraform Cloud/HCP Terraformとの比較

HashiCorp公式のマネージドプラットフォームです。

比較軸HCP TerraformCodePipeline
料金有料(Free枠あり)AWSの従量課金
state管理組み込み別途S3+DynamoDB
policy-as-codeSentinel対応SCP/CloudFormationで代替
マルチクラウド対応(Azure/GCP)AWS専用

AWS Organizations + Control Towerの活用

本記事のマルチアカウント構成を組織規模にスケールさせる場合:

発展構成:  Control Tower → ランディングゾーン自動セットアップ  Organizations SCP → 各アカウントへのガードレール適用  AWS Config → インフラのコンプライアンス継続評価  Security Hub → セキュリティ所見の一元管理

Lambda/ECSへのCI/CD拡張

Terraform以外のリソースにも同じパイプラインパターンを適用できます:

Lambda CI/CD:  GitHub → CodePipeline → CodeBuild (build + test) → ManualApproval → Lambda DeployECS CI/CD:  GitHub → CodePipeline → CodeBuild (Docker build) → ECR Push → ECS Blue/Green Deploy

10-4. シリーズリンクまとめ

AWS×Terraform 複数人開発シリーズ(全3弾)

第1弾から読む

本シリーズが、あなたのチームのTerraform CI/CD構築の参考になれば幸いです。AWS CodePipelineを使ったエンタープライズグレードのインフラ自動化で、安全・確実・監査可能なデプロイフローを実現してください。