1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraform × CodePipeline × CodeBuildでCI/CDパイプラインを構築し、ドリフト検出まで自動化してみた

1
Last updated at Posted at 2026-02-26

はじめに

本記事では、Terraform × CodePipeline × CodeBuildでCI/CDパイプラインを構築し、Drift検出まで自動化してみます。

全体アーキテクチャ

本ハンズオンで構築するパイプラインの全体像はこちらです。
image.png

以下2つのパイプラインを作成します。

  • GitHubのmainブランチへのpushをトリガーとしたterraform実行によるCI/CDパイプライン
  • 指定時刻に実行されるドリフト検知用パイプライン

1. 事前準備

1.1 ツールのインストール

Terraform CLI

以下リンクから最新バージョンのwindows用terraformをダウンロードします。
https://developer.hashicorp.com/terraform/install
image.png

ダウンロードしたバイナリファイルを解凍し、実行ファイルなどを任意の箇所へ保存します。

AWS CLI V2

以下リンクからV2のwindows用AWSCLIをダウンロードします。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

image.png

ダウンロードしたインストーラーを使用し、インストールします。

Git

以下リンクから最新バージョンのwindows用gitをダウンロードします。
https://git-scm.com/install/windows

image.png

ダウンロードした実行ファイルを使用し、インストールします。

1.2 AWS認証設定

以下のコマンドを実行し、awsの認証情報を設定します。ここで設定した認証情報を使用して、terraformからawsにリソースを作成していきます。今回は検証なので、使用する権限はAdminです。

aws configure
# AWS Access Key ID: xxxxxxxxxx
# AWS Secret Access Key: xxxxxxxxxx
# Default region name: ap-northeast-1
# Default output format: json

1.3 ディレクトリ構成

すべてのリソースをmoduleに分離し、ルートのmain.tfから呼び出す構成です。

terraform-cicd-demo/
├── main.tf                  # ルート: 全moduleの呼び出し
├── variables.tf             # ルート: 入力変数
├── outputs.tf               # ルート: 出力
├── backend.tf               # S3バックエンド設定
├── terraform.tfvars         # 変数の値
├── modules/
│   ├── tf-backend/          # S3ステートバケット
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── pipeline/            # CodePipeline + 支援リソース(S3, KMS, SNS, IAM)
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── codebuild/           # CodeBuildプロジェクト群 + IAM
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── buildspec/               # ビルド実行時に実行するコマンドを記述したファイル
    ├── buildspec_validate.yml
    ├── buildspec_plan.yml
    ├── buildspec_apply.yml
    └── buildspec_drift.yml

terraformはplan,apply実行時のパスと同階層にあるtfの拡張子がついたファイルを読み込む。ファイル名は特に指定されておらず、自由につけることができる。また、main.tfなどの1つのファイルにすべての定義を集約することも可能。ただ、ベストプラクティスとして、以下のように用途別にファイルを分割することが推奨されている。
main.tf : メインのリソース定義、モジュールの呼び出し定義
modules/main.tf : モジュールとして使用するリソース定義
imports.tf : インポートするリソース定義
variables.tf : 変数定義(variableブロックで定義する)
outputs.tf 出力定義(outputブロックで定義する)
terraform.tfvars : 変数の値
backend.tf : バックエンド定義
providers.tf : プロバイダーの設定

2. S3バックエンドの構築

Terraformのステートファイルをリモートで安全に管理するために、S3バケットを最初に作成します。

Terraformで排他制御のためにS3を使う理由
ローカル保存だと以下の問題がある。

  • チームで状態ファイルが同期されず、各自バラバラの状態になる
  • 同時にterraform applyするとステートファイルが壊れる
  • PCが壊れたら状態ファイル消失でインフラ管理不能

解決策としてS3を使う。ステートファイルをチーム全体で共有・暗号化・バージョン管理できる。ロックファイルを用いて同時実行を防ぐロック機能を実現する。

2.1 バックエンド用リソースの定義

ステートファイルを保存するS3バケットを定義したtfファイルを作成します。

# terraform-cicd-demo/modules/tf-backend/main.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-cicdpipeline-verification"

  lifecycle {
    prevent_destroy = true # リソース削除時Terraformがエラーを出して処理を中断する
  }

  tags = {
    Name        = "Terraform State"
    Environment = "shared"
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

また、別モジュールにて使用する値を出力するためにoutput.tfも作成します。

# terraform-cicd-demo/modules/tf-backend/output.tf
output "bucket_arn" {
  value = aws_s3_bucket.terraform_state.arn
}

エントリポイントとなるmain.tfをプロジェクトフォルダのルートに作成します。全moduleを呼び出し、各moduleに変数を渡し、outputで依存関係をつなぎます。
バックエンド用リソースの定義ファイルをモジュールとして呼び出します。

# terraform-cicd-demo/main.tf

module "tf_backend" {
  source = "./modules/tf-backend/"
}

2.2 初回適用とバックエンド移行

以下コマンド実行前にプロバイダー情報を記載したproviders.tfファイルを用意します。

# terraform-cicd-demo/providers.tf
provider "aws" {
  region = "ap-northeast-1"
}

ルートに配置したmain.tfにて定義したリソースを作成するため、以下のコマンドを実行します。
コマンド実行後、ローカルにステートファイル(xxx.tfstate)が作成されます。

initを実行し、環境の初期化を行います。

# terraform-cicd-demo/ で実行
> terraform init
C:\terraform-cicd-demo>terraform init
Initializing the backend...
Initializing modules...
- tf-backend in modules\tf-backend
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v6.31.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

planを実行し、applyが正常に行えるかを確認します。

# terraform-cicd-demo/ で実行
> terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # module.tf_backend.aws_s3_bucket.terraform_state will be created
  + resource "aws_s3_bucket" "terraform_state" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "my-terraform-state-cicdpipeline-verification"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + request_payer               = (known after apply)
      + tags                        = {
          + "Environment" = "shared"
          + "ManagedBy"   = "terraform"
          + "Name"        = "Terraform State"
        }
      + tags_all                    = {
          + "Environment" = "shared"
          + "ManagedBy"   = "terraform"
          + "Name"        = "Terraform State"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # module.tf_backend.aws_s3_bucket_public_access_block.terraform_state will be created
  + resource "aws_s3_bucket_public_access_block" "terraform_state" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "ap-northeast-1"
      + restrict_public_buckets = true
    }

  # module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state will be created
  + resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + rule {
          + blocked_encryption_types = []
          + bucket_key_enabled       = true

          + apply_server_side_encryption_by_default {
              + sse_algorithm     = "aws:kms"
                # (1 unchanged attribute hidden)
            }
        }
    }

  # module.tf_backend.aws_s3_bucket_versioning.terraform_state will be created
  + resource "aws_s3_bucket_versioning" "terraform_state" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

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

問題なくplanに成功したため、applyを実行します。

# terraform-cicd-demo/ で実行
> terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # module.tf_backend.aws_s3_bucket.terraform_state will be created
  + resource "aws_s3_bucket" "terraform_state" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "my-terraform-state-cicdpipeline-verification"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + request_payer               = (known after apply)
      + tags                        = {
          + "Environment" = "shared"
          + "ManagedBy"   = "terraform"
          + "Name"        = "Terraform State"
        }
      + tags_all                    = {
          + "Environment" = "shared"
          + "ManagedBy"   = "terraform"
          + "Name"        = "Terraform State"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # module.tf_backend.aws_s3_bucket_public_access_block.terraform_state will be created
  + resource "aws_s3_bucket_public_access_block" "terraform_state" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "ap-northeast-1"
      + restrict_public_buckets = true
    }

  # module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state will be created
  + resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + rule {
          + blocked_encryption_types = []
          + bucket_key_enabled       = true

          + apply_server_side_encryption_by_default {
              + sse_algorithm     = "aws:kms"
                # (1 unchanged attribute hidden)
            }
        }
    }

  # module.tf_backend.aws_s3_bucket_versioning.terraform_state will be created
  + resource "aws_s3_bucket_versioning" "terraform_state" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.tf_backend.aws_s3_bucket.terraform_state: Creating...
module.tf_backend.aws_s3_bucket.terraform_state: Creation complete after 1s [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Creating...
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Creating...
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Creating...
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Creation complete after 1s [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Creation complete after 1s [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Creation complete after 2s [id=my-terraform-state-cicdpipeline-verification]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

provider.tf : リソース作成、管理環境との接続設定ファイル。どのクラウド(AWS/Azure/GCP)を使うか、どのリージョンで作業するかを設定する。
terraform init : 実行環境の初期化を行う。provider.tfに記載のあるプロバイダーのプラグイン(今回はAWSへ作業を行うためのツール)のダウンロードやローカルにあるmodulesフォルダの登録など。
terraform plan : 何が作られるか/変更されるか/削除されるかを表示する。実際には何も作られない。
terraform apply : 実際にリソースを作成する
ステートファイル : Terraformが管理するリソースの記録。現在の状態との変更点を比較する際に使用される。

2.3 バックエンド設定

ステートファイルの管理をS3へ移行するために必要な設定を行います。

# terraform-cicd-demo/backend.tf
terraform {
  backend "s3" {
    bucket       = "my-terraform-state-cicdpipeline-verification"
    key          = "terraform.tfstate"
    region       = "ap-northeast-1"
    encrypt      = true
    use_lockfile = true # S3ベースのステートロック使用時に設定
  }
}

以下のコマンドを実行し、backend.tfを読み込みステートファイルをローカルからS3へ移行します。

# terraform-cicd-demo/ で実行
>terraform init -migrate-state
Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the newly
  configured "s3" backend. Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v6.31.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

これまでステートファイルのロックはDynamoDBベースだったが、現在は非推奨となっており、将来のマイナーバージョンで削除される予定となっている。S3ベースのステートロックがv1.11でGA(一般提供)され、推奨されている。そのため今回の検証ではそちらを使用している。

2.4 実行結果確認

「2.3 バックエンド設定」を経て、想定通りの状態になっているかを確認します。

S3にステートファイルが存在するか確認

バックエンド設定でのコマンド実行前
image.png
実行後
image.png

想定通りステートファイルがS3バケットに保管されています。

バージョニングの確認

image.png

S3バケットのバージョニングが有効化されています。

ロックの動作確認

別ターミナルで同時にplanを実行してロックエラーを確認します。

ターミナル1

> terraform plan
aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]

No changes. Your infrastructure matches the configuration.

ターミナル2

> terraform plan
│ Error: Error acquiring the state lock
│
│ Error message: operation error S3: PutObject, https response error StatusCode: 412, RequestID: AH999CP1Y803NRCR, HostID: +/HQE91RZVG/+4fZRUPDXdQADod3QJDyvD5ds0ubvnOxfuyfehRartn1JeWS8TG1P2rspIPoemo=, api
│ error PreconditionFailed: At least one of the pre-conditions you specified did not hold
│ Lock Info:
│   ID:        46de9cd5-71d6-e8bd-f162-b5459f55d225
│   Path:      my-terraform-state-cicdpipeline-verification/terraform.tfstate
│   Operation: OperationTypePlan
│   Who:       xxx
│   Version:   1.14.4
│   Created:   2026-02-11 12:02:03.5195666 +0000 UTC
│   Info:
│
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.

ターミナル1でS3のステートファイルをロックしているため、ターミナル2でロックエラーが起きることを確認できました。

ロックファイルが一時的に作成される。
image.png

ステートファイルのロック時の流れ

開発者A: terraform plan, apply 開始
↓
S3: ロックファイル作成
↓
開発者A: 作業完了
↓
S3: ロックファイル削除
↓
開発者B: terraform apply 開始
↓
S3: ロックファイル作成

ロックが解除されていない状態で別の作業者がロックしようとした時の流れ

開発者A: terraform plan, apply 開始
↓
S3: ロックファイル作成
↓
開発者B: terraform plan, apply 開始
↓
ロックエラー発生

エラーが発生する理由
ロックファイルが存在してる状態で、別のユーザがロックファイルを作成しようとするとエラーになる

3. CodePipeline + CodeBuildで自動デプロイ

3.1 ソースリポジトリの選択

2024年7月に一度非推奨とされたCodeCommitは、2025年11月にGA復帰しました。本ハンズオンではGitHubをソースとして使用しますが、CodeCommitやCodeCatalystへの差し替えも可能です。

3.2 パイプラインリソース作成

CodePipelineの定義と共にその中で参照するS3バケット、KMSキー、SNSトピック、IAMロールを作成します。

# terraform-cicd-demo/modules/pipeline/main.tf

data "aws_caller_identity" "current" {}

# ---- アーティファクト用S3バケット ----
resource "aws_s3_bucket" "pipeline_artifacts" {
  bucket = "${var.project_name}-artifacts-${data.aws_caller_identity.current.account_id}"

  tags = {
    ManagedBy  = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "pipeline_artifacts" {
  bucket = aws_s3_bucket.pipeline_artifacts.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" {
  bucket = aws_s3_bucket.pipeline_artifacts.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# ---- アーティファクト暗号化用KMSキー ----
resource "aws_kms_key" "pipeline" {
  description             = "KMS key for CodePipeline artifact encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = {
    Name       = "Pipeline KMS Key"
    ManagedBy  = "terraform"
  }
}

resource "aws_kms_alias" "pipeline" {
  name          = "alias/${var.project_name}-pipeline"
  target_key_id = aws_kms_key.pipeline.key_id
}

# ---- 承認通知用SNSトピック ----
resource "aws_sns_topic" "pipeline_approval" {
  name = "${var.project_name}-pipeline_approval"

  tags = {
    ManagedBy  = "terraform"
  }
}

resource "aws_sns_topic_subscription" "approval_email" {
  topic_arn = aws_sns_topic.pipeline_approval.arn
  protocol  = "email"
  endpoint  = var.approval_email # variables.tfで定義
}

# ---- GitHub接続 ----
resource "aws_codestarconnections_connection" "github" {
  name          = "${var.project_name}-github"
  provider_type = "GitHub"
}

# ---- CodePipeline IAMロール ----
resource "aws_iam_role" "pipeline" {
  name = "${var.project_name}-codepipeline-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "codepipeline.amazonaws.com" }
    }]
  })
  tags = {
    ManagedBy  = "terraform"
  }
}

resource "aws_iam_role_policy" "pipeline" {
  name = "${var.project_name}-codepipeline-policy"
  role = aws_iam_role.pipeline.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:PutObject", "s3:GetBucketVersioning"]
        Resource = [aws_s3_bucket.pipeline_artifacts.arn, "${aws_s3_bucket.pipeline_artifacts.arn}/*"]
      },
      {
        Effect   = "Allow"
        Action   = ["codestar-connections:UseConnection"]
        Resource = aws_codestarconnections_connection.github.arn
      },
      {
        Effect   = "Allow"
        Action   = ["codebuild:BatchGetBuilds", "codebuild:StartBuild"]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = ["kms:Decrypt", "kms:DescribeKey", "kms:Encrypt", "kms:GenerateDataKey*", "kms:ReEncrypt*"]
        Resource = aws_kms_key.pipeline.arn
      },
      {
        Effect   = "Allow"
        Action   = ["sns:Publish"]
        Resource = aws_sns_topic.pipeline_approval.arn
      },
      {
        Effect   = "Allow"
        Action   = ["codepipeline:TagResource"]
        Resource = aws_codepipeline.pipeline.arn
      },
    ]
  })
}

# CodePipeline V2
resource "aws_codepipeline" "pipeline" {
  name          = "${var.project_name}-codepipeline"
  role_arn      = aws_iam_role.pipeline.arn
  pipeline_type = "V2"

  artifact_store {
    location = aws_s3_bucket.pipeline_artifacts.id
    type     = "S3"
    encryption_key {
      id   = aws_kms_key.pipeline.arn
      type = "KMS"
    }
  }

  stage {
    name = "Source"
    action {
      name             = "GitHub"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_output"]
      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.github.arn
        FullRepositoryId = var.github_repo
        BranchName       = var.github_branch
      }
    }
  }

  stage {
    name = "Validate"
    action {
      name             = "TerraformValidate"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["source_output"]
      output_artifacts = ["validate_output"]
      configuration    = { ProjectName = var.codebuild_validate_name }
    }
  }

  stage {
    name = "Plan"
    action {
      name             = "TerraformPlan"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["source_output"]
      output_artifacts = ["plan_output"]
      configuration    = { ProjectName = var.codebuild_plan_name }
    }
  }

  stage {
    name = "Approval"
    action {
      name     = "ManualApproval"
      category = "Approval"
      owner    = "AWS"
      provider = "Manual"
      version  = "1"
      configuration = {
        NotificationArn = aws_sns_topic.pipeline_approval.arn
        CustomData      = "Terraform planの結果を確認し、承認してください。"
      }
    }
  }

  stage {
    name = "Apply"
    action {
      name            = "TerraformApply"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["plan_output"]
      configuration   = { ProjectName = var.codebuild_apply_name }
    }
  }
  tags = {
    ManagedBy      = "terraform"
  }
}

var.xxx(変数名)
variables.tfで定義したxxxという変数を参照する

上記パイプラインリソース定義用のmain.tf内で使用する変数(var.xxx)をvariables.tfに定義します。

# terraform-cicd-demo/modules/pipeline/variables.tf
た
variable "project_name" {
  type = string
}

variable "github_repo" {
  type = string
}

variable "github_branch" {
  type    = string
  default = "main"
}

variable "approval_email" {
  type = string
}

variable "codebuild_validate_name" {
  type = string
}

variable "codebuild_plan_name" {
  type = string
}

variable "codebuild_apply_name" {
  type = string
}

また、別モジュールにて使用する値を出力するためにoutput.tfを作成します。

# terraform-cicd-demo/modules/pipeline/output.tf
output "artifacts_bucket_arn" {
  value = aws_s3_bucket.pipeline_artifacts.arn
}

output "pipeline_artifacts_key_arn" {
  value = aws_kms_key.pipeline.arn
}

output "pipeline_approval_topic_arn" {
  value = aws_sns_topic.pipeline_approval.arn
}

output "github_codestarconnections_arn" {
  value = aws_codestarconnections_connection.github.arn
}

output "pipeline_role_arn" {
  value = aws_iam_role.pipeline.arn
}

output "pipeline_arn" {
  value = aws_codepipeline.pipeline.arn
}

3.3 CodeBuildプロジェクト群

CodeBuildの定義と共にその中で参照するIAMロールを作成します。

# terraform-cicd-demo/modules/codebuild/main.tf

resource "aws_iam_role" "codebuild" {
  name = "${var.project_name}-codebuild-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "codebuild.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy" "codebuild" {
  name = "${var.project_name}-codebuild-policy"
  role = aws_iam_role.codebuild.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = ["s3:*"]
        Resource = [
          var.state_bucket_arn, "${var.state_bucket_arn}/*",
          var.artifacts_bucket_arn, "${var.artifacts_bucket_arn}/*"
        ]
      },
      {
        Effect   = "Allow"
        Action   = ["kms:Decrypt", "kms:DescribeKey", "kms:Encrypt", "kms:GenerateDataKey*", "kms:ReEncrypt*", "kms:GetKeyPolicy", "kms:GetKeyRotationStatus", "kms:ListResourceTags"]
        Resource = var.pipeline_artifacts_key_arn
      },
      {
        Effect   = "Allow"
        Action   = "kms:ListAliases"
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
        Resource = "*"
      },
      {
        Effect   = "Allow"
        Action   = ["iam:GetRole", "iam:PassRole", "iam:ListRolePolicies", "iam:ListRolePolicies", "iam:GetRolePolicy", "iam:ListAttachedRolePolicies", "iam:PutRolePolicy"],
        Resource = [var.pipeline_role_arn, aws_iam_role.codebuild.arn]
      },
      {
        Effect   = "Allow"
        Action   = ["sns:GetTopicAttributes", "sns:ListTagsForResource", "SNS:GetSubscriptionAttributes"]
        Resource = var.pipeline_approval_topic_arn
      },
      {
        Effect   = "Allow"
        Action   = ["codeconnections:GetConnection", "codestar-connections:GetConnection", "codeconnections:ListTagsForResource"]
        Resource = var.github_codestarconnections_arn
      },
      {
        Effect   = "Allow"
        Action   = "codebuild:BatchGetProjects"
        Resource = [aws_codebuild_project.validate.arn, aws_codebuild_project.plan.arn, aws_codebuild_project.apply.arn]
      },
      {
        Effect   = "Allow"
        Action   = ["codepipeline:GetPipeline", "codepipeline:ListTagsForResource", "codepipeline:TagResource"]
        Resource = var.pipeline_arn
      }
    ]
  })
}

# ---- Validate ----
resource "aws_codebuild_project" "validate" {
  name         = "${var.project_name}-validate"
  service_role = aws_iam_role.codebuild.arn
  artifacts { type = "CODEPIPELINE" }
  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
    type         = "LINUX_CONTAINER"
  }
  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec/buildspec_validate.yml"
  }
}

# ---- Plan ----
resource "aws_codebuild_project" "plan" {
  name         = "${var.project_name}-plan"
  service_role = aws_iam_role.codebuild.arn
  artifacts { type = "CODEPIPELINE" }
  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
    type         = "LINUX_CONTAINER"
  }
  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec/buildspec_plan.yml"
  }
}

# ---- Apply ----
resource "aws_codebuild_project" "apply" {
  name         = "${var.project_name}-apply"
  service_role = aws_iam_role.codebuild.arn
  artifacts { type = "CODEPIPELINE" }
  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
    type         = "LINUX_CONTAINER"
  }
  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec/buildspec_apply.yml"
  }
}

上記CodeBuild定義用のmain.tf内で使用する変数(var.xxx)をvariables.tfに定義します。

# terraform-cicd-demo/modules/codebuild/variables.tf
variable "project_name" {
  type = string
}

variable "state_bucket_arn" {
  type = string
}

variable "artifacts_bucket_arn" {
  type = string
}

variable "pipeline_artifacts_key_arn" {
  type = string
}

variable "pipeline_approval_topic_arn" {
  type = string
}

variable "github_codestarconnections_arn" {
  type = string
}

variable "pipeline_role_arn" {
  type = string
}

variable "pipeline_arn" {
  type = string
}

また、別モジュールにて使用する値を出力するためにoutput.tfを作成します。

# terraform-cicd-demo/modules/codebuild/output.tf
output "validate_project_name" {
  value = aws_codebuild_project.validate.name
}

output "plan_project_name" {
  value = aws_codebuild_project.plan.name
}

output "apply_project_name" {
  value = aws_codebuild_project.apply.name
}

3.4 Buildspecファイル

パイプラインの3ステージに対応するCodeBuildプロジェクトを定義します。

Validate Stage(構文チェック)

# terraform-cicd-demo/buildspec/buildspec_validate.yml
version: 0.2

env:
  variables:
    TF_VERSION: "1.14.4"
    TF_WORKING_DIR: "."

phases:
  install: # terraformのバイナリファイルをダウンロードし、解凍して、バージョン確認
    commands:
      - wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
      - unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
      - terraform version
  pre_build: # 対象ディレクトリへ移動し、initによるバックエンド初期化・プロバイダーダウンロード
    commands:
      - cd ${TF_WORKING_DIR}
      - terraform init -input=false
  build: # フォーマットチェック、構文・設定の妥当性チェック
    commands:
      - terraform fmt -check -recursive
      - terraform validate

Plan Stage(実行計画)

# terraform-cicd-demo/buildspec/buildspec_plan.yml
version: 0.2

env:
  variables:
    TF_VERSION: "1.14.4"
    TF_WORKING_DIR: "."

phases:
  install: # terraformのバイナリファイルをダウンロードし、解凍
    commands:
      - wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
      - unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
  pre_build: # 対象ディレクトリへ移動し、initによるバックエンド初期化・プロバイダーダウンロード
    commands:
      - cd ${TF_WORKING_DIR}
      - terraform init -input=false
  build: # 実行計画をバイナリ形式のファイルに保存、人間が読めるJSON形式に変換
    commands:
      - terraform plan -input=false -out=tfplan
      - terraform show -json tfplan > tfplan.json

artifacts: # 全サブディレクトリ内の全ファイルの内容をCodePipelineの成果物としてS3に保存
  files:
    - "**/*"
  base-directory: ${TF_WORKING_DIR}

Apply Stage(実際に適用)

# terraform-cicd-demo/buildspec/buildspec_apply.yml
version: 0.2

env:
  variables:
    TF_VERSION: "1.14.4"
    TF_WORKING_DIR: "."

phases:
  install: # terraformのバイナリファイルをダウンロードし、解凍
    commands:
      - wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
      - unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
  pre_build: # 対象ディレクトリへ移動し、initによるバックエンド初期化・プロバイダーダウンロード
    commands:
      - cd ${TF_WORKING_DIR}
      - terraform init -input=false
  build: # apply実行時の確認プロンプトをスキップし、Plan Stageで生成したファイル(tfplan)をそのまま適用
    commands:
      - terraform apply -input=false -auto-approve tfplan

tfplanを渡す理由
plan時点で確定した内容だけを適用できるから。tfplanを使用しない場合、plan,applyそれぞれの実行時点での.tfファイルを評価することになる。結果PlanとApplyの間に差分が生まれる可能性があり、意図しない変更が適用されるリスクが高まる。

3.5 CodePipeline + CodeBuildリソース作成

3.2 ~ 3.4にて作成したtfファイルを元にCodePipeline+CodeBuildリソースを作成していきます。

まずは、ルートのmain.tfにそれらのモジュールを呼び出す定義を追加します。

# main.tf

# 1. ステートバックエンド用S3バケット
module "tf_backend" {
  source = "./modules/tf-backend"
}


# 2. CodeBuild
module "codebuild" {
  source = "./modules/codebuild"

  project_name                   = var.project_name
  state_bucket_arn               = module.tf_backend.bucket_arn
  artifacts_bucket_arn           = module.pipeline.artifacts_bucket_arn
  pipeline_artifacts_key_arn     = module.pipeline.pipeline_artifacts_key_arn
  pipeline_approval_topic_arn    = module.pipeline.pipeline_approval_topic_arn
  github_codestarconnections_arn = module.pipeline.github_codestarconnections_arn
  pipeline_role_arn              = module.pipeline.pipeline_role_arn
  pipeline_arn                   = module.pipeline.pipeline_arn

}

# 3. CodePipeline
module "pipeline" {
  source = "./modules/pipeline"

  project_name   = var.project_name
  github_repo    = var.github_repo
  github_branch  = var.github_branch
  approval_email = var.approval_email

  codebuild_validate_name = module.codebuild.validate_project_name
  codebuild_plan_name     = module.codebuild.plan_project_name
  codebuild_apply_name    = module.codebuild.apply_project_name
}

module.xxx(モジュール名).yyy(outputの名前)
xxxというモジュールが公開しているyyyというoutputを参照する

上記module呼び出し用のmain.tf内で使用する変数(var.xxx)をvariables.tfに定義します。

# terraform-cicd-demo/variables.tf

variable "project_name" {
  description = "プロジェクト名(リソース名のプレフィックス)"
  type        = string
  default     = "terraform-cicd-demo"
}

variable "github_repo" {
  description = "GitHubリポジトリ(org/repo形式)"
  type        = string
}

variable "github_branch" {
  description = "対象ブランチ"
  type        = string
  default     = "main"
}

variable "approval_email" {
  description = "承認通知の送信先メールアドレス"
  type        = string
}

呼び出されるmodule側では呼び出し側から受け取る変数をvariableブロックで宣言しておく必要があり、呼び出し側(rootのmain.tf)のmoduleブロックで渡された値がそこに入る仕組みになっている。

デプロイを自動化するにはplan,apply実行時に毎回変数を入力するわけにはいきません。そこでterraform.tfvarsに変数の値をあらかじめ記載しておくことで、実行時にその値が読み取られ、変数を聞かれなくなります。

# terraform-cicd-demo/terraform.tfvars
# github_branchのみvariables.tfにてmainをdefault値としているため、ここでは記載なし
project_name = "terraform-cicd-demo"
github_repo = "xxx/terraform-cicd-demo"
approval_email = xxx

ファイル名をterraform.tfvars、terraform.tfvars.jsonもしくは*.auto.tfvars(*は任意の文字列)とすることで特に指定せずともplan,apply実行時に自動でファイルを読み込ませることができる。一方xxx.tfvarsのように自由な名前にすることも可能だが、その場合はコマンド実行時に-var-file="xxx.tfvars"という用に指定する必要がある。
また、terraform.tfvars、terraform.tfvars.jsonと*.auto.tfvarsの違いは後者が前者よりも名前の自由度があること。それによりdev.auto.tfvars,prod.auto.tfvarsなど環境ごとに変数をまとめることができる点にある。変数が少ないうちは前者を、変数が多くなり管理が煩雑になった際は後者を使うなど使い分けられる。

variables.tfのdefaultと*.tfvarsの違い
variables.tfのdefault : 共通で使われる汎用的なデフォルト値。コードの中に記載し、バージョン管理ツール(Gitなど)で管理される。パイプラインの起動トリガーになるブランチは普通mainだよね、など。
*.tfvars : 主に環境・プロジェクト固有の実値を記載。バージョン管理ツール(Gitなど)で管理したくないシークレットや環境ごとの値(dev,prodなど)を記載する。基本的にローカルで保管する。そういった値を人と共有したい場合はSecret ManagerやCI/CD環境変数、HCP Terraformなどを使い、ファイルではなくサービス・環境変数で管理する手段に切り替えるとよい。
※両者定義時の優先順位は*.tfvarsのほうが高い。

新たに作成したmoduleを登録し使用可能にするため、terraform initを実行します。

# terraform-cicd-demo/ で実行
> terraform init
Initializing the backend...
Initializing modules...
- codebuild in modules\codebuild
- pipeline in modules\pipeline
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v6.32.1

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

planを実行し、applyが正常に行えるかを確認します。

# terraform-cicd-demo/ で実行
> terraform plan
module.pipeline.data.aws_caller_identity.current: Reading...
module.tf_backend.aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.data.aws_caller_identity.current: Read complete after 0s [id=xxx]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.codebuild.aws_codebuild_project.apply will be created
  + resource "aws_codebuild_project" "apply" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-apply"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_apply.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_codebuild_project.plan will be created
  + resource "aws_codebuild_project" "plan" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-plan"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_plan.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_codebuild_project.validate will be created
  + resource "aws_codebuild_project" "validate" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-validate"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_validate.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_iam_role.codebuild will be created
  + resource "aws_iam_role" "codebuild" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "codebuild.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "terraform-cicd-demo-codebuild-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # module.codebuild.aws_iam_role_policy.codebuild will be created
  + resource "aws_iam_role_policy" "codebuild" {
      + id          = (known after apply)
      + name        = "terraform-cicd-demo-codebuild-policy"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # module.pipeline.aws_codepipeline.pipeline will be created
  + resource "aws_codepipeline" "pipeline" {
      + arn            = (known after apply)
      + execution_mode = "SUPERSEDED"
      + id             = (known after apply)
      + name           = "terraform-cicd-demo-codepipeline"
      + pipeline_type  = "V2"
      + region         = "ap-northeast-1"
      + role_arn       = (known after apply)
      + tags           = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all       = {
          + "ManagedBy"  = "terraform"
        }
      + trigger_all    = (known after apply)

      + artifact_store {
          + location = (known after apply)
          + type     = "S3"
            # (1 unchanged attribute hidden)

          + encryption_key {
              + id   = (known after apply)
              + type = "KMS"
            }
        }

      + stage {
          + name = "Source"

          + action {
              + category         = "Source"
              + configuration    = (known after apply)
              + name             = "GitHub"
              + output_artifacts = [
                  + "source_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeStarSourceConnection"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Validate"

          + action {
              + category         = "Build"
              + configuration    = {
                  + "ProjectName" = "terraform-cicd-demo-validate"
                }
              + input_artifacts  = [
                  + "source_output",
                ]
              + name             = "TerraformValidate"
              + output_artifacts = [
                  + "validate_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeBuild"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Plan"

          + action {
              + category         = "Build"
              + configuration    = {
                  + "ProjectName" = "terraform-cicd-demo-plan"
                }
              + input_artifacts  = [
                  + "source_output",
                ]
              + name             = "TerraformPlan"
              + output_artifacts = [
                  + "plan_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeBuild"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Approval"

          + action {
              + category      = "Approval"
              + configuration = (known after apply)
              + name          = "ManualApproval"
              + owner         = "AWS"
              + provider      = "Manual"
              + region        = (known after apply)
              + run_order     = (known after apply)
              + version       = "1"
            }
        }
      + stage {
          + name = "Apply"

          + action {
              + category        = "Build"
              + configuration   = {
                  + "ProjectName" = "terraform-cicd-demo-apply"
                }
              + input_artifacts = [
                  + "plan_output",
                ]
              + name            = "TerraformApply"
              + owner           = "AWS"
              + provider        = "CodeBuild"
              + region          = (known after apply)
              + run_order       = (known after apply)
              + version         = "1"
            }
        }
    }

  # module.pipeline.aws_codestarconnections_connection.github will be created
  + resource "aws_codestarconnections_connection" "github" {
      + arn               = (known after apply)
      + connection_status = (known after apply)
      + id                = (known after apply)
      + name              = "terraform-cicd-demo-github"
      + provider_type     = "GitHub"
      + region            = "ap-northeast-1"
      + tags_all          = (known after apply)
    }

  # module.pipeline.aws_iam_role.pipeline will be created
  + resource "aws_iam_role" "pipeline" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "codepipeline.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "terraform-cicd-demo-codepipeline-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags                  = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all              = {
          + "ManagedBy"  = "terraform"
        }
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # module.pipeline.aws_iam_role_policy.pipeline will be created
  + resource "aws_iam_role_policy" "pipeline" {
      + id          = (known after apply)
      + name        = "terraform-cicd-demo-codepipeline-policy"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # module.pipeline.aws_kms_alias.pipeline will be created
  + resource "aws_kms_alias" "pipeline" {
      + arn            = (known after apply)
      + id             = (known after apply)
      + name           = "alias/terraform-cicd-demo-pipeline"
      + name_prefix    = (known after apply)
      + region         = "ap-northeast-1"
      + target_key_arn = (known after apply)
      + target_key_id  = (known after apply)
    }

  # module.pipeline.aws_kms_key.pipeline will be created
  + resource "aws_kms_key" "pipeline" {
      + arn                                = (known after apply)
      + bypass_policy_lockout_safety_check = false
      + customer_master_key_spec           = "SYMMETRIC_DEFAULT"
      + deletion_window_in_days            = 7
      + description                        = "KMS key for CodePipeline artifact encryption"
      + enable_key_rotation                = true
      + id                                 = (known after apply)
      + is_enabled                         = true
      + key_id                             = (known after apply)
      + key_usage                          = "ENCRYPT_DECRYPT"
      + multi_region                       = (known after apply)
      + policy                             = (known after apply)
      + region                             = "ap-northeast-1"
      + rotation_period_in_days            = (known after apply)
      + tags                               = {
          + "ManagedBy"  = "terraform"
          + "Name"       = "Pipeline KMS Key"
        }
      + tags_all                           = {
          + "ManagedBy"  = "terraform"
          + "Name"       = "Pipeline KMS Key"
        }
    }

  # module.pipeline.aws_s3_bucket.pipeline_artifacts will be created
  + resource "aws_s3_bucket" "pipeline_artifacts" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "terraform-cicd-demo-artifacts-xxx"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + request_payer               = (known after apply)
      + tags                        = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all                    = {
          + "ManagedBy"  = "terraform"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts will be created
  + resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "ap-northeast-1"
      + restrict_public_buckets = true
    }

  # module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts will be created
  + resource "aws_s3_bucket_versioning" "pipeline_artifacts" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

  # module.pipeline.aws_sns_topic.pipeline_approval will be created
  + resource "aws_sns_topic" "pipeline_approval" {
      + arn                         = (known after apply)
      + beginning_archive_time      = (known after apply)
      + content_based_deduplication = false
      + fifo_throughput_scope       = (known after apply)
      + fifo_topic                  = false
      + id                          = (known after apply)
      + name                        = "terraform-cicd-demo-pipeline_approval"
      + name_prefix                 = (known after apply)
      + owner                       = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + signature_version           = (known after apply)
      + tags                        = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all                    = {
          + "ManagedBy"  = "terraform"
        }
      + tracing_config              = (known after apply)
    }

  # module.pipeline.aws_sns_topic_subscription.approval_email will be created
  + resource "aws_sns_topic_subscription" "approval_email" {
      + arn                             = (known after apply)
      + confirmation_timeout_in_minutes = 1
      + confirmation_was_authenticated  = (known after apply)
      + endpoint                        = "xxx"
      + endpoint_auto_confirms          = false
      + filter_policy_scope             = (known after apply)
      + id                              = (known after apply)
      + owner_id                        = (known after apply)
      + pending_confirmation            = (known after apply)
      + protocol                        = "email"
      + raw_message_delivery            = false
      + region                          = "ap-northeast-1"
      + topic_arn                       = (known after apply)
    }

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

問題なくplanに成功したため、applyを実行します。

> terraform apply
module.pipeline.data.aws_caller_identity.current: Reading...
module.tf_backend.aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.data.aws_caller_identity.current: Read complete after 0s [id=xxx]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.codebuild.aws_codebuild_project.apply will be created
  + resource "aws_codebuild_project" "apply" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-apply"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_apply.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_codebuild_project.plan will be created
  + resource "aws_codebuild_project" "plan" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-plan"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_plan.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_codebuild_project.validate will be created
  + resource "aws_codebuild_project" "validate" {
      + arn                  = (known after apply)
      + auto_retry_limit     = (known after apply)
      + badge_enabled        = false
      + badge_url            = (known after apply)
      + build_timeout        = 60
      + description          = (known after apply)
      + encryption_key       = (known after apply)
      + id                   = (known after apply)
      + name                 = "terraform-cicd-demo-validate"
      + project_visibility   = "PRIVATE"
      + public_project_alias = (known after apply)
      + queued_timeout       = 480
      + region               = "ap-northeast-1"
      + service_role         = (known after apply)
      + tags_all             = (known after apply)

      + artifacts {
          + encryption_disabled    = false
          + override_artifact_name = false
          + type                   = "CODEPIPELINE"
        }

      + environment {
          + compute_type                = "BUILD_GENERAL1_SMALL"
          + image                       = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
          + image_pull_credentials_type = "CODEBUILD"
          + privileged_mode             = false
          + type                        = "LINUX_CONTAINER"
        }

      + source {
          + buildspec = "buildspec/buildspec_validate.yml"
          + type      = "CODEPIPELINE"
        }
    }

  # module.codebuild.aws_iam_role.codebuild will be created
  + resource "aws_iam_role" "codebuild" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "codebuild.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "terraform-cicd-demo-codebuild-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags_all              = (known after apply)
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # module.codebuild.aws_iam_role_policy.codebuild will be created
  + resource "aws_iam_role_policy" "codebuild" {
      + id          = (known after apply)
      + name        = "terraform-cicd-demo-codebuild-policy"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # module.pipeline.aws_codepipeline.pipeline will be created
  + resource "aws_codepipeline" "pipeline" {
      + arn            = (known after apply)
      + execution_mode = "SUPERSEDED"
      + id             = (known after apply)
      + name           = "terraform-cicd-demo-codepipeline"
      + pipeline_type  = "V2"
      + region         = "ap-northeast-1"
      + role_arn       = (known after apply)
      + tags           = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all       = {
          + "ManagedBy"  = "terraform"
        }
      + trigger_all    = (known after apply)

      + artifact_store {
          + location = (known after apply)
          + type     = "S3"
            # (1 unchanged attribute hidden)

          + encryption_key {
              + id   = (known after apply)
              + type = "KMS"
            }
        }

      + stage {
          + name = "Source"

          + action {
              + category         = "Source"
              + configuration    = (known after apply)
              + name             = "GitHub"
              + output_artifacts = [
                  + "source_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeStarSourceConnection"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Validate"

          + action {
              + category         = "Build"
              + configuration    = {
                  + "ProjectName" = "terraform-cicd-demo-validate"
                }
              + input_artifacts  = [
                  + "source_output",
                ]
              + name             = "TerraformValidate"
              + output_artifacts = [
                  + "validate_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeBuild"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Plan"

          + action {
              + category         = "Build"
              + configuration    = {
                  + "ProjectName" = "terraform-cicd-demo-plan"
                }
              + input_artifacts  = [
                  + "source_output",
                ]
              + name             = "TerraformPlan"
              + output_artifacts = [
                  + "plan_output",
                ]
              + owner            = "AWS"
              + provider         = "CodeBuild"
              + region           = (known after apply)
              + run_order        = (known after apply)
              + version          = "1"
            }
        }
      + stage {
          + name = "Approval"

          + action {
              + category      = "Approval"
              + configuration = (known after apply)
              + name          = "ManualApproval"
              + owner         = "AWS"
              + provider      = "Manual"
              + region        = (known after apply)
              + run_order     = (known after apply)
              + version       = "1"
            }
        }
      + stage {
          + name = "Apply"

          + action {
              + category        = "Build"
              + configuration   = {
                  + "ProjectName" = "terraform-cicd-demo-apply"
                }
              + input_artifacts = [
                  + "plan_output",
                ]
              + name            = "TerraformApply"
              + owner           = "AWS"
              + provider        = "CodeBuild"
              + region          = (known after apply)
              + run_order       = (known after apply)
              + version         = "1"
            }
        }
    }

  # module.pipeline.aws_codestarconnections_connection.github will be created
  + resource "aws_codestarconnections_connection" "github" {
      + arn               = (known after apply)
      + connection_status = (known after apply)
      + id                = (known after apply)
      + name              = "terraform-cicd-demo-github"
      + provider_type     = "GitHub"
      + region            = "ap-northeast-1"
      + tags_all          = (known after apply)
    }

  # module.pipeline.aws_iam_role.pipeline will be created
  + resource "aws_iam_role" "pipeline" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "codepipeline.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "terraform-cicd-demo-codepipeline-role"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + tags                  = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all              = {
          + "ManagedBy"  = "terraform"
        }
      + unique_id             = (known after apply)

      + inline_policy (known after apply)
    }

  # module.pipeline.aws_iam_role_policy.pipeline will be created
  + resource "aws_iam_role_policy" "pipeline" {
      + id          = (known after apply)
      + name        = "terraform-cicd-demo-codepipeline-policy"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # module.pipeline.aws_kms_alias.pipeline will be created
  + resource "aws_kms_alias" "pipeline" {
      + arn            = (known after apply)
      + id             = (known after apply)
      + name           = "alias/terraform-cicd-demo-pipeline"
      + name_prefix    = (known after apply)
      + region         = "ap-northeast-1"
      + target_key_arn = (known after apply)
      + target_key_id  = (known after apply)
    }

  # module.pipeline.aws_kms_key.pipeline will be created
  + resource "aws_kms_key" "pipeline" {
      + arn                                = (known after apply)
      + bypass_policy_lockout_safety_check = false
      + customer_master_key_spec           = "SYMMETRIC_DEFAULT"
      + deletion_window_in_days            = 7
      + description                        = "KMS key for CodePipeline artifact encryption"
      + enable_key_rotation                = true
      + id                                 = (known after apply)
      + is_enabled                         = true
      + key_id                             = (known after apply)
      + key_usage                          = "ENCRYPT_DECRYPT"
      + multi_region                       = (known after apply)
      + policy                             = (known after apply)
      + region                             = "ap-northeast-1"
      + rotation_period_in_days            = (known after apply)
      + tags                               = {
          + "ManagedBy"  = "terraform"
          + "Name"       = "Pipeline KMS Key"
          + "created_by" = "tomoya.konagayoshi"
        }
      + tags_all                           = {
          + "ManagedBy"  = "terraform"
          + "Name"       = "Pipeline KMS Key"
          + "created_by" = "tomoya.konagayoshi"
        }
    }

  # module.pipeline.aws_s3_bucket.pipeline_artifacts will be created
  + resource "aws_s3_bucket" "pipeline_artifacts" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "terraform-cicd-demo-artifacts-xxx"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_region               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + request_payer               = (known after apply)
      + tags                        = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all                    = {
          + "ManagedBy"  = "terraform"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

  # module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts will be created
  + resource "aws_s3_bucket_public_access_block" "pipeline_artifacts" {
      + block_public_acls       = true
      + block_public_policy     = true
      + bucket                  = (known after apply)
      + id                      = (known after apply)
      + ignore_public_acls      = true
      + region                  = "ap-northeast-1"
      + restrict_public_buckets = true
    }

  # module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts will be created
  + resource "aws_s3_bucket_versioning" "pipeline_artifacts" {
      + bucket = (known after apply)
      + id     = (known after apply)
      + region = "ap-northeast-1"

      + versioning_configuration {
          + mfa_delete = (known after apply)
          + status     = "Enabled"
        }
    }

  # module.pipeline.aws_sns_topic.pipeline_approval will be created
  + resource "aws_sns_topic" "pipeline_approval" {
      + arn                         = (known after apply)
      + beginning_archive_time      = (known after apply)
      + content_based_deduplication = false
      + fifo_throughput_scope       = (known after apply)
      + fifo_topic                  = false
      + id                          = (known after apply)
      + name                        = "terraform-cicd-demo-pipeline_approval"
      + name_prefix                 = (known after apply)
      + owner                       = (known after apply)
      + policy                      = (known after apply)
      + region                      = "ap-northeast-1"
      + signature_version           = (known after apply)
      + tags                        = {
          + "ManagedBy"  = "terraform"
        }
      + tags_all                    = {
          + "ManagedBy"  = "terraform"
        }
      + tracing_config              = (known after apply)
    }

  # module.pipeline.aws_sns_topic_subscription.approval_email will be created
  + resource "aws_sns_topic_subscription" "approval_email" {
      + arn                             = (known after apply)
      + confirmation_timeout_in_minutes = 1
      + confirmation_was_authenticated  = (known after apply)
      + endpoint                        = "xxx"
      + endpoint_auto_confirms          = false
      + filter_policy_scope             = (known after apply)
      + id                              = (known after apply)
      + owner_id                        = (known after apply)
      + pending_confirmation            = (known after apply)
      + protocol                        = "email"
      + raw_message_delivery            = false
      + region                          = "ap-northeast-1"
      + topic_arn                       = (known after apply)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.pipeline.aws_codestarconnections_connection.github: Creating...
module.codebuild.aws_iam_role.codebuild: Creating...
module.pipeline.aws_kms_key.pipeline: Creating...
module.pipeline.aws_iam_role.pipeline: Creating...
module.pipeline.aws_sns_topic.pipeline_approval: Creating...
module.pipeline.aws_s3_bucket.pipeline_artifacts: Creating...
module.pipeline.aws_codestarconnections_connection.github: Creation complete after 1s [id=arn:aws:codestar-connections:ap-northeast-1:xxx:connection/0290fbc1-57e6-41fc-9454-e4ce5c40490f]
module.pipeline.aws_sns_topic.pipeline_approval: Creation complete after 1s [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval]
module.pipeline.aws_sns_topic_subscription.approval_email: Creating...
module.pipeline.aws_sns_topic_subscription.approval_email: Creation complete after 0s [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval:3adb84e3-0932-4171-96e7-6d3d93c9e9d7]
module.pipeline.aws_s3_bucket.pipeline_artifacts: Creation complete after 1s [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts: Creating...
module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts: Creating...
module.codebuild.aws_iam_role.codebuild: Creation complete after 2s [id=terraform-cicd-demo-codebuild-role]
module.pipeline.aws_iam_role.pipeline: Creation complete after 2s [id=terraform-cicd-demo-codepipeline-role]
module.codebuild.aws_iam_role_policy.codebuild: Creating...
module.codebuild.aws_codebuild_project.plan: Creating...
module.codebuild.aws_codebuild_project.validate: Creating...
module.codebuild.aws_codebuild_project.apply: Creating...
module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts: Creation complete after 1s [id=terraform-cicd-demo-artifacts-xxx]
module.codebuild.aws_iam_role_policy.codebuild: Creation complete after 0s [id=terraform-cicd-demo-codebuild-role:terraform-cicd-demo-codebuild-policy]
module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts: Creation complete after 2s [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_kms_key.pipeline: Creation complete after 9s [id=2b1162c5-fc78-4336-987d-7dcab0d0a9d9]
module.pipeline.aws_kms_alias.pipeline: Creating...
module.pipeline.aws_iam_role_policy.pipeline: Creating...
module.pipeline.aws_kms_alias.pipeline: Creation complete after 0s [id=alias/terraform-cicd-demo-pipeline]
module.pipeline.aws_iam_role_policy.pipeline: Creation complete after 0s [id=terraform-cicd-demo-codepipeline-role:terraform-cicd-demo-codepipeline-policy]
module.codebuild.aws_codebuild_project.validate: Creation complete after 8s [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-validate]
module.codebuild.aws_codebuild_project.plan: Still creating... [00m10s elapsed]
module.codebuild.aws_codebuild_project.apply: Still creating... [00m10s elapsed]
module.codebuild.aws_codebuild_project.apply: Creation complete after 16s [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-apply]
module.codebuild.aws_codebuild_project.plan: Creation complete after 16s [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-plan]
module.pipeline.aws_codepipeline.pipeline: Creating...
module.pipeline.aws_codepipeline.pipeline: Creation complete after 1s [id=terraform-cicd-demo-codepipeline]

Apply complete! Resources: 16 added, 0 changed, 0 destroyed.

いくつか手動で実行する必要のある作業があります。

まず、SNSのサブスクリプションの確認です。この作業によってSNSトピックを介して、登録したメールアドレスにメールが送信されるようになります。terraform.tfvarsのapproval_emailで指定したアドレスにサブスクリプションの確認メールが届いているはずです。メールのリンク先に遷移することで承認が可能なのですが、今回はリンクをコピーし、コンソール上で承認を行います。

なぜメールのリンク遷移でサブスクリプションの確認を行わないか
SNSのメールにはサブスクリプションの解除用リンクがついてくる。簡単に解除できる利便性はあるが、意図せず解除してしまうと気づかぬうちに重要なメールが送信されなくなってしまう。
コンソール、CLIにて確認作業を行うことで、メールの解除用リンクを踏んでしまっても解除されなくなる。

image.png
image.png
image.png
image.png

次にAWSリソースをGitHubに接続するため接続の更新を行います。AWSConnectorを認証します。
image.png
image.png
新しいアプリをインストールするを選択します。
image.png
awsリソースよりアクセスするリポジトリを選択し、インストールと認証を行います。
image.png
image.png

image.png

3.6 動作確認

ローカルでtfファイルに変更を加えた後、githubのmainブランチへpushした結果、想定通りパイプラインが動作するかを確認します。

今回はcodepipelineのリソースに新たなタグpipeline_check = "true"をつけます。

# CodePipeline V2
resource "aws_codepipeline" "pipeline" {
  ...
  tags = {
    ManagedBy  = "terraform"
    pipeline_check = "true" #現在GitHub上にあがっているファイルとの差異部分
  }
}

gitでローカルからpushします。

~/terraform-cicd-demo (main)
$ git add .

~/terraform-cicd-demo (main)
$ git commit -m "add tag to pipeline"
[main b2d7399] add tag to pipeline
 Committer: xxx
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

 1 file changed, 1 insertion(+)

xxx@LNV-X13-925 MINGW64 ~/terraform-cicd-demo (main)
$ git push origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 12 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 603 bytes | 301.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/xxx/terraform-cicd-demo.git
   a949ea9..b2d7399  main -> main

pushに成功したので、それをトリガーとしてパイプラインが開始されていることを確認できました。
image.png

また、後続のvalidate、planステージも成功しています。
image.png

validateステージのbuild実行ログ

[Container] 2026/02/16 15:25:27.064472 Running on CodeBuild On-demand
[Container] 2026/02/16 15:25:27.064486 Waiting for agent ping
[Container] 2026/02/16 15:25:27.268308 Waiting for DOWNLOAD_SOURCE
[Container] 2026/02/16 15:25:28.647067 Phase is DOWNLOAD_SOURCE
[Container] 2026/02/16 15:25:28.648253 CODEBUILD_SRC_DIR=/codebuild/output/src1241271192/src
[Container] 2026/02/16 15:25:28.648745 YAML location is /codebuild/output/src1241271192/src/buildspec/buildspec_validate.yml
[Container] 2026/02/16 15:25:28.650597 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2026/02/16 15:25:28.650682 Processing environment variables
[Container] 2026/02/16 15:25:28.809187 No runtime version selected in buildspec.
[Container] 2026/02/16 15:25:28.859919 Moving to directory /codebuild/output/src1241271192/src
[Container] 2026/02/16 15:25:28.859944 Cache is not defined in the buildspec
[Container] 2026/02/16 15:25:28.903131 Skip cache due to: no paths specified to be cached
[Container] 2026/02/16 15:25:28.903549 Registering with agent
[Container] 2026/02/16 15:25:28.943535 Phases found in YAML: 3
[Container] 2026/02/16 15:25:28.943551  BUILD: 2 commands
[Container] 2026/02/16 15:25:28.943556  INSTALL: 3 commands
[Container] 2026/02/16 15:25:28.943560  PRE_BUILD: 2 commands
[Container] 2026/02/16 15:25:28.944013 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2026/02/16 15:25:28.944025 Phase context status code:  Message: 
[Container] 2026/02/16 15:25:29.061605 Entering phase INSTALL
[Container] 2026/02/16 15:25:29.101276 Running command wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip

[Container] 2026/02/16 15:25:31.203057 Running command unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
Archive:  terraform_1.14.4_linux_amd64.zip
  inflating: /usr/local/bin/LICENSE.txt  
  inflating: /usr/local/bin/terraform  

[Container] 2026/02/16 15:25:31.996997 Running command terraform version
Terraform v1.14.4
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v6.32.1

Your version of Terraform is out of date! The latest version
is 1.14.5. You can update by downloading from https://developer.hashicorp.com/terraform/install

[Container] 2026/02/16 15:25:32.578645 Phase complete: INSTALL State: SUCCEEDED
[Container] 2026/02/16 15:25:32.578665 Phase context status code:  Message: 
[Container] 2026/02/16 15:25:32.621122 Entering phase PRE_BUILD
[Container] 2026/02/16 15:25:32.622475 Running command cd ${TF_WORKING_DIR}

[Container] 2026/02/16 15:25:32.630294 Running command terraform init -input=false
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing modules...
- codebuild in modules/codebuild
- pipeline in modules/pipeline
- tf_backend in modules/tf-backend
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v6.32.1...
- Installed hashicorp/aws v6.32.1 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

[Container] 2026/02/16 15:25:46.973455 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2026/02/16 15:25:46.973481 Phase context status code:  Message: 
[Container] 2026/02/16 15:25:47.018371 Entering phase BUILD
[Container] 2026/02/16 15:25:47.019702 Running command terraform fmt -check -recursive

[Container] 2026/02/16 15:25:47.066822 Running command terraform validate
Success! The configuration is valid.


[Container] 2026/02/16 15:25:52.316674 Phase complete: BUILD State: SUCCEEDED
[Container] 2026/02/16 15:25:52.316694 Phase context status code:  Message: 
[Container] 2026/02/16 15:25:52.359719 Entering phase POST_BUILD
[Container] 2026/02/16 15:25:52.362649 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2026/02/16 15:25:52.362664 Phase context status code:  Message: 
[Container] 2026/02/16 15:25:52.428354 Set report auto-discover timeout to 5 seconds
[Container] 2026/02/16 15:25:52.429475 Expanding base directory path:  .
[Container] 2026/02/16 15:25:52.435199 Assembling file list
[Container] 2026/02/16 15:25:52.435215 Expanding .
[Container] 2026/02/16 15:25:52.438326 Expanding file paths for base directory .
[Container] 2026/02/16 15:25:52.438339 Assembling file list
[Container] 2026/02/16 15:25:52.438343 Expanding **/*
[Container] 2026/02/16 15:25:52.444159 No matching auto-discover report paths found
[Container] 2026/02/16 15:25:52.444177 Report auto-discover file discovery took 0.015824 seconds
[Container] 2026/02/16 15:25:52.444190 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2026/02/16 15:25:52.444196 Phase context status code:  Message: 

planステージのbuild実行ログ

[Container] 2026/02/16 15:26:31.394093 Running on CodeBuild On-demand
[Container] 2026/02/16 15:26:31.394106 Waiting for agent ping
[Container] 2026/02/16 15:26:31.595489 Waiting for DOWNLOAD_SOURCE
[Container] 2026/02/16 15:26:32.952012 Phase is DOWNLOAD_SOURCE
[Container] 2026/02/16 15:26:32.953332 CODEBUILD_SRC_DIR=/codebuild/output/src2519090299/src
[Container] 2026/02/16 15:26:32.953981 YAML location is /codebuild/output/src2519090299/src/buildspec/buildspec_plan.yml
[Container] 2026/02/16 15:26:32.956114 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2026/02/16 15:26:32.956228 Processing environment variables
[Container] 2026/02/16 15:26:33.102021 No runtime version selected in buildspec.
[Container] 2026/02/16 15:26:33.133838 Moving to directory /codebuild/output/src2519090299/src
[Container] 2026/02/16 15:26:33.133862 Cache is not defined in the buildspec
[Container] 2026/02/16 15:26:33.180221 Skip cache due to: no paths specified to be cached
[Container] 2026/02/16 15:26:33.180577 Registering with agent
[Container] 2026/02/16 15:26:33.223219 Phases found in YAML: 3
[Container] 2026/02/16 15:26:33.223237  INSTALL: 2 commands
[Container] 2026/02/16 15:26:33.223319  PRE_BUILD: 2 commands
[Container] 2026/02/16 15:26:33.223416  BUILD: 2 commands
[Container] 2026/02/16 15:26:33.223771 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2026/02/16 15:26:33.223782 Phase context status code:  Message: 
[Container] 2026/02/16 15:26:33.353387 Entering phase INSTALL
[Container] 2026/02/16 15:26:33.401465 Running command wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip

[Container] 2026/02/16 15:26:35.432962 Running command unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
Archive:  terraform_1.14.4_linux_amd64.zip
  inflating: /usr/local/bin/LICENSE.txt  
  inflating: /usr/local/bin/terraform  

[Container] 2026/02/16 15:26:36.267184 Phase complete: INSTALL State: SUCCEEDED
[Container] 2026/02/16 15:26:36.267209 Phase context status code:  Message: 
[Container] 2026/02/16 15:26:36.311736 Entering phase PRE_BUILD
[Container] 2026/02/16 15:26:36.312755 Running command cd ${TF_WORKING_DIR}

[Container] 2026/02/16 15:26:36.322567 Running command terraform init -input=false
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing modules...
- codebuild in modules/codebuild
- pipeline in modules/pipeline
- tf_backend in modules/tf-backend
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v6.32.1...
- Installed hashicorp/aws v6.32.1 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

[Container] 2026/02/16 15:26:49.680264 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2026/02/16 15:26:49.680282 Phase context status code:  Message: 
[Container] 2026/02/16 15:26:49.730714 Entering phase BUILD
[Container] 2026/02/16 15:26:49.732056 Running command terraform plan -input=false -out=tfplan
module.pipeline.data.aws_caller_identity.current: Reading...
module.pipeline.aws_codestarconnections_connection.github: Refreshing state... [id=arn:aws:codestar-connections:ap-northeast-1:xxx:connection/01c7dee9-0241-4b80-8c11-49832e7a6b4b]
module.pipeline.aws_kms_key.pipeline: Refreshing state... [id=2b1162c5-fc78-4336-987d-7dcab0d0a9d9]
module.pipeline.aws_iam_role.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role]
module.codebuild.aws_iam_role.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role]
module.pipeline.aws_sns_topic.pipeline_approval: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval]
module.tf_backend.aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.data.aws_caller_identity.current: Read complete after 0s [id=xxx]
module.pipeline.aws_s3_bucket.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_kms_alias.pipeline: Refreshing state... [id=alias/terraform-cicd-demo-pipeline]
module.pipeline.aws_sns_topic_subscription.approval_email: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval:3adb84e3-0932-4171-96e7-6d3d93c9e9d7]
module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.aws_iam_role_policy.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role:terraform-cicd-demo-codepipeline-policy]
module.codebuild.aws_codebuild_project.plan: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-plan]
module.codebuild.aws_codebuild_project.apply: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-apply]
module.codebuild.aws_codebuild_project.validate: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-validate]
module.pipeline.aws_codepipeline.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline]
module.codebuild.aws_iam_role_policy.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role:terraform-cicd-demo-codebuild-policy]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.pipeline.aws_codepipeline.pipeline will be updated in-place
  ~ resource "aws_codepipeline" "pipeline" {
        id             = "terraform-cicd-demo-codepipeline"
        name           = "terraform-cicd-demo-codepipeline"
      ~ tags           = {
            "ManagedBy"      = "terraform"
          + "pipeline_check" = "true"
        }
      ~ tags_all       = {
          + "pipeline_check" = "true"
            # (2 unchanged elements hidden)
        }
        # (6 unchanged attributes hidden)

        # (6 unchanged blocks hidden)
    }

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

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

[Container] 2026/02/16 15:26:57.057699 Running command terraform show -json tfplan > tfplan.json

[Container] 2026/02/16 15:27:01.274286 Phase complete: BUILD State: SUCCEEDED
[Container] 2026/02/16 15:27:01.274304 Phase context status code:  Message: 
[Container] 2026/02/16 15:27:01.330729 Entering phase POST_BUILD
[Container] 2026/02/16 15:27:01.333818 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2026/02/16 15:27:01.333832 Phase context status code:  Message: 
[Container] 2026/02/16 15:27:01.457328 Expanding base directory path: ${TF_WORKING_DIR}
[Container] 2026/02/16 15:27:01.460940 Assembling file list
[Container] 2026/02/16 15:27:01.460953 Expanding .
[Container] 2026/02/16 15:27:01.464320 Expanding file paths for base directory .
[Container] 2026/02/16 15:27:01.464334 Assembling file list
[Container] 2026/02/16 15:27:01.464338 Expanding **/*
[Container] 2026/02/16 15:27:01.467914 Found 25 file(s)
[Container] 2026/02/16 15:27:15.941656 Set report auto-discover timeout to 5 seconds
[Container] 2026/02/16 15:27:15.941699 Expanding base directory path:  .
[Container] 2026/02/16 15:27:15.945298 Assembling file list
[Container] 2026/02/16 15:27:15.945314 Expanding .
[Container] 2026/02/16 15:27:15.948628 Expanding file paths for base directory .
[Container] 2026/02/16 15:27:15.948642 Assembling file list
[Container] 2026/02/16 15:27:15.948738 Expanding **/*
[Container] 2026/02/16 15:27:15.952256 No matching auto-discover report paths found
[Container] 2026/02/16 15:27:15.952305 Report auto-discover file discovery took 0.010649 seconds
[Container] 2026/02/16 15:27:15.952331 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2026/02/16 15:27:15.952338 Phase context status code:  Message: 

approvalステージへ到達すると、SNSでapply実行承認依頼のメールが登録済みのアドレスに対して送信されます。
image.png

リンク先はパイプラインの画面となっており、ここから承認を行っていきます。(後続のapplyステージが成功となっているのは前回実行時の情報が残っているためです)
image.png
image.png

承認され、後続のapplyステージが実行され、パイプラインが成功しました。

applyステージのbuild実行ログ

[Container] 2026/02/16 16:08:21.343046 Running on CodeBuild On-demand
[Container] 2026/02/16 16:08:21.343061 Waiting for agent ping
[Container] 2026/02/16 16:08:21.544709 Waiting for DOWNLOAD_SOURCE
[Container] 2026/02/16 16:08:28.651638 Phase is DOWNLOAD_SOURCE
[Container] 2026/02/16 16:08:28.653126 CODEBUILD_SRC_DIR=/codebuild/output/src2579309578/src
[Container] 2026/02/16 16:08:28.653775 YAML location is /codebuild/output/src2579309578/src/buildspec/buildspec_apply.yml
[Container] 2026/02/16 16:08:28.656859 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2026/02/16 16:08:28.657741 Processing environment variables
[Container] 2026/02/16 16:08:28.864931 No runtime version selected in buildspec.
[Container] 2026/02/16 16:08:28.896618 Moving to directory /codebuild/output/src2579309578/src
[Container] 2026/02/16 16:08:28.896727 Cache is not defined in the buildspec
[Container] 2026/02/16 16:08:28.942025 Skip cache due to: no paths specified to be cached
[Container] 2026/02/16 16:08:28.942454 Registering with agent
[Container] 2026/02/16 16:08:28.985530 Phases found in YAML: 3
[Container] 2026/02/16 16:08:28.985548  INSTALL: 2 commands
[Container] 2026/02/16 16:08:28.985553  PRE_BUILD: 2 commands
[Container] 2026/02/16 16:08:28.985556  BUILD: 1 commands
[Container] 2026/02/16 16:08:28.985973 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2026/02/16 16:08:28.985988 Phase context status code:  Message: 
[Container] 2026/02/16 16:08:29.122321 Entering phase INSTALL
[Container] 2026/02/16 16:08:29.165006 Running command wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip

[Container] 2026/02/16 16:08:30.945226 Running command unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
Archive:  terraform_1.14.4_linux_amd64.zip
  inflating: /usr/local/bin/LICENSE.txt  
  inflating: /usr/local/bin/terraform  

[Container] 2026/02/16 16:08:31.740908 Phase complete: INSTALL State: SUCCEEDED
[Container] 2026/02/16 16:08:31.740926 Phase context status code:  Message: 
[Container] 2026/02/16 16:08:31.788113 Entering phase PRE_BUILD
[Container] 2026/02/16 16:08:31.789335 Running command cd ${TF_WORKING_DIR}

[Container] 2026/02/16 16:08:31.798888 Running command terraform init -input=false
Initializing the backend...
Initializing modules...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v6.32.1

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

[Container] 2026/02/16 16:08:37.256812 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2026/02/16 16:08:37.256831 Phase context status code:  Message: 
[Container] 2026/02/16 16:08:37.295855 Entering phase BUILD
[Container] 2026/02/16 16:08:37.296982 Running command terraform apply -input=false -auto-approve tfplan
module.pipeline.aws_codepipeline.pipeline: Modifying... [id=terraform-cicd-demo-codepipeline]
module.pipeline.aws_codepipeline.pipeline: Modifications complete after 0s [id=terraform-cicd-demo-codepipeline]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

[Container] 2026/02/16 16:08:43.114089 Phase complete: BUILD State: SUCCEEDED
[Container] 2026/02/16 16:08:43.114108 Phase context status code:  Message: 
[Container] 2026/02/16 16:08:43.158242 Entering phase POST_BUILD
[Container] 2026/02/16 16:08:43.162166 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2026/02/16 16:08:43.162178 Phase context status code:  Message: 
[Container] 2026/02/16 16:08:43.248641 Set report auto-discover timeout to 5 seconds
[Container] 2026/02/16 16:08:43.254835 Expanding base directory path:  .
[Container] 2026/02/16 16:08:43.258959 Assembling file list
[Container] 2026/02/16 16:08:43.258971 Expanding .
[Container] 2026/02/16 16:08:43.263132 Expanding file paths for base directory .
[Container] 2026/02/16 16:08:43.263501 Assembling file list
[Container] 2026/02/16 16:08:43.263536 Expanding **/*
[Container] 2026/02/16 16:08:43.274135 No matching auto-discover report paths found
[Container] 2026/02/16 16:08:43.274200 Report auto-discover file discovery took 0.025559 seconds
[Container] 2026/02/16 16:08:43.274219 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2026/02/16 16:08:43.274324 Phase context status code:  Message: 

image.png

コンソール上で実行前と実行後を比較すると、ローカルのtfファイルにて追加したタグがパイプラインに付与されています。

実行前
image.png

実行後
image.png

また、github上のコードでもタグが追加されています。
image.png

4. ドリフト検出の実装

ドリフトとは、Terraformのコード(.tfファイル)と実際のAWS環境がズレてしまっている状態のことです。
例えば、誰かがAWSコンソールから手動でリソースを変更したとき、Terraformコードには反映されていないのでドリフトが発生します。

4.1 定期的なドリフト検出の仕組み

terraform plan -detailed-exitcodeを利用すると、終了コードで差分の有無を判定できます。

終了コード 意味
0 差分なし
1 エラー発生
2 差分あり(ドリフト検出)

4.2 ドリフト検出用リソース作成

ドリフト検出用のパイプライン、Buildプロジェクト、Buildspecファイルを作成するため、既存のファイルに定義を追記します。
また、毎日指定時刻に自動でパイプラインを実行するスケジューラーを設定します。これにより「気づかないうちにDriftが溜まっていた」という事態を防げます。

EventBridge SchedulerとEventBridge Rulesの違い
本記事のドリフト検出ではEventBridge Schedulerを使用している。従来のEventBridge Rulesでも定期実行は可能だが、Schedulerはタイムゾーンの直接指定(schedule_expression_timezone = "Asia/Tokyo")、柔軟な時間ウィンドウなどに対応しており、スケジュール用途に特化した新しいサービスとなっている。

# terraform-cicd-demo/modules/pipeline/main.tf
# ドリフト検出用のパイプライン
resource "aws_codepipeline" "driftcheck-pipeline" {
  name          = "${var.project_name}-driftcheck-codepipeline"
  role_arn      = aws_iam_role.pipeline.arn
  pipeline_type = "V2"

  artifact_store {
    location = aws_s3_bucket.pipeline_artifacts.id
    type     = "S3"
    encryption_key {
      id   = aws_kms_key.pipeline.arn
      type = "KMS"
    }
  }

  stage {
    name = "Source"
    action {
      name             = "GitHub"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_output"]
      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.github.arn
        FullRepositoryId = var.github_repo
        BranchName       = var.github_branch
        DetectChanges    = false
      }
    }
  }

  stage {
    name = "Drift"
    action {
      name            = "DriftDetection"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      version         = "1"
      input_artifacts = ["source_output"] # ソースからtfファイルが渡される
      configuration   = { ProjectName = var.codebuild_drift_name }
    }
  }
}

# ---- ドリフト検知用SNSトピック ----
resource "aws_sns_topic" "drift_detection" {
  name = "${var.project_name}-drift_detection"

  tags = {
    ManagedBy  = "terraform"
  }
}

resource "aws_sns_topic_subscription" "drift_detection_email" {
  topic_arn = aws_sns_topic.drift_detection.arn
  protocol  = "email"
  endpoint  = var.approval_email # variables.tfで定義
}

# IAMロール(scheduler.amazonaws.comがCodePipelineを起動できる権限)
resource "aws_iam_role" "scheduler" {
  name = "${var.project_name}-scheduler-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "scheduler.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "scheduler" {
  name = "${var.project_name}-scheduler-policy"
  role = aws_iam_role.scheduler.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "codepipeline:StartPipelineExecution"
      Resource = aws_codepipeline.driftcheck-pipeline.arn
    }]
  })
}

# スケジュール本体
resource "aws_scheduler_schedule" "driftcheck-pipeline-schedule" {
  name = "${var.project_name}-driftcheck-pipeline-schedule"

  flexible_time_window {
    mode = "OFF" # 指定時刻ちょうどに実行
  }

  schedule_expression          = "cron(5 22 * * ? *)" # 毎日JST 22:05
  schedule_expression_timezone = "Asia/Tokyo"        # タイムゾーン直接指定できる

  target {
    arn      = aws_codepipeline.driftcheck-pipeline.arn
    role_arn = aws_iam_role.scheduler.arn
  }
}
# terraform-cicd-demo/modules/pipeline/variables.tf
variable "codebuild_drift_name" {
  type = string
}
# terraform-cicd-demo/modules/pipeline/output.tf
output "pipeline_driftdetection_topic_arn" {
  value = aws_sns_topic.drift_detection.arn
}

output "driftcheck-pipeline_arn" {
  value = aws_codepipeline.driftcheck-pipeline.arn
}

output "scheduler_role_arn" {
  value = aws_iam_role.scheduler.arn
}

output "scheduler_arn" {
  value = aws_scheduler_schedule.driftcheck-pipeline-schedule.arn
}
# modules/codebuild/main.tf
resource "aws_iam_role_policy" "codebuild" {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["iam:GetRole", "iam:PassRole", "iam:ListRolePolicies", "iam:ListRolePolicies", "iam:GetRolePolicy", "iam:ListAttachedRolePolicies", "iam:PutRolePolicy"],
        Resource = [var.pipeline_role_arn, aws_iam_role.codebuild.arn, var.scheduler_role_arn]
      },
      {
        Effect   = "Allow"
        Action   = ["sns:GetTopicAttributes", "sns:ListTagsForResource", "SNS:GetSubscriptionAttributes","SNS:Publish"]
        Resource = [var.pipeline_approval_topic_arn, var.pipeline_driftdetection_topic_arn]
      },
      {
        Effect   = "Allow"
        Action   = "codebuild:BatchGetProjects"
        Resource = [aws_codebuild_project.validate.arn, aws_codebuild_project.plan.arn, aws_codebuild_project.apply.arn,aws_codebuild_project.drift.arn]
      },
      {
        Effect   = "Allow"
        Action   = ["codepipeline:GetPipeline", "codepipeline:ListTagsForResource", "codepipeline:TagResource"]
        Resource = [var.pipeline_arn, var.driftcheck-pipeline_arn]
      },
      {
        Effect   = "Allow"
        Action   = "scheduler:GetSchedule"
        Resource = [var.scheduler_arn]
      }
    ]
  })
}
# ---- Drift ----
resource "aws_codebuild_project" "drift" {
  name         = "${var.project_name}-drift"
  service_role = aws_iam_role.codebuild.arn
  artifacts { type = "CODEPIPELINE" }
  environment {
    compute_type = "BUILD_GENERAL1_SMALL"
    image        = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
    type         = "LINUX_CONTAINER"

    environment_variable {
      name  = "DRIFT_SNS_TOPIC_ARN"
      value = var.pipeline_driftdetection_topic_arn  # SNSトピックのARN
    }
  }
  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec/buildspec_drift.yml"
  }
}
# terraform-cicd-demo/modules/codebuild/variables.tf
variable "pipeline_driftdetection_topic_arn" {
  type = string
}

variable "driftcheck-pipeline_arn" {
  type = string
}

variable "scheduler_role_arn" {
  type = string
}

variable "scheduler_arn" {
  type = string
}
# terraform-cicd-demo/modules/codebuild/output.tf
output "drift_project_name" {
  value = aws_codebuild_project.drift.name
}
# terraform-cicd-demo/main.tf
module "codebuild" {
  pipeline_driftdetection_topic_arn    = module.pipeline.pipeline_driftdetection_topic_arn
  driftcheck-pipeline_arn              = module.pipeline.driftcheck-pipeline_arn
  scheduler_role_arn                   = module.pipeline.scheduler_role_arn
  scheduler_arn                        = module.pipeline.scheduler_arn
}

module "pipeline" {
  codebuild_drift_name                 = module.codebuild.drift_project_name
}
# terraform-cicd-demo/buildspec/buildspec_drift.yml
version: 0.2

env:
  variables:
    TF_VERSION: "1.14.4"
    TF_WORKING_DIR: "."
  shell: bash

phases:
  install:
    commands:
      - wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
      - unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
  pre_build:
    commands:
      - cd ${TF_WORKING_DIR}
      - terraform init -input=false
  build:
    commands:
      - |
        set +e
        terraform plan -detailed-exitcode -input=false -out=drift.tfplan 2>&1 | tee plan_output.txt
        EXIT_CODE=${PIPESTATUS[0]}
        set -e
        if [ $EXIT_CODE -eq 2 ]; then
          echo "DRIFT DETECTED"
          
          cat > drift_message.txt << EOF
        =======================
            Terraform Drift検出
        =======================
        
        検出日時: $(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M:%S JST')
        
        【差分があるリソース】
        EOF
                  
                  terraform show -no-color drift.tfplan | awk '
                    /# .* will be/ { 
                      if (NR > 1) print "";
                      next;
                    }
                    
                    /~|+|-/ && /=/ && !/unchanged/ {
                      gsub(/^[ \t]+/, "   ");
                      gsub(/~/, " ");
                      gsub(/\+/, " ");
                      gsub(/-/, " ");
                      print;
                    }
                  ' >> drift_message.txt
                  
                  echo "" >> drift_message.txt
                  echo "詳細: https://ap-northeast-1.console.aws.amazon.com/codesuite/codebuild/projects/terraform-cicd-demo-drift" >> drift_message.txt
                  
                  aws sns publish --topic-arn "$DRIFT_SNS_TOPIC_ARN" --subject "Drift検出" --message file://drift_message.txt
                  exit 0
                elif [ $EXIT_CODE -eq 1 ]; then
                  echo "ERROR during drift detection"
                  exit 1
                else
                  echo "No drift detected"
                fi

実際にドリフトチェックパイプラインを実行してみます。
image.png

Driftステージの実行ログです。
「No changes. Your infrastructure matches the configuration.
 Terraform has compared your real infrastructure against your configuration
 and found no differences, so no changes are needed.
 No drift detected」
この箇所から実際の環境とコード上に差異がないことがわかります。

[Container] 2026/02/17 16:15:52.552833 Running on CodeBuild On-demand
[Container] 2026/02/17 16:15:52.552844 Waiting for agent ping
[Container] 2026/02/17 16:15:52.754083 Waiting for DOWNLOAD_SOURCE
[Container] 2026/02/17 16:15:54.027025 Phase is DOWNLOAD_SOURCE
[Container] 2026/02/17 16:15:54.028163 CODEBUILD_SRC_DIR=/codebuild/output/src2285338373/src
[Container] 2026/02/17 16:15:54.028695 YAML location is /codebuild/output/src2285338373/src/buildspec/buildspec_drift.yml
[Container] 2026/02/17 16:15:54.030725 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2026/02/17 16:15:54.031149 Processing environment variables
[Container] 2026/02/17 16:15:54.201800 No runtime version selected in buildspec.
[Container] 2026/02/17 16:15:54.235105 Moving to directory /codebuild/output/src2285338373/src
[Container] 2026/02/17 16:15:54.235129 Cache is not defined in the buildspec
[Container] 2026/02/17 16:15:54.273293 Skip cache due to: no paths specified to be cached
[Container] 2026/02/17 16:15:54.273638 Registering with agent
[Container] 2026/02/17 16:15:54.310164 Phases found in YAML: 3
[Container] 2026/02/17 16:15:54.310218  BUILD: 1 commands
[Container] 2026/02/17 16:15:54.310224  INSTALL: 2 commands
[Container] 2026/02/17 16:15:54.310228  PRE_BUILD: 2 commands
[Container] 2026/02/17 16:15:54.310583 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2026/02/17 16:15:54.310596 Phase context status code:  Message: 
[Container] 2026/02/17 16:15:54.407152 Entering phase INSTALL
[Container] 2026/02/17 16:15:54.447775 Running command wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip

[Container] 2026/02/17 16:15:56.043133 Running command unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
Archive:  terraform_1.14.4_linux_amd64.zip
  inflating: /usr/local/bin/LICENSE.txt  
  inflating: /usr/local/bin/terraform  

[Container] 2026/02/17 16:15:56.823601 Phase complete: INSTALL State: SUCCEEDED
[Container] 2026/02/17 16:15:56.823619 Phase context status code:  Message: 
[Container] 2026/02/17 16:15:56.856300 Entering phase PRE_BUILD
[Container] 2026/02/17 16:15:56.857431 Running command cd ${TF_WORKING_DIR}

[Container] 2026/02/17 16:15:56.864915 Running command terraform init -input=false
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing modules...
- codebuild in modules/codebuild
- pipeline in modules/pipeline
- tf_backend in modules/tf-backend
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v6.32.1...
- Installed hashicorp/aws v6.32.1 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

[Container] 2026/02/17 16:16:09.801338 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2026/02/17 16:16:09.801356 Phase context status code:  Message: 
[Container] 2026/02/17 16:16:09.851487 Entering phase BUILD
[Container] 2026/02/17 16:16:09.852817 Running command set +e
        terraform plan -detailed-exitcode -input=false -out=drift.tfplan 2>&1 | tee plan_output.txt
        EXIT_CODE=${PIPESTATUS[0]}
        set -e
        if [ $EXIT_CODE -eq 2 ]; then
          echo "DRIFT DETECTED"
          
          cat > drift_message.txt << EOF
          =======================
              Terraform Drift検出
          =======================
          
          検出日時: $(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M:%S JST')
          
          【差分があるリソース】
          EOF
                  
          terraform show -no-color drift.tfplan | awk '
            /# .* will be/ { 
              if (NR > 1) print "";
              next;
            }
            
            /~|+|-/ && /=/ && !/unchanged/ {
              gsub(/^[ \t]+/, "   ");
              gsub(/~/, " ");
              gsub(/\+/, " ");
              gsub(/-/, " ");
              print;
            }
          ' >> drift_message.txt
                  
          echo "" >> drift_message.txt
          echo "詳細: https://ap-northeast-1.console.aws.amazon.com/codesuite/codebuild/projects/terraform-cicd-demo-drift" >> drift_message.txt
          
          aws sns publish --topic-arn "$DRIFT_SNS_TOPIC_ARN" --subject "Drift検出" --message file://drift_message.txt
          exit 0
        elif [ $EXIT_CODE -eq 1 ]; then
          echo "ERROR during drift detection"
          exit 1
        else
          echo "No drift detected"
module.codebuild.aws_iam_role.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role]
module.pipeline.aws_kms_key.pipeline: Refreshing state... [id=2b1162c5-fc78-4336-987d-7dcab0d0a9d9]
module.pipeline.aws_iam_role.scheduler: Refreshing state... [id=terraform-cicd-demo-scheduler-role]
module.pipeline.aws_iam_role.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role]
module.pipeline.aws_codestarconnections_connection.github: Refreshing state... [id=arn:aws:codestar-connections:ap-northeast-1:xxx:connection/01c7dee9-0241-4b80-8c11-49832e7a6b4b]
module.pipeline.aws_sns_topic.pipeline_approval: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval]
module.tf_backend.aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.aws_sns_topic.drift_detection: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-drift_detection]
module.pipeline.data.aws_caller_identity.current: Reading...
module.pipeline.data.aws_caller_identity.current: Read complete after 0s [id=xxx]
module.pipeline.aws_s3_bucket.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_kms_alias.pipeline: Refreshing state... [id=alias/terraform-cicd-demo-pipeline]
module.pipeline.aws_sns_topic_subscription.drift_detection_email: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-drift_detection:e6644158-54a5-48a1-bfb5-cd86cd000eb1]
module.pipeline.aws_sns_topic_subscription.approval_email: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval:3adb84e3-0932-4171-96e7-6d3d93c9e9d7]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.codebuild.aws_codebuild_project.drift: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-drift]
module.codebuild.aws_codebuild_project.apply: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-apply]
module.codebuild.aws_codebuild_project.validate: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-validate]
module.codebuild.aws_codebuild_project.plan: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-plan]
module.pipeline.aws_codepipeline.driftcheck-pipeline: Refreshing state... [id=terraform-cicd-demo-driftcheck-codepipeline]
module.pipeline.aws_codepipeline.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline]
module.pipeline.aws_scheduler_schedule.driftcheck-pipeline-schedule: Refreshing state... [id=default/terraform-cicd-demo-driftcheck-pipeline-schedule]
module.pipeline.aws_iam_role_policy.scheduler: Refreshing state... [id=terraform-cicd-demo-scheduler-role:terraform-cicd-demo-scheduler-policy]
module.pipeline.aws_iam_role_policy.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role:terraform-cicd-demo-codepipeline-policy]
module.codebuild.aws_iam_role_policy.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role:terraform-cicd-demo-codebuild-policy]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
No drift detected

[Container] 2026/02/17 16:16:16.868547 Phase complete: BUILD State: SUCCEEDED
[Container] 2026/02/17 16:16:16.868568 Phase context status code:  Message: 
[Container] 2026/02/17 16:16:16.922311 Entering phase POST_BUILD
[Container] 2026/02/17 16:16:16.925701 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2026/02/17 16:16:16.925717 Phase context status code:  Message: 
[Container] 2026/02/17 16:16:16.993461 Set report auto-discover timeout to 5 seconds
[Container] 2026/02/17 16:16:16.993550 Expanding base directory path:  .
[Container] 2026/02/17 16:16:16.996695 Assembling file list
[Container] 2026/02/17 16:16:16.996711 Expanding .
[Container] 2026/02/17 16:16:16.999819 Expanding file paths for base directory .
[Container] 2026/02/17 16:16:16.999833 Assembling file list
[Container] 2026/02/17 16:16:16.999836 Expanding **/*
[Container] 2026/02/17 16:16:17.003257 No matching auto-discover report paths found
[Container] 2026/02/17 16:16:17.003276 Report auto-discover file discovery took 0.009815 seconds
[Container] 2026/02/17 16:16:17.003305 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2026/02/17 16:16:17.003311 Phase context status code:  Message: 

次に意図的にコンソール上でドリフトチェックパイプラインにタグを付与することでドリフトを作り出します。
image.png
image.png

今回はEventBridgeSchedulerよりパイプラインを実行します。設定した時間通りにパイプラインが起動することを確認できました。
image.png
image.png
image.png

Driftステージの実行ログです。ドリフトが検出されたため、終了コードが2となり、条件分岐に従って、SNSによってメールが登録済みアドレスに対して送信されます。

[Container] 2026/02/18 13:05:44.809938 Running on CodeBuild On-demand
[Container] 2026/02/18 13:05:44.809958 Waiting for agent ping
[Container] 2026/02/18 13:05:45.014149 Waiting for DOWNLOAD_SOURCE
[Container] 2026/02/18 13:05:46.275525 Phase is DOWNLOAD_SOURCE
[Container] 2026/02/18 13:05:46.276594 CODEBUILD_SRC_DIR=/codebuild/output/src1949182582/src
[Container] 2026/02/18 13:05:46.277138 YAML location is /codebuild/output/src1949182582/src/buildspec/buildspec_drift.yml
[Container] 2026/02/18 13:05:46.277779 Selecting shell bash as specified in buildspec.
[Container] 2026/02/18 13:05:46.279198 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2026/02/18 13:05:46.279300 Processing environment variables
[Container] 2026/02/18 13:05:46.417311 No runtime version selected in buildspec.
[Container] 2026/02/18 13:05:46.455404 Moving to directory /codebuild/output/src1949182582/src
[Container] 2026/02/18 13:05:46.455429 Cache is not defined in the buildspec
[Container] 2026/02/18 13:05:46.501132 Skip cache due to: no paths specified to be cached
[Container] 2026/02/18 13:05:46.501465 Registering with agent
[Container] 2026/02/18 13:05:46.543513 Phases found in YAML: 3
[Container] 2026/02/18 13:05:46.543533  INSTALL: 2 commands
[Container] 2026/02/18 13:05:46.543538  PRE_BUILD: 2 commands
[Container] 2026/02/18 13:05:46.543542  BUILD: 1 commands
[Container] 2026/02/18 13:05:46.543951 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2026/02/18 13:05:46.543976 Phase context status code:  Message: 
[Container] 2026/02/18 13:05:46.668166 Entering phase INSTALL
[Container] 2026/02/18 13:05:46.710165 Running command wget -q https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip

[Container] 2026/02/18 13:05:48.370601 Running command unzip -o terraform_${TF_VERSION}_linux_amd64.zip -d /usr/local/bin/
Archive:  terraform_1.14.4_linux_amd64.zip
  inflating: /usr/local/bin/LICENSE.txt  
  inflating: /usr/local/bin/terraform  

[Container] 2026/02/18 13:05:49.288496 Phase complete: INSTALL State: SUCCEEDED
[Container] 2026/02/18 13:05:49.288515 Phase context status code:  Message: 
[Container] 2026/02/18 13:05:49.340694 Entering phase PRE_BUILD
[Container] 2026/02/18 13:05:49.342099 Running command cd ${TF_WORKING_DIR}

[Container] 2026/02/18 13:05:49.354933 Running command terraform init -input=false
Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing modules...
- codebuild in modules/codebuild
- pipeline in modules/pipeline
- tf_backend in modules/tf-backend
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v6.32.1...
- Installed hashicorp/aws v6.32.1 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

[Container] 2026/02/18 13:06:04.703028 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2026/02/18 13:06:04.703062 Phase context status code:  Message: 
[Container] 2026/02/18 13:06:04.748365 Entering phase BUILD
[Container] 2026/02/18 13:06:04.749680 Running command set +e
terraform plan -detailed-exitcode -input=false -out=drift.tfplan 2>&1 | tee plan_output.txt
EXIT_CODE=${PIPESTATUS[0]}
set -e
if [ $EXIT_CODE -eq 2 ]; then
  echo "DRIFT DETECTED"
  
  cat > drift_message.txt << EOF
=======================
    Terraform Drift検出
=======================

検出日時: $(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M:%S JST')

【差分があるリソース】
EOF
          
          terraform show -no-color drift.tfplan | awk '
            /# .* will be/ { 
              if (NR > 1) print "";
              next;
            }
            
            /~|+|-/ && /=/ && !/unchanged/ {
              gsub(/^[ \t]+/, "   ");
              gsub(/~/, " ");
              gsub(/\+/, " ");
              gsub(/-/, " ");
              print;
            }
          ' >> drift_message.txt
          
          echo "" >> drift_message.txt
          echo "詳細: https://ap-northeast-1.console.aws.amazon.com/codesuite/codebuild/projects/terraform-cicd-demo-drift" >> drift_message.txt
          
          aws sns publish --topic-arn "$DRIFT_SNS_TOPIC_ARN" --subject "Drift検出" --message file://drift_message.txt
          exit 0
        elif [ $EXIT_CODE -eq 1 ]; then
          echo "ERROR during drift detection"
          exit 1
        else
          echo "No drift detected"
        fi
module.pipeline.data.aws_caller_identity.current: Reading...
module.pipeline.aws_iam_role.scheduler: Refreshing state... [id=terraform-cicd-demo-scheduler-role]
module.pipeline.aws_sns_topic.pipeline_approval: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval]
module.pipeline.aws_codestarconnections_connection.github: Refreshing state... [id=arn:aws:codestar-connections:ap-northeast-1:xxx:connection/01c7dee9-0241-4b80-8c11-49832e7a6b4b]
module.tf_backend.aws_s3_bucket.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.pipeline.data.aws_caller_identity.current: Read complete after 0s [id=xxx]
module.pipeline.aws_iam_role.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role]
module.pipeline.aws_kms_key.pipeline: Refreshing state... [id=2b1162c5-fc78-4336-987d-7dcab0d0a9d9]
module.codebuild.aws_iam_role.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role]
module.pipeline.aws_sns_topic.drift_detection: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-drift_detection]
module.pipeline.aws_s3_bucket.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_kms_alias.pipeline: Refreshing state... [id=alias/terraform-cicd-demo-pipeline]
module.pipeline.aws_sns_topic_subscription.approval_email: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-pipeline_approval:3adb84e3-0932-4171-96e7-6d3d93c9e9d7]
module.pipeline.aws_sns_topic_subscription.drift_detection_email: Refreshing state... [id=arn:aws:sns:ap-northeast-1:xxx:terraform-cicd-demo-drift_detection:e6644158-54a5-48a1-bfb5-cd86cd000eb1]
module.pipeline.aws_s3_bucket_versioning.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.pipeline.aws_s3_bucket_public_access_block.pipeline_artifacts: Refreshing state... [id=terraform-cicd-demo-artifacts-xxx]
module.tf_backend.aws_s3_bucket_versioning.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_public_access_block.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.tf_backend.aws_s3_bucket_server_side_encryption_configuration.terraform_state: Refreshing state... [id=my-terraform-state-cicdpipeline-verification]
module.codebuild.aws_codebuild_project.validate: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-validate]
module.codebuild.aws_codebuild_project.apply: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-apply]
module.codebuild.aws_codebuild_project.plan: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-plan]
module.codebuild.aws_codebuild_project.drift: Refreshing state... [id=arn:aws:codebuild:ap-northeast-1:xxx:project/terraform-cicd-demo-drift]
module.pipeline.aws_codepipeline.driftcheck-pipeline: Refreshing state... [id=terraform-cicd-demo-driftcheck-codepipeline]
module.pipeline.aws_codepipeline.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline]
module.pipeline.aws_iam_role_policy.pipeline: Refreshing state... [id=terraform-cicd-demo-codepipeline-role:terraform-cicd-demo-codepipeline-policy]
module.pipeline.aws_iam_role_policy.scheduler: Refreshing state... [id=terraform-cicd-demo-scheduler-role:terraform-cicd-demo-scheduler-policy]
module.pipeline.aws_scheduler_schedule.driftcheck-pipeline-schedule: Refreshing state... [id=default/terraform-cicd-demo-driftcheck-pipeline-schedule]
module.codebuild.aws_iam_role_policy.codebuild: Refreshing state... [id=terraform-cicd-demo-codebuild-role:terraform-cicd-demo-codebuild-policy]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.pipeline.aws_codepipeline.driftcheck-pipeline will be updated in-place
  ~ resource "aws_codepipeline" "driftcheck-pipeline" {
        id             = "terraform-cicd-demo-driftcheck-codepipeline"
        name           = "terraform-cicd-demo-driftcheck-codepipeline"
      ~ tags           = {
          - "draftcheck" = "true" -> null
        }
      ~ tags_all       = {
          - "draftcheck" = "true" -> null
        }
        # (6 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

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

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: drift.tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "drift.tfplan"
DRIFT DETECTED
{
    "MessageId": "6a1cab72-2f38-56e7-93a1-c97d94ad91f6"
}

[Container] 2026/02/18 13:06:29.405785 Phase complete: BUILD State: SUCCEEDED
[Container] 2026/02/18 13:06:29.405809 Phase context status code:  Message: 
[Container] 2026/02/18 13:06:29.463845 Entering phase POST_BUILD
[Container] 2026/02/18 13:06:29.466699 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2026/02/18 13:06:29.466719 Phase context status code:  Message: 
[Container] 2026/02/18 13:06:29.540863 Set report auto-discover timeout to 5 seconds
[Container] 2026/02/18 13:06:29.542031 Expanding base directory path:  .
[Container] 2026/02/18 13:06:29.547347 Assembling file list
[Container] 2026/02/18 13:06:29.547362 Expanding .
[Container] 2026/02/18 13:06:29.551932 Expanding file paths for base directory .
[Container] 2026/02/18 13:06:29.551946 Assembling file list
[Container] 2026/02/18 13:06:29.552030 Expanding **/*
[Container] 2026/02/18 13:06:29.560481 No matching auto-discover report paths found
[Container] 2026/02/18 13:06:29.560497 Report auto-discover file discovery took 0.019634 seconds
[Container] 2026/02/18 13:06:29.560511 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2026/02/18 13:06:29.560517 Phase context status code:  Message: 

image.png

4.3 ドリフト検出後のリカバリ手順

ドリフトが検出された場合、対応方法は2つあります。

方法A: コードを正とする(実環境をコードに合わせる)

terraform apply
# => 手動変更を上書きし、コード定義の状態に戻す

方法B: 実環境を正とする(コードを実環境に合わせる)

terraform plan                             # 差分を確認
# tfファイルを手動変更に合わせて修正
terraform plan                             # 差分がなくなることを確認

まとめ

本記事では、Terraform、CodePipeline、CodeBuildを使って「コードをpushしたらインフラが自動でデプロイされる」CI/CDパイプラインをゼロから構築し、さらにDrift検出の自動化までを一通り検証しました。
実際に手を動かして確認できたポイントは以下のとおりです。

  • S3バックエンド: ステートファイルをS3で一元管理し、use_lockfile = trueによるS3ネイティブロックで同時実行を防止できること。2ターミナルでの同時実行でロックエラーが発生することも確認しました。
  • module構成: リソースを tf-backend / pipeline / codebuildに分離し、ルートのmain.tfからoutput経由で依存関係をつなぐことで、見通しの良い構成にできること。
  • CodePipeline V2: GitHubへのpushをトリガーにSource → Validate → Plan → Approval → Applyの5ステージが順に実行され、SNS メールでの承認フローを挟んだうえで自動デプロイできること。
  • ドリフト検出: EventBridge Schedulerで指定時刻にドリフトチェック用パイプラインを起動し、terraform plan -detailed-exitcodeの終了コードで差分を判定、検出時にはSNSでメール通知が届くこと。

今回は検証目的のためIAM権限を広めに設定している箇所がありますが、本番運用では最小権限の原則に従って絞り込むことを推奨します。また、Secrets Managerとの統合や複数環境(dev/staging/prod)への展開については、本記事の構成を土台として拡張していけます。

この記事が皆様のお役に立てば幸いです。質問やフィードバックがあれば、コメント欄でお気軽にお寄せください!

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?