Help us understand the problem. What is going on with this article?

理想を追い求めたCI/CDパイプラインをTerraformで実装するためのポイント

はじめに

CI/CDパイプラインは素晴らしい。
AWSのCode兄弟を使ったパイプラインは、機能をAWSに閉じ込めながらなんでもできる万能感を感じる。
しかし、チュートリアルに載っているようなシンプルなパイプラインだけでは、現実にある課題を解決することはできない。

金融サービス向けに理想のCI/CDを追い求めたお話は、そんな現実にある課題もひっくるめてある程度解決してくれるベストプラクティスだと考えている。

が、実際にこれを運用しようとすると、クロスアカウントで色々なことをしなければならず、シンプルなパイプラインと比べると難易度が高くなる。

この記事では、このパイプラインを実現するためのクロスアカウント設定を中心にポイントを整理しながらTerraformで実装してみる。

前提知識として、以下が必要である(この時点でちょっとハードルが高い)。

  • (実際に試してみるなら)クロスアカウントな環境
  • IAM関連の知識(Assune Roleをちゃんと理解している必要あり)
  • Terraformをある程度書いたことがある(記事中の構成は全然美しくないので、必要に応じてリファクタしてもらいたい……)
  • EventBridgeの概念を理解している

動かしてみる前に

確認のためにアカウントを行き来するので、アカウント跨ぎのスイッチロールを入れておいた方が良い。

Swith Roleで複数のAWSアカウント間を切替える

また、Terraformも複数アカウントの処理が必要になるので、CLIの設定をしておく。

AWS CLIで複数アカウントのアクセスキーを管理して扱う設定

構成図

crossaccountpipeline.png

上記を構成するための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なりにデプロイする環境
00_main.tf
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を参照したりするために、以下の設定を入れておこう。

02_data_sources.tf
################################################################################
# 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の記事と重複するので割愛する。

13_iam_role_cross_account.tf
################################################################################
# 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" "codebuild1" {
  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.arnaws_iam_role.codepipeline_service_account.arnは後でパイプラインを作るところでリソースを定義している。

22_kms_cross_account.tf
################################################################################
# KMS                                                                          #
################################################################################
resource "aws_kms_key" "cross_account" {
  description = "KMS Key for CodePipeline"
  policy      = "${data.aws_iam_policy_document.kms_cross_account.json}"

}

resource "aws_kms_alias" "cross_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側からアクセスできるように設定をする。

21_s3_cross_account.tf
################################################################################
# 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していくので、そのための設定を行う必要がある。

23_ecr_cross_account.tf
################################################################################
# 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バケットを作っておく。
これは普通のパイプラインに必要なリソースなので、特別なことは特にない。

31_s3_build_account.tf
################################################################################
# S3                                                                           #
################################################################################
resource "aws_s3_bucket" "build_artifact" {
  provider = "aws.build_account"

  bucket = "${local.build_artifact_s3bucket_name}"
}

IAMロール

IAMロールも普通にパイプラインに必要なポリシをアタッチしたサービスロールを作れば良い。
ちょっと面倒になってFullAccessとかの管理ポリシを使ってるのはご容赦いただきたい……。

11_iam_role_build_account.tf
################################################################################
# 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

32_codebuild_build_account.tf
################################################################################
# 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

33_codepipeline_build_account.tf
################################################################################
# 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ロール

12_iam_role_service_account.tf
################################################################################
# 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

34_codebuild_service_account.tf
################################################################################
# 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}"

  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

35_codepipeline_service_account.tf
################################################################################
# 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」のトップ画面で以下のボタンを押下する。
キャプチャ.png

以下の画面が開くので、送信(build_account)側のアカウントIDを登録する。
キャプチャ2.png

送信(build_account)側のイベント送信トリガの設定

送信(build_account)側でパイプラインの完了を検知してイベント送信するCloudWatch Eventのルールを作る。

  "detail-type": [
    "CodePipeline Pipeline Execution State Change"
  ],

がパイプラインの更新をひっかけるイベント定義で、

  "detail": {
    "state": [
      "SUCCEEDED"
    ]
  },

が、パイプラインの成功をフィルタするための定義だ。

これで、パイプライン成功時にlocal.service_event_bus_arnにイベント送信するという定義が出来上がる。

41_cloudwatch_event_build_account.tf
################################################################################
# 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}"
  }
}
42_event_pattern_build_account.json
{
  "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とイベントフィルタ定義の全体像は以下のような感じになる。

43_cloudwatch_event_service_account.tf
################################################################################
# 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}"
  }
}
44_event_pattern_service_account.json
{
  "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を追いかけるのはかったるくてしょうがないので、結局は、以下の手順で確認するのが一番楽だった。

  1. 一番シンプルなイベントトリガを作り、Lambda関数をターゲットにする。 Lambda関数では、
import pprint
~中略~
  pprint.pprint(event)

して、イベントトリガのJSONの内容をダンプする。万が一、トリガが正しく引かれていないなら、この時点でCloudWatch Logsに何も出ないので、そこでトリガが引かれない原因の切り分けができる。
2. ↑で表示した内容に合わせ、イベントパターンを編集する。だいたいイベントパターンのマッチングが間違ってるからトリガが発動しないのだよ……。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした