前回はAWS Configによる「非準拠の検知と自動修復」を扱いました。今回は視点をパイプライン側に移し、CodePipelineを複数のAWSアカウントにまたがって設計する際の考え方を整理します。dev/stg/prodを分離する組織であれば、ほぼ必ず向き合うことになるテーマです。
なぜ本番アカウントにパイプラインを置かないのか
まず前提として、多くの組織で「パイプラインは本番アカウントに置かない」という設計が採用されます。理由は3つあります。
1. 攻撃対象領域(Attack Surface)の最小化
CodePipeline/CodeBuildはGitHubと接続したり、外部からトリガーされたりする「入口」になります。ここが本番アカウント内にあると、CI/CD周りの脆弱性がそのまま本番リソースへの侵入経路になり得ます。パイプライン専用の「Toolsアカウント」に隔離しておけば、たとえパイプライン側が侵害されても、本番への影響はAssumeRoleで許可した権限の範囲に限定されます。
2. 権限の一元管理
本番・検証・開発それぞれのアカウントに個別パイプラインを作ると、IAMポリシーやKMSキー、S3バケットポリシーがアカウント数だけ分散し、監査が困難になります。Toolsアカウントに集約すれば「誰が・いつ・何をデプロイしたか」を一箇所のCloudTrailで追えます。
3. 最小権限の原則
本番アカウント内にパイプラインがあると、パイプライン自体が本番のIAMロール・リソースに直接触れる強い権限を持ちがちです。Toolsアカウント + AssumeRoleにすることで、「本番アカウント側が許可した操作だけ」に権限を絞り込めます。
[Tools Account] [Dev Account] [Prod Account]
- CodePipeline
- CodeBuild
- S3(アーティファクト)
- KMS(暗号化キー)
│ │ │
└─ AssumeRole ─────────────────┘ │
└─ AssumeRole ───────────────────────────────────┘
Toolsアカウントは兼任してもよい
「全部のアカウントを分離するのが理想」と分かっていても、アカウント数が増えるほどOrganizations・SCP・IAM管理・請求の運用コストは増えます。実務では、優先順位をつけて分離範囲を決めるのが現実的です。
1. Prod ← 必ず分離(最重要。攻撃対象領域・誤操作リスクが最も高い)
2. Stg ← できれば分離(本番相当のテストなので影響が大きい)
3. Tools ← Devと兼任可(実験環境なので許容されやすい)
Devは実験的な環境なので、パイプラインと同居しても実害が小さい一方、本番は「事故った時の被害が桁違い」なので、そこだけは分離する価値が高い、という判断です。AWS Control Towerの標準的なOU構成でも、「Security」「Log Archive」は必ず独立させる一方、実験用のOUの中でDevとTools系アカウントを緩く運用する構成はよく見られます。
「Tools = Dev兼用にして、Prodだけを新規アカウントに切り出す」という移行が、コストと安全性のバランスが取れた現実的な落としどころになりやすいです。
クロスアカウントパイプラインの3要素
パイプラインアカウント(Tools)と、実際にデプロイする先のアカウント(ターゲット)が異なる構成では、以下の3つの設定が必須になります。
[Tools Account] [Prod Account]
CodePipeline
├─ Source(GitHub)
├─ Build(CodeBuild)
└─ Deploy → AssumeRole ────→ デプロイ実行
1. ターゲットアカウント側のクロスアカウントIAMロール
デプロイ先アカウントに、パイプラインアカウントを信頼するロールを作成します。
resource "aws_iam_role" "codepipeline_cross_account" {
name = "prod-codepipeline-deploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${var.tools_account_id}:root" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "sts:ExternalId" = var.external_id } # なりすまし対策
}
}]
})
}
2. アーティファクトストア(S3)のクロスアカウントアクセス許可
パイプラインアカウントのS3バケットに、ターゲットアカウントからの読み取りを許可します。
resource "aws_s3_bucket_policy" "artifact_store" {
bucket = aws_s3_bucket.artifact_store.id
policy = jsonencode({
Statement = [{
Effect = "Allow"
Principal = { AWS = aws_iam_role.codepipeline_cross_account.arn }
Action = ["s3:GetObject", "s3:GetObjectVersion"]
Resource = "${aws_s3_bucket.artifact_store.arn}/*"
}]
})
}
3. KMSキーのクロスアカウント許可(最重要ポイント)
アーティファクトはS3上でKMS暗号化されているため、ターゲットアカウントがそのKMSキーを使ってデータを復号できる権限が必須です。ここを忘れるとAccessDeniedで詰まる、というのが実務で一番よくあるハマりポイントです。
resource "aws_kms_key" "artifact" {
policy = jsonencode({
Statement = [
{
Effect = "Allow"
Principal = { AWS = aws_iam_role.codepipeline_cross_account.arn }
Action = ["kms:Decrypt", "kms:DescribeKey", "kms:GenerateDataKey"]
Resource = "*"
}
]
})
}
3つのうちどれか1つでも欠けると、デプロイステージがそこで失敗します。「IAMロール」「S3バケットポリシー」「KMSキーポリシー」の3点セットは必ず揃える、と覚えておくと構築時に漏れが出にくくなります。
dev/stg/prodの環境判別
複数環境にデプロイするパイプラインを設計する際、「今どの環境向けにデプロイしているか」をどう判別するかは、パイプラインを1本にするか複数に分けるかで考え方が変わります。
パターン1: 1本のパイプライン内でステージごとに判別
Stageの並びで環境を表現し、判別ロジックをコード内に書くのではなく、「ステージが違う=role_arnが違う=デプロイ先が違う」という構造そのもので判別します。
stage {
name = "DeployDev"
action {
role_arn = aws_iam_role.dev_deploy.arn
configuration = { ClusterName = "app-dev-cluster" }
}
}
stage {
name = "ApprovalForProd"
action { category = "Approval"; provider = "Manual" }
}
stage {
name = "DeployProd"
action {
role_arn = aws_iam_role.prod_deploy.arn
configuration = { ClusterName = "app-prod-cluster" }
}
}
Prodへ進む前にManual Approvalを挟むことで、意図しない本番反映も防げます。
パターン2: 複数パイプラインに分けて、ブランチで判別
パイプラインを完全に分ける場合、「振り分け」という概念自体がなくなります。各パイプラインが最初から特定のブランチに固定で紐付いており、判定ロジックはSourceステージのブランチ指定そのものに存在します。
resource "aws_codepipeline" "dev" {
name = "app-dev-pipeline"
stage {
name = "Source"
action {
configuration = {
ConnectionArn = aws_codestarconnections_connection.github.arn
FullRepositoryId = "org/app"
BranchName = "develop"
}
}
}
}
resource "aws_codepipeline" "prod" {
name = "app-prod-pipeline"
stage {
name = "Source"
action {
configuration = {
BranchName = "main"
}
}
}
}
CodeStar Connections経由のGitHub連携は、裏側でEventBridgeルールを自動生成しており、「指定ブランチへのpush」イベントが発生した時だけ、そのパイプラインが起動します。つまり判定はGitHub側のブランチ運用ルールに委ねられており、AWS側では「そのブランチのイベントを拾うパイプラインが1つに決まっている」だけです。
2つの構造の違いまとめ
| 構成 | 判定が行われる場所 |
|---|---|
| 1本のパイプライン(Stageで分離) | パイプライン内部のrole_arnの違い(構造そのもの) |
| 複数パイプラインに分離 | トリガー元(ブランチ名 / タグ名 / EventBridgeパターン) |
一つ注意したいのは、「パイプライン全体の再実行」をしても、ブランチが動的に変わることはないという点です。Release change(start-pipeline-execution)は、そのパイプラインのSourceアクションに設定されたブランチを毎回取得するだけで、常に同じブランチの最新コミットを取ってきます。「最初からやり直す」というのはSourceステージからやり直すという意味であり、ブランチ自体はパイプライン定義に固定された設定値です。
「1回ビルド・複数環境デプロイ」の原則
環境ごとの判別方法とあわせて重要なのが、ビルド成果物は環境ごとに作り直さないという原則です。
Dev/Stg/Prodでそれぞれ再ビルドすると、「Devでは動いたがProdでは動かない」というビルド差異のリスクが生まれます。実務では、1回ビルドしたコンテナイメージ(ECRのタグ)を、環境ごとにデプロイ先を変えるだけで使い回すのが鉄則です。環境ごとの差分は、ビルド成果物ではなく環境変数・Secrets Manager・Parameter Store側で吸収します。
GitHub main ブランチへのマージ
↓
CodePipeline(Toolsアカウント)
↓
Build(共通のイメージをビルド、タグにcommit hashを付与)
↓
DeployDev (AssumeRole: Devアカウント)
↓
[Manual Approval]
↓
DeployStg (AssumeRole: Stgアカウント)
↓
[Manual Approval]
↓
DeployProd (AssumeRole: Prodアカウント)
まとめ
CodePipelineのマルチアカウント設計における要点は次の3つです。
- パイプラインはToolsアカウントに集約し、本番アカウントには置かない。攻撃対象領域の最小化と権限の一元管理が理由。Devとの兼任は許容範囲、Prodの分離は必須
- クロスアカウントデプロイには「IAMロール・S3バケットポリシー・KMSキーポリシー」の3点セットが必要。どれか1つでも欠けるとデプロイが失敗する
- 環境判別は「パイプライン構造(role_arn)」か「トリガー元のブランチ」のどちらかに委ねる。判別ロジックを自前で書く必要はない