- 1 AWS CodePipeline×CodeBuildで構築するTerraform CI/CD — エンタープライズ運用パターン
- 1.1 1. この記事について
- 1.2 2. CodePipeline × CodeBuild 全体アーキテクチャ
- 1.3 3. 前提リソースのセットアップ
- 1.4 4. SourceステージとPlanステージのCodeBuild設定
- 1.5 5. マニュアル承認ステージ
- 1.6 6. Apply BuildステージとCodeBuild設定
- 1.7 7. マルチ環境構成(dev/stg/prod)
- 1.8 8. マルチアカウント構成
- 1.9 9. 運用・モニタリング・セキュリティ強化
- 1.10 10. まとめとシリーズ完結
AWS CodePipeline×CodeBuildで構築するTerraform CI/CD — エンタープライズ運用パターン
- 第1弾(公開済み): 複数人開発の基盤 — state管理・lock・drift対策
- 第2弾(公開済み): PR駆動CI/CD — GitHub Actions+OIDCで複数人レビューフローを構築
- 第3弾(本記事): AWS CodePipeline×CodeBuildで構築するTerraform CI/CD
関連シリーズ(前提知識):
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つの理由を示す。
- AWSネイティブな監査証跡: すべてのパイプライン操作がCloudTrailに記録され、「誰が・いつ・何をApproveしたか」をAWS上で一元管理できる
- マニュアル承認の標準機能:
Manual Approvalステージが組み込み機能として提供されており、applyの前に人間の承認を挟む設計が容易 - 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 Role | AWS IAMのみで完結(一元管理) |
| コスト | GitHub Actionsの実行時間料金 | CodePipeline実行回数 + CodeBuild実行時間 |
| マルチアカウント対応 | 可能(Cross-account Role設定が必要) | 可能(Cross-account Action標準サポート) |
| マルチ環境構成 | ワークフローの分岐で対応 | ステージ追加で対応(GUI/Terraform両方) |
| Slack/SNS通知 | GitHub Actions + Slack Action | SNS + 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上での権限管理を一元化したい組織
前提知識
本記事は以下の知識を前提としている。未習得の場合は先にリンク先を参照してほしい。
| 分野 | 必要な知識 | 参照先 |
|---|---|---|
| Terraform | init / plan / apply の実行経験・リモートstate・module | Terraform実践 |
| state管理 | S3+DynamoDBバックエンド・ロックの仕組み | 第1弾 |
| OIDC基礎 | IAM Role・信頼ポリシー・OIDC認証フロー | 第2弾 |
| AWS CodePipeline | パイプライン・ステージ・アクションの概念 | AWS公式ドキュメント |
| AWS CodeBuild | buildspec.yml・ビルドプロジェクトの概念 | AWS公式ドキュメント |
| AWS IAM | Role・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各フェーズの詳細説明
| フェーズ | 実行内容 | 失敗時の動作 |
|---|---|---|
install | Terraform本体・TFLintをインストール | インストール失敗 → ビルド失敗 |
pre_build | init・fmt check・validate | フォーマット違反・構文エラー → ビルド失敗 |
build | terraform plan実行・JSON保存 | planエラー(exit=1) → ビルド失敗。変更あり(exit=2)は正常継続 |
post_build | plan結果のサマリー表示 | 失敗しても実行される |
-detailed-exitcodeの使い方:terraform plan -detailed-exitcodeは終了コード2を「変更あり(正常)」として返す。シェルでは$?が2でも失敗と見なさないようPIPESTATUSで明示的に制御する必要がある。exit 1(エラー)のみ失敗として扱い、exit 2(変更あり)は正常終了させる点がポイントだ。
4-2. Plan用CodeBuildプロジェクトの作成
AWSコンソール手順
- CodeBuild → 「ビルドプロジェクトを作成する」
- プロジェクト名:
terraform-plan-project - 説明: 「Terraform plan ステージ用 CodeBuild プロジェクト」
- ソース
- ソースプロバイダー: 「AWS CodePipeline」(CodePipelineから呼ばれる場合はこれを選択)
- 環境
- 環境イメージ: 「マネージドイメージ」
- オペレーティングシステム: 「Amazon Linux」
- ランタイム: 「Standard」
- イメージ: 「aws/codebuild/amazonlinux2-x86_64-standard:5.0」(最新を選択)
- イメージのバージョン: 「このランタイムバージョンの最新イメージを常に使用する」
- 環境タイプ: 「Linux EC2」
- 特権モード: オフ(Dockerビルドが不要な場合)
- サービスロール: 「既存のサービスロール」→
codebuild-plan-roleを選択 - Buildspec
- 「buildspecファイルを使用する」を選択
- Buildspec名:
buildspec-plan.yml - アーティファクト
- タイプ: 「Amazon S3」
- バケット名:
myorg-pipeline-artifacts - 名前:
plan-artifacts - パスの削除: オフ
- ログ
- CloudWatch Logs: 有効
- グループ名:
/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コンソール手順
- CodePipeline → 「パイプラインを作成する」
- パイプライン設定
- パイプライン名:
terraform-cicd-pipeline - パイプラインタイプ: V2(変数・条件・フィルタリングをサポート)
- 実行モード: 「キュー」(前の実行を待ってから次を開始)
- サービスロール: 「既存のサービスロール」→
codepipeline-roleを選択 - アドバンスト設定
- アーティファクトストア: 「カスタムロケーション」→
myorg-pipeline-artifacts - 暗号化キー: 「デフォルトAWSマネージドキー」
- Sourceステージ
- ソースプロバイダー: 「GitHub(GitHub App)」※CodeStar Connectionsを使用
- 接続: 先ほど作成した
github-myorg-connection - リポジトリ名:
myorg/terraform-repo - ブランチ名:
main - パイプライントリガー: 「特定のブランチへのプッシュ」(デフォルト)
- 出力アーティファクト形式: 「CodePipeline形式」
- ビルドステージ(Plan)
- ビルドプロバイダー: 「AWS CodeBuild」
- リージョン:
ap-northeast-1 - プロジェクト名:
terraform-plan-project - ビルドタイプ: 「単一ビルド」
- 入力アーティファクト:
SourceArtifact - 出力アーティファクト:
PlanArtifact - 「パイプラインを作成する」をクリック
パイプラインタイプ 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 main2. 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 sources | CodeBuildのIAMロールにS3アクセス権限がない | codebuild-plan-role にstateバケットへのGetObject/ListBucket権限を追加 |
Error: Failed to install provider | プロバイダーのダウンロードに失敗 | NAT Gatewayまたはインターネットアクセスを確認(プライベートサブネットで実行している場合) |
The action failed because no branch named main exists | Sourceステージでブランチ名が不一致 | GitHubのデフォルトブランチ名(main vs master)を確認 |
Error: Connection 'github-myorg-connection' is not AVAILABLE | CodeStarConnectionsのOAuth認証が未完了 | AWSコンソールのConnections画面でOAuth認証を完了させる |
╷ Error: Reference to undeclared resource | Terraformコードの構文エラー | 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 authorized | IAMロールの権限が不足 | 不足しているアクション・リソースをポリシーに追加 |
パイプラインの手動再実行
# 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 tableSection 4 まとめ
| 設定内容 | リソース/ファイル | 確認ポイント |
|---|---|---|
| buildspec-plan.yml | Gitリポジトリ | init・fmt・validate・plan が順に実行される |
| terraform-plan-project | CodeBuildプロジェクト | IAMロールがcodebuild-plan-roleに設定されている |
| terraform-cicd-pipeline | CodePipeline | V2・QUEUED・アーティファクトストアが正しい |
| Sourceステージ | CodeStarConnections | 接続ステータスがAVAILABLEである |
| Planステージ | CodeBuild | plan出力が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 ファイル) | チームメンバー全員 |
| ManualApproval | terraform 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コンソール手順
- CodePipeline → terraform-cicd-pipeline を選択
- 「パイプラインを編集」をクリック
- Planステージの後ろの「+」ボタンをクリック → 「ステージを追加」
- ステージ名:
Approve - 「アクショングループを追加」をクリック
アクション設定:
| 項目 | 値 |
|—|—|
| アクション名 | ManualApproval |
| アクションプロバイダー | Manual |
| SNS トピック ARN | arn:aws:sns:ap-northeast-1:123456789012:terraform-pipeline-notifications |
| URL(任意) | CodePipelineコンソールURL |
| コメント(任意) | terraform plan結果を確認してApproveしてください |
- 「完了」→「保存」をクリック
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コンソールで確認
- AWSコンソール → CodePipeline → terraform-cicd-pipeline
- Planステージの「詳細」をクリック
- 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 replacement | replace = 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コンソールから承認操作
- コンソール → Approveステージ → 「レビュー」
- コメント:
plan確認OK — EC2追加のみ、destroyなし - 「承認」をクリック
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_output | PlanステージのアーティファクトをApplyステージが受け取る環境変数 |
-auto-approve tfplan | ユーザー入力なしで保存済みplanを適用 |
terraform output -json | apply後のリソース出力をアーティファクトとして保存 |
| キャッシュ設定 | プロバイダーキャッシュでビルド時間を短縮 |
6-2. Apply用CodeBuildプロジェクトの作成
AWSコンソール手順
- CodeBuild → 「ビルドプロジェクトを作成」
| 設定項目 | 値 |
|---|---|
| プロジェクト名 | terraform-apply-project |
| ソース | No source(CodePipelineから受け取る) |
| 環境イメージ | マネージドイメージ / Amazon Linux 2023 |
| ランタイム | Standard |
| イメージ | aws/codebuild/standard:7.0 |
| サービスロール | codebuild-apply-role |
| Buildspec | buildspecファイルを使用 → buildspec-apply.yml |
| ログ | CloudWatch Logs: /aws/codebuild/terraform-apply |
- 「ビルドプロジェクトを作成」をクリック
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 denied | IAMロールに権限不足 | 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_ACTIONS6-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/view6-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 mainStep 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.yml | Gitリポジトリ | tfplanを受け取り、apply -auto-approve で実行 |
| terraform-apply-project | CodeBuildプロジェクト | codebuild-apply-role が設定されている |
| Apply ステージ | CodePipeline | plan_output を入力アーティファクトとして受け取る |
| EventBridge通知 | CloudWatch Events | SUCCEEDED/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.tfstate7-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_ENV と TF_DIR という環境変数で行います。これにより、buildspec.yml を1本書けば全環境に使い回せます。
7-4. 環境別 buildspec.yml の分岐
CodeBuild の buildspec.yml で TF_ENV と TF_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_DIRterraform-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 = true7-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-bucketPR を作成して 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-devStep 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-stgStep 4: stg 承認後、prod 昇格
prod の承認ゲートで確認すること: ✅ plan で destroy が発生していないこと ✅ 変更対象のリソースが意図したものだけであること ✅ stg 環境での動作確認が完了していること ✅ 本番リリースの時間帯として適切かaws s3 ls | grep myorg-app-data-prodStep 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)" fiTF_ENV_CAPITALIZE について: dev → Dev、prod → Prod のように頭文字を大文字にした値です。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_role | Terraformコードで宣言的に設定 | provider.tf が環境ごとに異なるコードになる |
| buildspec AssumeRole | buildspec を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-Dev | dev アカウント (111111111111) | 管理アカウントの CodeBuild からの AssumeRole を許可 |
| TerraformApplyRole-Prod | prod アカウント (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 | サービス | 意味 |
|---|---|---|
StartPipelineExecution | CodePipeline | パイプライン手動起動 |
PutApprovalResult | CodePipeline | ManualApproval の承認/拒否 |
StopPipelineExecution | CodePipeline | パイプラインの手動停止 |
UpdatePipeline | CodePipeline | パイプライン設定変更 |
StartBuild | CodeBuild | ビルド起動 |
BatchDeleteBuilds | CodeBuild | ビルド履歴削除 |
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経由(疎結合) | ネイティブ統合(密結合) |
| ManualApproval | PR承認フローで代替 | 専用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を操作できます。
| 比較軸 | Atlantis | CodePipeline |
|---|---|---|
| セットアップ | EC2/EKSへの自己ホスト | フルマネージド |
| PR連携 | PRコメントで操作(atlantis plan/apply) | 別途GitHub Actionsと組み合わせ |
| AWSネイティブ | なし | 完全統合 |
| 適合シーン | Terraform専用チーム・OSS重視 | AWSオールイン環境 |
Terraform Cloud/HCP Terraformとの比較
HashiCorp公式のマネージドプラットフォームです。
| 比較軸 | HCP Terraform | CodePipeline |
|---|---|---|
| 料金 | 有料(Free枠あり) | AWSの従量課金 |
| state管理 | 組み込み | 別途S3+DynamoDB |
| policy-as-code | Sentinel対応 | 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 Deploy10-4. シリーズリンクまとめ
- 第1弾: 複数人開発の基盤 — state管理・lock・drift対策
- 第2弾: PR駆動CI/CD — GitHub Actions+OIDCで複数人レビューフローを構築
- 第3弾(本記事): AWS CodePipeline×CodeBuildで構築するTerraform CI/CD — エンタープライズ運用パターン
本シリーズが、あなたのチームのTerraform CI/CD構築の参考になれば幸いです。AWS CodePipelineを使ったエンタープライズグレードのインフラ自動化で、安全・確実・監査可能なデプロイフローを実現してください。