はじめに
CI/CDパイプラインは素晴らしい。
AWSのCode兄弟を使ったパイプラインは、機能をAWSに閉じ込めながらなんでもできる万能感を感じる。
しかし、チュートリアルに載っているようなシンプルなパイプラインだけでは、現実にある課題を解決することはできない。
金融サービス向けに理想のCI/CDを追い求めたお話は、そんな現実にある課題もひっくるめてある程度解決してくれるベストプラクティスだと考えている。
が、実際にこれを運用しようとすると、クロスアカウントで色々なことをしなければならず、シンプルなパイプラインと比べると難易度が高くなる。
この記事では、このパイプラインを実現するためのクロスアカウント設定を中心にポイントを整理しながらTerraformで実装してみる。
前提知識として、以下が必要である(この時点でちょっとハードルが高い)。
- (実際に試してみるなら)クロスアカウントな環境
- IAM関連の知識(Assune Roleをちゃんと理解している必要あり)
- Terraformをある程度書いたことがある(記事中の構成は全然美しくないので、必要に応じてリファクタしてもらいたい……)
- EventBridgeの概念を理解している
動かしてみる前に
確認のためにアカウントを行き来するので、アカウント跨ぎのスイッチロールを入れておいた方が良い。
また、Terraformも複数アカウントの処理が必要になるので、CLIの設定をしておく。
AWS CLIで複数アカウントのアクセスキーを管理して扱う設定
構成図
上記を構成するためのTerrafromは以下の通り。
.
├── 00_main.tf
├── 01_variables.tf
├── 02_data_sources.tf
├── 11_iam_role_build_account.tf
├── 12_iam_role_service_account.tf
├── 13_iam_role_cross_account.tf
├── 21_s3_cross_account.tf
├── 22_kms_cross_account.tf
├── 23_ecr_cross_account.tf
├── 31_s3_build_account.tf
├── 32_codebuild_build_account.tf
├── 33_codepipeline_build_account.tf
├── 34_codebuild_service_account.tf
├── 35_codepipeline_service_account.tf
├── 41_cloudwatch_event_build_account.tf
├── 42_event_pattern_build_account.json
├── 43_cloudwatch_event_service_account.tf
└── 44_event_pattern_service_account.json
うーん、複雑!これはなかなか大変なので、細かく刻んで確認をしていこう。
もうちょっとイケてる分類がある気がするけど、ひとまず今回はこれで。
プロバイダの設定
main.tfにクロスアカウントのプロバイダ設定を入れておく。なお、この記事中では、以下のように環境名を定義している。
- build_account: Mavenでjavaのビルドする環境
- service_account: 実際にこの後にECSなりにデプロイする環境
provider "aws" {
alias = "build_account"
shared_credentials_file = "~/.aws/credentials"
profile = "build_account"
region = "ap-northeast-1"
}
provider "aws" {
alias = "service_account"
shared_credentials_file = "~/.aws/credentials"
profile = "service_account"
region = "ap-northeast-1"
}
また、それぞれのアカウントIDを参照したりするために、以下の設定を入れておこう。
################################################################################
# Account Identity #
################################################################################
data "aws_caller_identity" "build_account" {
provider = aws.build_account
}
data "aws_caller_identity" "service_account" {
provider = aws.service_account
}
これは、${data.aws_caller_identity.service_account.account_id}
とすることで、アカウントIDを拾える。
CodeCommitのクロスアカウント設定
概要
まずはCodeCommitのクロスアカウント設定だ。
基本的にDevelopers.IOの以下の記事が分かりやすいのでこれを参考にする。
【Developers.IO】CodePipelineでアカウントをまたいだパイプラインを作成してみる
ぶっちゃけ言えばこの記事でほぼ完成なのだが、趣旨はCodeCommitから先まで考えてIaC化することなので、一旦は気にしないことにしよう。
記事内に書いてあるように、以下の条件を満たす必要がある。
CodePipeline/CodeBuildはS3に暗号化されたファイルを置くことでアーティファクトをやり取りしています。 そのため、こういったアカウントをまたいだパイプラインを構築するには以下のような設定がされている必要があります。
- CodePipeline/CodeBuildで暗号化キーが指定されていること
- CodePipelineで開発環境のCodeCommitにアクセスするアクションについて、開発アカウント側のCodeCommitアクセス用IAMロールが指定されていること。
- 開発アカウント側のCodeCommitアクセス用IAMロールでCodeCommitのリポジトリにアクセスできること
- 本番環境アカウントのCodePipelineのサービスロールからリソース側のアカウントのCodeCommitアクセス用ロールにAssumeRoleできること
- アーティファクト用S3バケットおよびそのオブジェクトの暗号化に使うKMSの暗号化キーに対して適切なロールにアクセス権が与えられていること
- CodePipelineのサービスロールからアクセスできること
- CodeBuildのサービスロールからアクセスできること
- CodeCommitアクセス用ロールからアクセスできること
つまり、CodePipeline/CodeBuildで暗号化キーを指定した上で、以下の図のようなアクセス権限を持つようにIAMロールおよびS3バケット、KMS暗号化キーを設定していく必要があります
service_accountにCodeCommitがアーティファクトを格納するためのIAMロール
クロスアカウントで必要になるIAMロールを作成しておく。
リソース定義ではprovider
で、どちらのアカウントに作成するかを明示しておこう。
デフォルトもあるが、書いておかないとたぶん後で読んだときにワケがわからなくなると思う。
設定の詳細な意味は、↑のDevelopers.IOの記事と重複するので割愛する。
################################################################################
# IAM Role for CodeCommit(Cross Account) #
################################################################################
resource "aws_iam_role" "codecommit_cross_account" {
provider = aws.build_account
name = "${local.codecommit_ca_iam_role_name}"
assume_role_policy = "${data.aws_iam_policy_document.codecommit_cross_account_trust.json}"
}
data "aws_iam_policy_document" "codecommit_cross_account_trust" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.service_account.account_id}:root",
]
}
}
}
resource "aws_iam_policy" "codecommit_cross_account" {
provider = aws.build_account
name = "codecommit-cross-account-policy"
description = "CodeCommit Cross Account Policy"
policy = "${data.aws_iam_policy_document.codecommit_cross_account.json}"
}
data "aws_iam_policy_document" "codecommit_cross_account" {
version = "2012-10-17"
statement {
sid = "UploadArtifactPolicy"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:PutObjectAcl",
]
resources = [
"${aws_s3_bucket.service_artifact.arn}/*",
]
}
statement {
sid = "KMSAccessPolicy"
effect = "Allow"
actions = [
"kms:DescribeKey",
"kms:GenerateDataKey*",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:Decrypt",
]
resources = [
"${aws_kms_key.cross_account.arn}",
]
}
statement {
sid = "CodeCommitAccessPolicy"
effect = "Allow"
actions = [
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:UploadArchive",
"codecommit:GetUploadArchiveStatus",
"codecommit:CancelUploadArchive",
]
resources = [
"${data.aws_codecommit_repository.application.arn}",
]
}
}
resource "aws_iam_role_policy_attachment" "codecommit_cross_account" {
provider = aws.build_account
role = "${aws_iam_role.codecommit_cross_account.name}"
policy_arn = "${aws_iam_policy.codecommit_cross_account.arn}"
}
ソースアーティファクトバケットの暗号化
クロスアカウントでCodeCommitのソースアーティファクトバケットを暗号化するためのKMSを定義する。
data.aws_iam_user.kms_key_manager.arn
は、暗号化の管理IAMユーザをあらかじめ決めておき、
データソースで参照しておく。
また、aws_iam_role.codebuild_service_account.arn
とaws_iam_role.codepipeline_service_account.arn
は後でパイプラインを作るところでリソースを定義している。
################################################################################
# KMS #
################################################################################
resource "aws_kms_key" "cross_account" {
provider = aws.service_account
description = "KMS Key for CodePipeline"
policy = "${data.aws_iam_policy_document.kms_cross_account.json}"
}
resource "aws_kms_alias" "cross_account" {
provider = aws.service_account
name = "alias/${local.key_alias_name}"
target_key_id = "${aws_kms_key.cross_account.key_id}"
}
data "aws_iam_policy_document" "kms_cross_account" {
version = "2012-10-17"
statement {
sid = "Enable IAM User Permissions"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.service_account.account_id}:root",
]
}
actions = [
"kms:*",
]
resources = [
"*",
]
}
statement {
sid = "Allow access for Key Administrators"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"${data.aws_iam_user.kms_key_manager.arn}",
]
}
actions = [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion",
]
resources = [
"*",
]
}
statement {
sid = "Allow use of the key"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"${aws_iam_role.codebuild_service_account.arn}",
"${aws_iam_role.codepipeline_service_account.arn}",
"arn:aws:iam::${data.aws_caller_identity.build_account.account_id}:root"
]
}
actions = [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
resources = [
"*",
]
}
statement {
sid = "Allow attachment of persistent resources"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"${aws_iam_role.codebuild_service_account.arn}",
"${aws_iam_role.codepipeline_service_account.arn}",
"arn:aws:iam::${data.aws_caller_identity.build_account.account_id}:root"
]
}
actions = [
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant"
]
resources = [
"*",
]
condition {
test = "Bool"
variable = "kms:GrantIsForAWSResource"
values = [
"true",
]
}
}
}
ソースアーティファクトバケットの定義
アーティファクトバケットもbuild_account側からアクセスできるように設定をする。
################################################################################
# S3 #
################################################################################
resource "aws_s3_bucket" "service_artifact" {
provider = aws.service_account
bucket = "${local.service_artifact_s3bucket_name}"
}
resource "aws_s3_bucket_policy" "cross_account" {
provider = aws.service_account
bucket = "${aws_s3_bucket.service_artifact.id}"
policy = "${data.aws_iam_policy_document.s3_cross_account.json}"
}
data "aws_iam_policy_document" "s3_cross_account" {
version = "2012-10-17"
statement {
sid = "DenyUnEncryptedObjectUploads"
effect = "Deny"
principals {
type = "AWS"
identifiers = [
"*",
]
}
actions = [
"s3:PutObject",
]
resources = [
"arn:aws:s3:::${aws_s3_bucket.service_artifact.id}/*"
]
condition {
test = "StringNotEquals"
variable = "s3:x-amz-server-side-encryption"
values = [
"aws:kms",
]
}
}
statement {
sid = "DenyInsecureConnections"
effect = "Deny"
principals {
type = "AWS"
identifiers = [
"*",
]
}
actions = [
"s3:*",
]
resources = [
"arn:aws:s3:::${aws_s3_bucket.service_artifact.id}/*"
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = [
"false",
]
}
}
statement {
sid = "CrossAccountS3GetPutPolicy"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.build_account.account_id}:root",
]
}
actions = [
"s3:Get*",
"s3:Put*",
]
resources = [
"arn:aws:s3:::${aws_s3_bucket.service_artifact.id}/*"
]
}
statement {
sid = "CrossAccountS3ListPolicy"
effect = "Allow"
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.build_account.account_id}:root",
]
}
actions = [
"s3:ListBucket",
]
resources = [
"arn:aws:s3:::${aws_s3_bucket.service_artifact.id}"
]
}
}
ECRのクロスアカウント設定
他と比べると随分とシンプルだが、service_accountからbuild_accountのECRにアクセスしてpullしていくので、そのための設定を行う必要がある。
################################################################################
# ECR #
################################################################################
resource "aws_ecr_repository_policy" "cross_account" {
provider = aws.build_account
repository = "${data.aws_ecr_repository.build_account.name}"
policy = "${data.aws_iam_policy_document.ecr_cross_account.json}"
}
data "aws_iam_policy_document" "ecr_cross_account" {
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
]
principals {
type = "AWS"
identifiers = [
"arn:aws:iam::${data.aws_caller_identity.service_account.account_id}:root",
]
}
}
}
パイプラインの作成
build_account側パイプライン
ここはそんなに凝ったことはやっていない。
S3バケット
build_account側のアーティファクト用のS3バケットを作っておく。
これは普通のパイプラインに必要なリソースなので、特別なことは特にない。
################################################################################
# S3 #
################################################################################
resource "aws_s3_bucket" "build_artifact" {
provider = aws.build_account
bucket = "${local.build_artifact_s3bucket_name}"
}
IAMロール
IAMロールも普通にパイプラインに必要なポリシをアタッチしたサービスロールを作れば良い。
ちょっと面倒になってFullAccessとかの管理ポリシを使ってるのはご容赦いただきたい……。
################################################################################
# IAM Role for CodeBuild #
################################################################################
resource "aws_iam_role" "codebuild_build_account" {
provider = aws.build_account
name = "${local.codebuild_iam_role_name}"
assume_role_policy = "${data.aws_iam_policy_document.codebuild_build_account_trust.json}"
}
data "aws_iam_policy_document" "codebuild_build_account_trust" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"codebuild.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "codebuild_build_account1" {
provider = aws.build_account
role = "${aws_iam_role.codebuild_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess"
}
resource "aws_iam_role_policy_attachment" "codebuild_build_account2" {
provider = aws.build_account
role = "${aws_iam_role.codebuild_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}
resource "aws_iam_role_policy_attachment" "codebuild_build_account3" {
provider = aws.build_account
role = "${aws_iam_role.codebuild_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
resource "aws_iam_role_policy_attachment" "codebuild_build_account4" {
provider = aws.build_account
role = "${aws_iam_role.codebuild_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
}
################################################################################
# IAM Role for CodePipeline #
################################################################################
resource "aws_iam_role" "codepipeline_build_account" {
provider = aws.build_account
name = "${local.codepipeline_iam_role_name}"
assume_role_policy = "${data.aws_iam_policy_document.codepipeline_build_account_trust.json}"
}
data "aws_iam_policy_document" "codepipeline_build_account_trust" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"codepipeline.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "codepipeline_build_account1" {
provider = aws.build_account
role = "${aws_iam_role.codepipeline_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AWSCodePipelineFullAccess"
}
resource "aws_iam_role_policy_attachment" "codepipeline_build_account2" {
provider = aws.build_account
role = "${aws_iam_role.codepipeline_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AWSCodeCommitFullAccess"
}
resource "aws_iam_role_policy_attachment" "codepipeline_build_account3" {
provider = aws.build_account
role = "${aws_iam_role.codepipeline_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
resource "aws_iam_role_policy_attachment" "codepipeline_build_account4" {
provider = aws.build_account
role = "${aws_iam_role.codepipeline_build_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess"
}
CodeBuild
################################################################################
# CodeBuild for Build Account #
################################################################################
resource "aws_codebuild_project" "build_account" {
provider = aws.build_account
name = "${local.build_project_name}"
service_role = "${aws_iam_role.codebuild_build_account.arn}"
source {
type = "CODEPIPELINE"
buildspec = "buildspec_build_account.yml"
}
artifacts {
type = "CODEPIPELINE"
}
environment {
type = "LINUX_CONTAINER"
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/standard:3.0-19.11.26"
privileged_mode = "true"
}
cache {
type = "LOCAL"
modes = [
"LOCAL_CUSTOM_CACHE",
]
}
}
CodePipeline
################################################################################
# CodePipeline for Build Account #
################################################################################
resource "aws_codepipeline" "build_account" {
provider = aws.build_account
name = "${local.codepipeline_name}"
role_arn = "${aws_iam_role.codepipeline_build_account.arn}"
artifact_store {
type = "S3"
location = "${aws_s3_bucket.build_artifact.bucket}"
}
stage {
name = "Source"
action {
run_order = 1
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["SourceArtifact"]
configuration = {
RepositoryName = "${local.repository_name}"
BranchName = "master"
}
}
}
stage {
name = "Build"
action {
run_order = 2
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["SourceArtifact"]
output_artifacts = ["BuildArtifact"]
configuration = {
ProjectName = "${aws_codebuild_project.build_account.name}"
}
}
}
}
service_account側パイプライン
ここも、build_account同様、特別なことはしていない。
IAMロール
################################################################################
# IAM Role for CodeBuild #
################################################################################
resource "aws_iam_role" "codebuild_service_account" {
provider = aws.service_account
name = "${local.codebuild_iam_role_name}"
assume_role_policy = "${data.aws_iam_policy_document.codebuild_service_account_trust.json}"
}
data "aws_iam_policy_document" "codebuild_service_account_trust" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"codebuild.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "codebuild_service_account1" {
provider = aws.service_account
role = "${aws_iam_role.codebuild_service_account.name}"
policy_arn = "${aws_iam_policy.codebuild_service_account_custom.arn}"
}
resource "aws_iam_policy" "codebuild_service_account_custom" {
provider = aws.service_account
name = "code-build-policy"
description = "CodeBuild Policy"
policy = "${data.aws_iam_policy_document.codebuild_service_account_custom.json}"
}
data "aws_iam_policy_document" "codebuild_service_account_custom" {
version = "2012-10-17"
statement {
sid = "CloudWatchLogsPolicy"
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
"*",
]
}
statement {
sid = "S3ObjectPolicy"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
]
resources = [
"*",
]
}
}
resource "aws_iam_role_policy_attachment" "codebuild_service_account2" {
provider = aws.service_account
role = "${aws_iam_role.codebuild_service_account.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}
################################################################################
# IAM Role for CodePipeline #
################################################################################
resource "aws_iam_role" "codepipeline_service_account" {
provider = aws.service_account
name = "${local.codepipeline_iam_role_name}"
assume_role_policy = "${data.aws_iam_policy_document.codepipeline_service_account_trust.json}"
}
data "aws_iam_policy_document" "codepipeline_service_account_trust" {
statement {
effect = "Allow"
actions = [
"sts:AssumeRole",
]
principals {
type = "Service"
identifiers = [
"codepipeline.amazonaws.com",
]
}
}
}
resource "aws_iam_role_policy_attachment" "codepipeline_service_account" {
provider = aws.service_account
role = "${aws_iam_role.codepipeline_service_account.name}"
policy_arn = "${aws_iam_policy.codepipeline_service_account_custom.arn}"
}
resource "aws_iam_policy" "codepipeline_service_account_custom" {
provider = aws.service_account
name = "code-pipeline-policy"
description = "CodePipeline Policy"
policy = "${data.aws_iam_policy_document.codepipeline_service_account_custom.json}"
}
data "aws_iam_policy_document" "codepipeline_service_account_custom" {
version = "2012-10-17"
statement {
sid = "AssumeRolePolicy"
effect = "Allow"
actions = [
"sts:AssumeRole",
]
resources = [
"arn:aws:iam::${data.aws_caller_identity.build_account.account_id}:role/*",
]
}
statement {
sid = "S3Policy"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetBucketVersioning",
]
resources = [
"*",
]
}
statement {
sid = "CodeBuildPolicy"
effect = "Allow"
actions = [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
]
resources = [
"*",
]
}
}
CodeBuild
################################################################################
# CodeBuild for Service Account #
################################################################################
resource "aws_codebuild_project" "service_account" {
provider = aws.service_account
name = "${local.build_project_name}"
service_role = "${aws_iam_role.codebuild_service_account.arn}"
encryption_key = aws_kms_key.cross_account.arn
source {
type = "CODEPIPELINE"
buildspec = "buildspec_service_account.yml"
}
artifacts {
type = "CODEPIPELINE"
}
environment {
type = "LINUX_CONTAINER"
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/standard:3.0-19.11.26"
privileged_mode = "true"
environment_variable {
name = "BUILD_ACCOUNT_ID"
value = "${data.aws_caller_identity.build_account.account_id}"
}
}
cache {
type = "LOCAL"
modes = [
"LOCAL_CUSTOM_CACHE",
]
}
CodePipeline
################################################################################
# CodePipeline for Service Account #
################################################################################
resource "aws_codepipeline" "service_account" {
provider = aws.service_account
name = "${local.codepipeline_name}"
role_arn = "${aws_iam_role.codepipeline_service_account.arn}"
artifact_store {
type = "S3"
location = "${aws_s3_bucket.service_artifact.bucket}"
encryption_key {
id = "${aws_kms_key.cross_account.arn}"
type = "KMS"
}
}
stage {
name = "Source"
action {
run_order = 1
name = "Source"
category = "Source"
owner = "AWS"
provider = "CodeCommit"
version = "1"
output_artifacts = ["SourceArtifact"]
configuration = {
RepositoryName = "${local.repository_name}"
BranchName = "master"
PollForSourceChanges = "false"
}
role_arn = "${aws_iam_role.codecommit_cross_account.arn}"
}
}
stage {
name = "Build"
action {
run_order = 2
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["SourceArtifact"]
output_artifacts = ["BuildArtifact"]
configuration = {
ProjectName = "${aws_codebuild_project.service_account.name}"
}
}
}
}
EventBridgeでアカウント間を繋ぐ
さて、ここまでできたら、あとはパイプラインを繋ぐだけだ。
EventBridgeでイベント送信するには、
- 送信(build_account)側で、受信(service_account)側へのデフォルトイベントバスにイベントを書き込むための権限
- 受信(service_account)側のデフォルトイベントバスに送信(build_account)側のアクセス許可を追加
が必要である。どちらも、1つだけ準備しておけば充分なので、IaC化はしない。
送信(build_account)側のIAMロール設定
以下のポリシを持ったIAMロールを作っておく
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"events:PutEvents"
],
"Resource": [
"arn:aws:events:ap-northeast-1:受信(service_account)側のアカウントID:event-bus/default"
]
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
受信(service_account)側のデフォルトイベントバスのアクセス許可
サービスの「EventBridge」のトップ画面で以下のボタンを押下する。
以下の画面が開くので、送信(build_account)側のアカウントIDを登録する。
送信(build_account)側のイベント送信トリガの設定
送信(build_account)側でパイプラインの完了を検知してイベント送信するCloudWatch Eventのルールを作る。
"detail-type": [
"CodePipeline Pipeline Execution State Change"
],
がパイプラインの更新をひっかけるイベント定義で、
"detail": {
"state": [
"SUCCEEDED"
]
},
が、パイプラインの成功をフィルタするための定義だ。
これで、パイプライン成功時にlocal.service_event_bus_arn
にイベント送信するという定義が出来上がる。
################################################################################
# CloudWatch Event for Build Account #
################################################################################
resource "aws_cloudwatch_event_rule" "codepipeline_success_build_account" {
provider = aws.build_account
name = "${local.event_rule_name}"
description = "${var.prefix} CodePipeline Success Event Rule"
event_pattern = "${data.template_file.event_pattern_build_account.rendered}"
}
resource "aws_cloudwatch_event_target" "event_bus_build_account" {
provider = aws.build_account
rule = "${aws_cloudwatch_event_rule.codepipeline_success_build_account.name}"
target_id = "${local.event_target_id}"
arn = "${local.service_event_bus_arn}"
role_arn = "${data.aws_iam_role.send_event_to_service_account.arn}"
}
data "template_file" "event_pattern_build_account" {
template = file("${path.module}/42_event_pattern_build_account.json")
vars = {
codepipeline_arn = "${aws_codepipeline.build_account.arn}"
}
}
{
"source": [
"aws.codepipeline"
],
"detail-type": [
"CodePipeline Pipeline Execution State Change"
],
"detail": {
"state": [
"SUCCEEDED"
]
},
"resources": [
"${codepipeline_arn}"
]
}
受信(service_account)側のイベント処理
送信側の設定に似ているが、送信側から送られてきたイベントをさらに処理して、aws_codepipeline.service_account.arn
をキックするようにしている。
自分のパイプライン成功をフックしてしまわないように、イベントのルールで
"account": [
"${build_account_id}"
],
として、以下のようにしてbuild_accountのアカウントIDを渡してフィルタしている。
vars = {
build_account_id = "${data.aws_caller_identity.build_account.account_id}"
}
CloudWatch Eventとイベントフィルタ定義の全体像は以下のような感じになる。
################################################################################
# CloudWatch Event for Service Account #
################################################################################
resource "aws_cloudwatch_event_rule" "codepipeline_success_service_account" {
provider = aws.service_account
name = "${local.event_rule_name}"
description = "${var.prefix} CodePipeline Success Event Rule"
event_pattern = "${data.template_file.event_pattern_service_account.rendered}"
}
resource "aws_cloudwatch_event_target" "event_bus_service_account" {
provider = aws.service_account
rule = "${aws_cloudwatch_event_rule.codepipeline_success_service_account.name}"
target_id = "${local.event_target_id}"
arn = "${aws_codepipeline.service_account.arn}"
role_arn = "${data.aws_iam_role.start_pipeline_service_account.arn}"
}
data "template_file" "event_pattern_service_account" {
template = file("${path.module}/44_event_pattern_service_account.json")
vars = {
build_account_id = "${data.aws_caller_identity.build_account.account_id}"
codepipeline_arn = "${aws_codepipeline.build_account.arn}"
}
}
{
"source": [
"aws.codepipeline"
],
"account": [
"${build_account_id}"
],
"detail-type": [
"CodePipeline Pipeline Execution State Change"
],
"detail": {
"state": [
"SUCCEEDED"
]
},
"resources": [
"${codepipeline_arn}"
]
}
これで、2つのパイプラインをEventBridgeで接続して連動できるようになった!
その他
CloudWatch Eventのイベントトリガが正しく発行されたか確認するには
[CloudWatch Events から実行された SSM RunCommand や Lambda のログについて]
以下の記事が分かりやすくて良かった。
(https://qiita.com/kusokamayarou/items/6e1ee054ecae437dadaa)
ただし、実際にはCloudTrailを追いかけるのはかったるくてしょうがないので、結局は、以下の手順で確認するのが一番楽だった。
- 一番シンプルなイベントトリガを作り、Lambda関数をターゲットにする。
Lambda関数では、
import pprint
~中略~
pprint.pprint(event)
して、イベントトリガのJSONの内容をダンプする。万が一、トリガが正しく引かれていないなら、この時点でCloudWatch Logsに何も出ないので、そこでトリガが引かれない原因の切り分けができる。
2. ↑で表示した内容に合わせ、イベントパターンを編集する。だいたいイベントパターンのマッチングが間違ってるからトリガが発動しないのだよ……。