この記事では、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を実行できる点で優れていると感じました。
今回の構築経験を活かして、今後の業務でも活用していきたいと思います。