この記事では、AWSにおけるTerraform用CI/CD構築をTerraformによって構築する方法についてご紹介します。
実際の構築手順ベースの記事となっていますので、CI/CD構築未経験の方やTerraform初心者の方がハンズオンする際の参考になれば幸いです。

目次

  1. 前提条件と開発環境
  2. システムアーキテクチャ
  3. 構築①:tfstateとロック制御管理リソースの作成
  4. 構築②:AWS CodeCommit(SSH接続設定とリポジトリ作成)
  5. 構築③:CI/CDパイプラインと承認フローの統合(CodeBuild / CodePipeline)
  6. 構築④:メンション付きSlack通知の実装(EventBridge / Lambda / Amazon Q Developer)
  7. おわりに

1. 前提条件と開発環境

本記事では、以下の環境で動作を確認しています。

  • OS: Windows (WSL2)
  • Git: v2.43.0
  • Terraform: v1.14.4
  • AWS CLI: v2.33.7
  • AWS Provider: v6.32.1
  • 使用リージョン: ap-northeast-1 (東京)

2. システムアーキテクチャ

今回構築するシステム構成は以下の内容です。

構成のポイント

  • AWSネイティブなCI/CD
    AWS CodeCommit(以下、CodeCommit)、AWS CodeBuild(以下、CodeBuild)、AWS CodePipeline(以下、CodePipeline)の連携により、AWS上でCI/CDパイプラインを完結。
  • 承認ベースのデプロイ
    terraform plan 結果を確認した後に手動承認を行うフロー。
  • カスタム通知の実装
    「特定ユーザーへのメンション」や「整形されたメッセージ」の通知設定をAmazon EventBridge(以下、EventBridge)、AWS Lambda(以下、Lambda)、Amazon SNS(以下、SNS)、Amazon Q Developer(旧称:AWS Chatbot、以下Chatbot)の連携で実現。

3. 構築①:tfstateとロック制御管理リソースの作成

Stateファイル管理のベストプラクティス

Terraformでインフラを管理する上で、Stateファイル(terraform.tfstate)の扱いは重要です。
AWSの公式ブログでは、以下の管理方法が推奨されています。
1. リモートバックエンドでの管理(S3):StateファイルをローカルPCではなく、耐久性の高いS3バケットで集中管理することで、チーム間での共有を容易にし、紛失リスクを低減します。
2. 排他制御の実装(DynamoDB):複数のユーザーやパイプラインが同時にterraform applyを実行してStateファイルが破損するのを防ぐため、DynamoDBを使用してStateファイルのロック管理を行います。
3. 機密情報の保護:Stateファイルにはリソースのパスワードなどが平文で含まれる場合があるため、S3の暗号化機能を有効にして保存します。

S3バケットとDynamoDBテーブルの作成

ディレクトリ構造

3章では以下のディレクトリを作成し、必要なファイルを配置していきます。

terraform-cicd
└──backend_infra/
├── main.tf # S3とDynamoDBのリソース定義
├── variables.tf # バケット名などの変数
├── output.tf # 作成したバケット名の出力
└── backend.tf # 最初は空、またはコメントアウト

リソースの定義

ここでは、検証のためバージョニング等の設定はせずにS3バケットとDynamoDBテーブルを定義します。
backend_infra/main.tf

provider "aws" {
region = var.region
}

# --- State保存用のS3バケット ---
resource "aws_s3_bucket" "terraform_state" {
bucket = "${var.name}-tfstate" #グローバルで一意な名前にする
}

# --- ロック制御用のDynamoDBテーブル ---
resource "aws_dynamodb_table" "terraform_locks" {
name = "${var.name}-tflock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"

attribute {
name = "LockID"
type = "S"
}
}

backend_infra/variables.tf

variable "name" {
type = string
description = "プロジェクトの識別名"
default = "mytest" # 好きな識別名を定義
}

variable "region" {
type = string
default = "ap-northeast-1" # 構築するリージョンを定義
}

backend_infra/output.tf

output "state_bucket_name" {
value = aws_s3_bucket.terraform_state.id
}

output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_locks.name
}

上記の定義が完了したら、以下のコマンドでコードをAWS環境に適用します。
ここでは、まだS3バケットが存在しないため、まずはローカルにtfstateを置いた状態でリソースを作成します。

cd ~/terraform-cicd/backend_infra
terraform init # Terraform初期化
terraform plan # 作成されるリソースを確認
terraform apply # AWS環境へのコード適用

バックエンド設定の追記

S3バケットとDynamoDBテーブルが作成されたら、それらをtfstateとロック管理の保存先として指定します。
backend_infra/backend.tf

terraform {
backend "s3" {
bucket = "mytest-tfstate" # main.tfで定義したバケット名
key = "backend/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "mytest-tflock" # main.tfで定義したテーブル名
encrypt = true
}
}

ステートファイル管理先の変更

-reconfigureを付けて初期化コマンドを実行し、ローカルのtfstateファイルをS3へ転送します。

terraform init -reconfigure

ここまでの手順で、State管理用のS3バケットとロック管理用DynamoDBテーブルの作成、およびリモートでのState管理の土台が完成しました。
次章では、CodeCommitリポジトリを作成していきます。

4. 構築②:AWS CodeCommit(SSH接続設定とリポジトリ作成)

CodeCommitリポジトリの作成

本章では、Terraformのソースコードを管理するためのリポジトリを作成します。
最終的には以下のディレクトリ構造となり、3つのリポジトリが必要となります。
今回は管理や修正を疎結合で行えるよう、それぞれ個別のリポジトリで管理するように構成します。

terraform-cicd/ # ルートディレクトリ
├── backend_infra  # バックエンド用
├── cicd       # CI/CD用
└── app_infra # Terraformで実際に構築したいリソース定義用

リポジトリの定義

まず、cicd/ ディレクトリ配下に codecommit.tfを作成し、3つのリポジトリを定義します。
cicd/codecommit.tf

# 1. バックエンド構築用コードのリポジトリ
resource "aws_codecommit_repository" "backend_infra" {
repository_name = "${var.name}-backend-infra"
}

# 2. CI/CD基盤自体のコードのリポジトリ
resource "aws_codecommit_repository" "cicd_infra" {
repository_name = "${var.name}-cicd-infra"
}

# 3. Terraformで実際に構築したいインフラ用のリポジトリ(パイプラインの監視対象)
resource "aws_codecommit_repository" "app_infra" {
repository_name = "${var.name}-app-infra"
}

cicd/provider.tfを作成し、構築先リージョンを定義します。
cicd/provider.tf

provider "aws" {
region = var.region
}

cicd/variables.tfを作成し、変数を定義します。
cicd/variables.tf

variable "name" {
type = string
description = "プロジェクトの識別名"
default = "mytest" # 好きな識別名を定義
}

variable "region" {
type = string
default = "ap-northeast-1" # 構築先リージョン
}

後から確認しやすくするため、以下のようにリポジトリへの接続URLをcicd/output.tfに定義しておきます。
cicd/output.tf

output "codecommit_backend_clone_url_ssh" {
value = aws_codecommit_repository.backend_infra.clone_url_ssh
}
output "codecommit_cicd_clone_url_ssh" {
value = aws_codecommit_repository.cicd_infra.clone_url_ssh
}
output "codecommit_app_clone_url_ssh" {
value = aws_codecommit_repository.app_infra.clone_url_ssh
}

3章で作成したバックエンド管理先を指定するため、以下をcicd/backend.tfに定義します。
cicd/backend.tf

terraform {
backend "s3" {
bucket = "mytest-tfstate" # 3章で作成したバケット名
key = "cicd/terraform.tfstate" # backend_infra/backend.tfとは別のパスを指定
region = "ap-northeast-1"
dynamodb_table = "mytest-tflock" # 3章で作成したテーブル名
encrypt = true
}
}

ここまで定義できたら、以下のコマンドを実行してAWS上に3つの空リポジトリを作成します。

cd ~/terraform-cicd/cicd
terraform init
terraform plan
terraform apply

SSHによるCodeCommit接続設定

CodeCommitへの接続にはHTTPSとSSHがありますが、今回は一度設定すれば接続時のユーザ名/パスワード入力の手間が省けるためSSH接続を使用します。
参考ドキュメント:https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/setting-up-ssh-unixes.html

Step1:SSHキーペアの作成(ローカルPC)

ターミナルで以下のコマンドを実行し、CodeCommit接続用のキーペアを生成します。

cd ~/.ssh
ssh-keygen -t rsa -b 4096
# 以下のようにオプションの設定値を聞かれるため、任意で設定してください。ここでは、キーペア名のみ設定しています。
Generating public/private rsa key pair.
Enter file in which to save the key (/home/user-name/.ssh/id_rsa):codecommit_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:

Step2: IAMユーザーへの公開鍵登録

次のコマンドを使用して、公開鍵の内容を確認してコピーします。

cat ~/.ssh/codecommit_rsa.pub
# 出力例
ssh-rsa EXAMPLE-AfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJB
gNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb2
5zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhc
NMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAw
DgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDAS=EXAMPLE user-name@ip-192-0-2-137

AWSコンソールのIAM > ユーザー > [対象ユーザ] > セキュリティ認証情報 タブを開きます。

AWS CodeCommitのSSH公開キーセクションで、SSH公開キーのアップロードをクリックし、コピーした内容を貼り付けます。
アップロード後に表示されるSSHキーIDを控えておいてください。

Step3: ローカルのSSH設定ファイルの編集

~/.ssh/config ファイルを作成(または編集)し、接続情報を記述します。
~/.ssh/config

Host git-codecommit.*.amazonaws.com
User [Step2で控えたSSHキーID]
IdentityFile ~/.ssh/codecommit_rsa # Step2で作成した秘密鍵のパス

ファイルの権限を適切に設定します。

chmod 600 ~/.ssh/config

Step4: 接続確認

以下のコマンドを実行し、正常に認証されるか確認します。

ssh git-codecommit.ap-northeast-1.amazonaws.com

「You have successfully authenticated over SSH.~」というメッセージが表示されれば成功です。

リポジトリにコードをpushする

SSH接続が完了したので、先ほど作成した/backend_infraのコードをPushします。

# pushするディレクトリへ移動
cd ~/terraform-cicd/backend_infra

# Gitリポジトリの初期化
git init

# .gitignoreの作成
cat < .gitignore
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
EOF

# ファイルのステージングとコミット
git add .
git commit -m "Initial commit: backend infrastructure"

# SSH形式のURLを指定して登録(リポジトリ名は自身の設定に合わせてください)
git remote add origin ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/mytest-backend-infra

# ブランチ名をmainに変更してPush
git branch -m main
git push -u origin main

ここまでの手順で、CodeCommitリポジトリの作成と3章で作成したバックエンド管理インフラのコードをCodeCommitにPushしました。
次章では、実際にCI/CDパイプラインをTerraformで構築していきます。

5. 構築③:CI/CDパイプラインと承認フローの統合(CodeBuild / CodePipeline)

本章では、CodeCommitにPushされたコードを検知し、自動でterraform planを実行し、手動で承認した時のみterraform applyが走る仕組みを構築します。

構成の概要

パイプラインは以下の4つのステージで構成します。
1. Source: CodeCommitのapp_infraリポジトリから最新コードを取得。
2. Build(Plan): CodeBuildでterraform planを実行。
3. Approve: レビュー担当者による手動承認。
4. Deploy(Apply): 承認されたらCodeBuildでterraform applyを実行。

BuildSpecファイルの作成

CodeBuildが実行する手順書として、2つのyamlファイルをapp_infra/に配置します。
/app_infra/buildspec_plan.yml

version: 0.2
phases:
install:
commands:
- tf_version="1.14.4" # 使用するバージョン
- wget https://releases.hashicorp.com/terraform/${tf_version}/terraform_${tf_version}_linux_amd64.zip
- unzip terraform_${tf_version}_linux_amd64.zip
- mv terraform /usr/local/bin/
pre_build:
commands:
- terraform init
build:
commands:
# plan結果を 'tfplan' というファイル名で書き出す
- terraform plan -out=tfplan
artifacts:
# 次のステージ(Apply)に引き継ぐファイルを指定
files:
- tfplan
- "*.tf"
- buildspec_apply.yml

/app_infra/buildspec_apply.yml

version: 0.2

phases:
install:
commands:
- tf_version="1.14.4" # Terraformバージョン
- wget https://releases.hashicorp.com/terraform/${tf_version}/terraform_${tf_version}_linux_amd64.zip
- unzip terraform_${tf_version}_linux_amd64.zip
- mv terraform /usr/local/bin/
pre_build:
commands:
- terraform init
build:
commands:
# 前のステージで作られた plan 結果をそのまま適用する
- terraform apply -auto-approve tfplan

権限の定義

次にcicd/配下にリソースを定義していきます。
CI/CDの各リソースが操作を実行するための権限をcicd/iam.tfに定義します。
cicd/iam.tf

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

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

resource "aws_iam_role_policy" "codepipeline_policy" {
name = "${var.name}-codepipeline_policy"
role = aws_iam_role.codepipeline_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = ["s3:GetObject", "s3:PutObject", "s3:GetBucketVersioning"]
Resource = [aws_s3_bucket.pipeline_artifacts.arn, "${aws_s3_bucket.pipeline_artifacts.arn}/*"]
Effect = "Allow"
},
{
Action = ["codebuild:BatchGetBuilds", "codebuild:StartBuild"]
Resource = "*"
Effect = "Allow"
},
{
Action = ["codecommit:GetBranch", "codecommit:GetCommit", "codecommit:UploadArchive", "codecommit:GetUploadArchiveStatus", "codecommit:CancelUploadArchive"]
Resource = "*" # 本来はCodeCommitのリポジトリARNに絞るのがベストですが、検証のため"*"としています。
Effect = "Allow"
}
]
})
}

# --- CodeBuild用 IAMロール ---
resource "aws_iam_role" "codebuild_role" {
name = "${var.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_policy" {
name = "${var.name}-codebuild_policy"
role = aws_iam_role.codebuild_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "*"
Effect = "Allow"
},
{
Action = ["s3:GetObject", "s3:PutObject", "s3:GetBucketVersioning"]
Resource = [aws_s3_bucket.pipeline_artifacts.arn, "${aws_s3_bucket.pipeline_artifacts.arn}/*"]
Effect = "Allow"
},
# TerraformがStateファイルを読むためにbackend用S3へのアクセスも必要
{
Action = ["s3:ListBucket", "s3:GetObject", "s3:PutObject"]
Resource = ["arn:aws:s3:::${var.name}-tfstate", "arn:aws:s3:::${var.name}-tfstate/*"]
Effect = "Allow"
},
{
Action = ["dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
Resource = "arn:aws:dynamodb:ap-northeast-1:*:table/${var.name}-tflock"
Effect = "Allow"
},
# ↓ ここにTerraformで操作したいリソース(VPC等)の権限を追加
{
Action = ["*"]
Resource = "*"
Effect = "Allow"
}
]
})
}

# EventBridge用IAM ロール
resource "aws_iam_role" "eventbridge_role" {
name = "${var.name}-eventbridge-role"

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

resource "aws_iam_role_policy" "event_bridge_policy" {
name = "${var.name}-eventbridge-policy"
role = aws_iam_role.eventbridge_role.id

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

自動起動用のEventBridgeの定義

CodeCommitへのPushを検知してパイプラインを起動させるため、cicd/eventbridge.tfを定義します。
cicd/eventbridge.tf

# EventBridge ルール: CodeCommit の変更を検知
resource "aws_cloudwatch_event_rule" "pipeline_trigger" {
name = "${var.name}-trigger-rule"
description = "Trigger CodePipeline when code is pushed to CodeCommit main branch"

event_pattern = jsonencode({
source = ["aws.codecommit"]
detail-type = ["CodeCommit Repository State Change"]
resources = [aws_codecommit_repository.app_infra.arn]
detail = {
event = ["referenceCreated", "referenceUpdated"]
referenceType = ["branch"]
referenceName = ["main"]
}
})
}

# EventBridge ターゲット: ルール検知時に CodePipeline を起動
resource "aws_cloudwatch_event_target" "pipeline_target" {
rule = aws_cloudwatch_event_rule.pipeline_trigger.name
target_id = "TriggerCodePipeline"
arn = aws_codepipeline.terraform_pipeline.arn
role_arn = aws_iam_role.eventbridge_role.arn
}

パイプライン用S3の定義

パイプラインの動作中に、データの受け渡しを行うため、cicd/s3.tfを定義します。
cicd/s3.tf

resource "aws_s3_bucket" "pipeline_artifacts" {
bucket = "${var.name}-pipeline-artifacts"
force_destroy = true
}

CodeBuildプロジェクトの定義

cicd/codebuild.tfを作成し、以下のように2つのCodeBuildプロジェクトを定義します。
cicd/codebuild.tf

# --- 共通の環境設定) ---
locals {
builder_image = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
}

# 1. Terraform Plan用プロジェクト
resource "aws_codebuild_project" "terraform_plan" {
name = "${var.name}-tf-plan"
service_role = aws_iam_role.codebuild_role.arn

artifacts {
type = "CODEPIPELINE"
}

environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = local.builder_image
type = "LINUX_CONTAINER"
privileged_mode = false
}

source {
type = "CODEPIPELINE"
buildspec = "buildspec_plan.yml" # 先ほど作成した実行手順書を指定
}
}

# 2. Terraform Apply用プロジェクト
resource "aws_codebuild_project" "terraform_apply" {
name = "${var.name}-tf-apply"
service_role = aws_iam_role.codebuild_role.arn

artifacts {
type = "CODEPIPELINE"
}

environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = local.builder_image
type = "LINUX_CONTAINER"
privileged_mode = false
}

source {
type = "CODEPIPELINE"
buildspec = "buildspec_apply.yml" # 先ほど作成した実行手順書を指定
}
}

CodePipelineの定義

各ステージを繋ぎ合わせるためにCodePipelineを作成します。今回は手動承認ステージとしてApprovalステージを作成します。
cicd/codepipeline.tf

resource "aws_codepipeline" "terraform_pipeline" {
name = "${var.name}-pipeline"
role_arn = aws_iam_role.codepipeline_role.arn

artifact_store {
location = aws_s3_bucket.pipeline_artifacts.bucket
type = "S3"
}

# --- Stage 1: Source (CodeCommitから取得) ---
stage {
name = "Source"
action {
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["source_output"]

configuration = {
RepositoryName = aws_codecommit_repository.app_infra.repository_name
BranchName = "main"
# EventBridgeを使用して検知するためポーリングはオフにする
PollForSourceChanges = "false"
}
}
}

# --- Stage 2: Plan (CodeBuildで実行) ---
stage {
name = "Plan"
action {
name = "Terraform-Plan"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["source_output"]
output_artifacts = ["plan_output"]

configuration = {
ProjectName = aws_codebuild_project.terraform_plan.name
}
}
}

# --- Stage 3: Approval (手動承認待ち) ---
stage {
name = "Approval"
action {
name = "Terraform-Approval"
category = "Approval"
owner = "AWS"
provider = "Manual"
version = "1"
}
}

# --- Stage 4: Apply (CodeBuildで実行) ---
stage {
name = "Apply"
action {
name = "Terraform-Apply"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["plan_output"]

configuration = {
ProjectName = aws_codebuild_project.terraform_apply.name
}
}
}
}

デプロイと動作確認

ここまでのコード作成が完了したら、CI/CDパイプラインをデプロイし、実際に実行されるかを確認します。

Step1: パイプライン基盤のデプロイ

cicd/ディレクトリで、定義したリソースをAWS上に構築します。

cd ~/terraform-cicd/cicd
terraform plan
terraform apply

Step2: app_infraのバックエンド設定

次に、実際にパイプラインを使用して構築したいリソースを定義するapp_infra/の状態管理を、3章で作成したS3バケットおよびDynamoDBテーブルで行うように設定します。
app_infra/backend.tf

terraform {
backend "s3" {
bucket = "mytest-tfstate" # 3章で作成したバケット名
key = "app-infra/terraform.tfstate" # backend_infraとは別のパスを指定
region = "ap-northeast-1"
dynamodb_table = "mytest-tflock" # 3章で作成したテーブル名
encrypt = true
}
}

Step3: テスト用リソースの追加とPush

app_infra/でも変数を使用できるよう、variables.tfを作成します。
app_infra/variables.tf

variable "name" {
type = string
description = "プロジェクトの識別名"
default = "mytest"
}

variable "region" {
type = string
default = "ap-northeast-1" # 構築するリージョンを定義
}

app_infra/provider.tfも以下のように定義します。
app_infra/provider.tf

provider "aws" {
region = var.region
}

パイプラインの動作を確認するため、app_infraにテスト用リソースを追加してPushしてみます。
例えば、main.tfに以下のS3バケットを作成します。
app_infra/main.tf

resource "aws_s3_bucket" "test_app_bucket" {
bucket = "${var.name}-test-bucket" # グローバルでユニークな値
force_destroy = true
tags = {
Name = "TriggerTest"
}
}

ローカルでterraform planを確認したい場合
基本的にはgit pushすればCodeBuildが実行してくれますが、Pushする前にローカルでPlanして確認したい場合は、app_infraディレクトリでterraform initを実行すれば、ローカルでもterraform planが可能になります。
ただし、チームで開発する際には「ローカルではPlanまで」を実行し、「Applyはパイプライン」をルール化することが必要になります。

ここまで準備ができたら、修正したファイルをCodeCommitへPushします。

cd ~/terraform-cicd/app_infra
git init
# 4章と同様に.gitignoreを作成
cat < .gitignore
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
EOF

git add .
git commit -m "Setup: app_infra with backend and test bucket"
git remote add origin ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/mytest-app-infra #4章で作成したリポジトリのURL
git branch -m main
git push -u origin main

AWSコンソールのCodePipeline画面を開き、以下の流れを確認します。
1. Source: Push検知により自動で起動
2. Build(Plan): terraform planが実行される
3. Approval: ステータスが「承認待ち」で停止する

Step5: 手動承認と適用

Build(Plan)を実行したCodeBuildのログから、terraform planの実行結果を確認します。
問題なければ、CodePipelineのApprovalステージを承認することでDeploy(Apply)が実行され、実際にS3バケットが作成されます。

本章のまとめ

ここまでの作業で、app_infraにAWSリソースを定義し、コードをPushすることで自動でPlan→手動承認→Applyが実行されるパイプラインが完成しました。
しかし、現状ではコンソールを確認しないと承認待ちに気づけないという不便さがあります。
次章以降では、EventBridge、Lambda、SNS、ChatbotによるSlackへのメンション付きメッセージ送信の仕組みを構築していきます。

6. 構築④:メンション付きSlack通知の実装(EventBridge / Lambda / Chatbot)

構成の概要

  1. EventBridge: パイプラインが「承認待ち」になったイベントを検知。
  2. Lambda: イベント情報をメンションを含めた形に整形。
  3. SNS: 整形されたメッセージをChatbotへ中継。
  4. Chatbot: Slackの特定チャンネルへ通知。

IAMロールの定義

LambdaとChatbotが動作するために必要な権限をcicd/iam.tfに追記します。
cicd/iam.tf

# Lambda用ロール
resource "aws_iam_role" "lambda_notification_role" {
name = "${var.name}-lambda-notification-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}

resource "aws_iam_role_policy" "lambda_notification_policy" {
name = "${var.name}-lambda-notification-policy"
role = aws_iam_role.lambda_notification_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sns:Publish"
Resource = aws_sns_topic.pipeline_notification.arn
Effect = "Allow"
},
{
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "*"
Effect = "Allow"
}
]
})
}

# Chatbot用ロール
resource "aws_iam_role" "chatbot_role" {
name = "${var.name}-chatbot-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "chatbot.amazonaws.com" }
}]
})
}

resource "aws_iam_role_policy_attachment" "chatbot_read_only" {
role = aws_iam_role.chatbot_role.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" # Slackで状態を表示するために必要
}

Lambdaの定義

メッセージ整形用のPythonコードと、Lambdaリソースを定義します。
cicd/lambda/message_formatting.pyを作成し、以下のコードを記述します。
cicd/lambda/message_formatting.py

import json
import boto3
import os

sns = boto3.client('sns')

def lambda_handler(event, context):
raw_ids = os.environ.get('SLACK_MENTION_IDS', '')
if raw_ids:
# IDが複数あればリストにして渡す
mentions = [f"<@{i.strip()}>" for i in raw_ids.split(',')]
# スペース区切りで結合 ("<@U123> <@U456>")
mention_string = " ".join(mentions)
else:
mention_string = ""

detail = event.get('detail', {})
pipeline_name = detail.get('pipeline', 'Unknown')
stage = detail.get('stage', 'Unknown')
state = detail.get('state', 'Unknown')

# Slackで表示したいテキスト(メンション込み)
main_text = f"{mention_string}n【Pipeline Notification】nPipeline: {pipeline_name}nStage: {stage}nStatus: {state}"

if stage == "Approval":
region = event.get('region', 'ap-northeast-1')
link = f"https://{region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/{pipeline_name}/view"
main_text += f"n承認が必要です。確認してください: {link}"

# --- JSON構造を作成 ---
custom_message = {
"version": "1.0",
"source": "custom",
"content": {
"title": f"CodePipeline Approval: {pipeline_name}",
"description": main_text
}
}

try:
response = sns.publish(
TopicArn=os.environ['TARGET_SNS_ARN'],
# JSON文字列として送信
Message=json.dumps(custom_message)
)
print(f"Response: {response}")
except Exception as e:
print(f"Error publishing to SNS: {str(e)}")
raise e

cicd/notifications.tfを作成し、以下のようにLambda関数を定義します。
cicd/notifications.tf

data "archive_file" "lambda_zip" {
type = "zip"
source_file = "${path.module}/lambda/message_formatting.py"
output_path = "${path.module}/lambda/lambda_function.zip"
}

resource "aws_lambda_function" "approval_notification" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
function_name = "${var.name}-approval-notification"
role = aws_iam_role.lambda_notification_role.arn
handler = "message_formatting.lambda_handler"
runtime = "python3.12"

environment {
variables = {
TARGET_SNS_ARN = aws_sns_topic.pipeline_notification.arn
SLACK_MENTION_IDS = join(",", var.slack_mention_ids)
}
}
}

メッセージに含めるメンション先ユーザーIDを変数として定義します。
cicd/variables.tf

variable "slack_mention_ids" {
type = list(string)
default = [
"UXXXXXXXXXX" # SlackのメンバーID(複数可)
]
}

EventBridgeの定義

承認ステージが開始されたタイミングでLambdaを起動するルールを追記します。
cicd/notifications.tf

resource "aws_cloudwatch_event_rule" "approval_needed" {
name = "${var.name}-approval-needed-rule"
description = "Trigger Lambda when CodePipeline reaches Approval stage"

event_pattern = jsonencode({
source = ["aws.codepipeline"]
detail-type = ["CodePipeline Action Execution State Change"]
detail = {
pipeline = ["${var.name}-pipeline"] # パイプライン名を指定
stage = ["Approval"]
state = ["STARTED"]
}
})
}

resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.approval_needed.name
target_id = "SendToLambda"
arn = aws_lambda_function.approval_notification.arn
}

resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.approval_notification.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.approval_needed.arn
}

SNS、Chatbotの定義

最後に、Slackへのメッセージ送信を行うSNSとChatbotを定義します。
cicd/notifications.tf

resource "aws_sns_topic" "pipeline_notification" {
name = "${var.name}-pipeline-notification"
}

resource "aws_sns_topic_policy" "pipeline_notification_sns_policy" {
arn = aws_sns_topic.pipeline_notification.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowPublishFromLambda"
Effect = "Allow"
Principal = {
AWS = aws_iam_role.lambda_notification_role.arn
}
Action = "SNS:Publish"
Resource = aws_sns_topic.pipeline_notification.arn
},
{
Sid = "AllowChatbotToSubscribe"
Effect = "Allow"
Principal = {
Service = "chatbot.amazonaws.com"
}
Action = [
"SNS:Subscribe",
"SNS:Receive",
"SNS:GetTopicAttributes"
]
Resource = aws_sns_topic.pipeline_notification.arn
}
]
})
}

resource "aws_chatbot_slack_channel_configuration" "slack" {
configuration_name = "${var.name}-slack-config"
iam_role_arn = aws_iam_role.chatbot_role.arn
slack_channel_id = var.slack_channel_id # 通知先のSlackチャンネルID
slack_team_id = var.slack_team_id # 通知先のSlackワークスペースID
sns_topic_arns = [aws_sns_topic.pipeline_notification.arn]
}

cicd/variables.tf

variable "slack_channel_id" {
type = string
default = "CXXXXXXXXXX" # 通知先のSlackチャンネルID
}

variable "slack_team_id" {
type = string
default = "TXXXXXXXXXX" # 通知先のSlackワークスペースID
}

ここまででコードの実装は完了となります。
最後に、以下のコマンドで通知基盤をデプロイします。

Terraformを実行する前に(または実行後に)、AWSコンソールの AWS Chatbot (Amazon Q Developer) 画面から、通知先の Slack ワークスペースを認証(クライアント設定) しておく必要があります。

cd ~/terraform-cicd/cicd
terraform plan
terraform apply

動作確認

通知基盤の実装は完了したため、app_infra/のコードを変更してPushしてみましょう。
ここまで正しく設定されていれば、設定したSlackチャンネルにメンション付きでメッセージが送信されるはずです。

7. おわりに

この記事では、Terraformを用いたCI/CDパイプラインの構築方法をご紹介しました。
実際に構築してみると、開発者が複数人いる場合や大規模な構築において、CodeCommitでのバージョン管理や管理者によるレビューといったルールに沿ってCI/CDを実行できる点で優れていると感じました。
今回の構築経験を活かして、今後の業務でも活用していきたいと思います。