この記事では、AWSにおけるTerraform用CI/CD構築をTerraformによって構築する方法についてご紹介します。
実際の構築手順ベースの記事となっていますので、CI/CD構築未経験の方やTerraform初心者の方がハンズオンする際の参考になれば幸いです。
目次
- 前提条件と開発環境
- システムアーキテクチャ
- 構築①:tfstateとロック制御管理リソースの作成
- 構築②:AWS CodeCommit(SSH接続設定とリポジトリ作成)
- 構築③:CI/CDパイプラインと承認フローの統合(CodeBuild / CodePipeline)
- 構築④:メンション付きSlack通知の実装(EventBridge / Lambda / Amazon Q Developer)
- おわりに
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)
構成の概要
- EventBridge: パイプラインが「承認待ち」になったイベントを検知。
- Lambda: イベント情報をメンションを含めた形に整形。
- SNS: 整形されたメッセージをChatbotへ中継。
- 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を実行できる点で優れていると感じました。
今回の構築経験を活かして、今後の業務でも活用していきたいと思います。
