CodeCommitのPRマージをCodeBuildの成功だけで承認させ、手動承認を防ぐ構成を構築した
はじめに
クラウド事業統括本部の浅野です。
AWS CodeCommitには承認ルール(Approval Rule)という、PR(プルリクエスト)のマージにレビュー承認を必須化する機能があります。この承認者を人ではなくCodeBuild(CI)にすると、「テストが成功したPRだけマージできる」という制御が、Lambdaなどの追加コンポーネントなしで組めます。
ただ承認ルールは「必要承認数」を条件にするだけなので、素朴に組むと人が手動でApproveしてもマージが通ってしまいます。今回は承認プールをCodeBuildのサービスロールに限定することで、人の承認では通らない構成を作り、さらに手動で強制的にマージできないような構成を構築してみました。
検証したいこと
- PRの作成・更新をトリガーにCodeBuildが自動起動し、テスト結果をPRコメントに投稿する
- CI(CodeBuild)の承認だけをマージ条件にする
- 権限を持つ人間がApproveしても迂回できないかを確認する
なお、CodeCommit + CodeBuild + EventBridgeでPRを自動チェックし、CodeBuildが承認する構成自体は、AWS公式ブログ Automated Code Review on Pull Requests using AWS CodeCommit and AWS CodeBuild でも紹介されています。ただし公式ブログでは承認プールをCodeBuildロールに限定していないため、そのままだと人が手動でApproveしてもマージが通ってしまいます。本記事はそこから一歩進めて、承認プールをCodeBuildロールに限定し、人の迂回まで防ぐ点を確認します。
構成
今回はテスト対象として、Ansibleのplaybookを検証する想定にしました(CodeBuildで実行できるものであれば、テスト内容は何でも構いません)。
全体の構成は次のとおりです。

テスト対象のplaybook(site.yml)は、ビルドコンテナ内の簡易Webサーバーへの疎通を確認するだけの内容です。アクセス先のポート番号を変えることで、テストの成功(8080)と失敗(未使用ポート)を切り替えます。
# 検証用playbook: ビルドコンテナ内の簡易Webサーバーへの疎通を確認する
# テストの成功/失敗の切り替えはこのファイルのポート番号を直接編集する
# 8080 = Webサーバーあり(テスト成功) / 9000等の未使用ポート = テスト失敗
- hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Webサーバー起動確認
ansible.builtin.uri:
url: "http://127.0.0.1:8080"
status_code: 200
CodeBuildのbuildspec(テスト実行 → PRコメント投稿 → 承認/差し戻し)は次のとおりです。playbookの終了コードでAPPROVE/REVOKEを分岐し、PRコメントの投稿と承認状態の更新をCodeBuildのサービスロールから直接実行しています。
version: 0.2
phases:
install:
commands:
# Ansible環境(uriモジュールはansible-core同梱)
- python3 -m pip install --quiet ansible-core
build:
commands:
- echo "PR=$PULL_REQUEST_ID SOURCE=$SOURCE_COMMIT_ID DEST=$DESTINATION_COMMIT_ID"
# テスト対象の簡易Webサーバーをコンテナ内で起動(ポート8080)
- nohup python3 -m http.server 8080 >/dev/null 2>&1 &
- sleep 1
# playbook実行。終了コードを捕捉してビルドは止めない
- PLAYBOOK_LOG=$(ansible-playbook -i "localhost," -c local -e ansible_python_interpreter="$(command -v python3)" site.yml 2>&1); EXITCODE=$?
- echo "$PLAYBOOK_LOG"
post_build:
commands:
- |
if [ "$EXITCODE" -eq 0 ]; then
RESULT="PASSED"
PR_STATUS="APPROVE"
else
RESULT="FAILED"
PR_STATUS="REVOKE"
fi
LOG_TAIL=$(echo "$PLAYBOOK_LOG" | tail -c 2000)
COMMENT=$(printf 'テスト結果: %s (build: %s)\n\n```\n%s\n```' "$RESULT" "$CODEBUILD_BUILD_ID" "$LOG_TAIL")
- >
aws codecommit post-comment-for-pull-request
--pull-request-id "$PULL_REQUEST_ID"
--repository-name approval-verify
--content "$COMMENT"
--before-commit-id "$DESTINATION_COMMIT_ID"
--after-commit-id "$SOURCE_COMMIT_ID"
- REVISION_ID=$(aws codecommit get-pull-request --pull-request-id "$PULL_REQUEST_ID" --query 'pullRequest.revisionId' --output text)
- >
aws codecommit update-pull-request-approval-state
--pull-request-id "$PULL_REQUEST_ID"
--revision-id "$REVISION_ID"
--approval-state "$PR_STATUS"
- exit "$EXITCODE"
承認ルールテンプレートやEventBridge、各IAMロールを含む全体のTerraformコードは折りたたみに掲載します。
Terraform 全文(main.tf)
data "aws_caller_identity" "current" {}
# ---------------------------------------------------------------
# CodeCommitリポジトリ
# ---------------------------------------------------------------
resource "aws_codecommit_repository" "this" {
repository_name = "approval-verify"
description = "承認プール検証用リポジトリ(一時検証・削除可)"
}
# ---------------------------------------------------------------
# CodeBuild(テスト → PRコメント投稿 → APPROVE/REVOKE)
# ---------------------------------------------------------------
resource "aws_iam_role" "codebuild" {
name = "approval-verify-codebuild-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codebuild.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "codebuild" {
name = "codebuild-policy"
role = aws_iam_role.codebuild.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"codecommit:GitPull",
"codecommit:GetPullRequest",
"codecommit:UpdatePullRequestApprovalState",
"codecommit:PostCommentForPullRequest",
]
Resource = aws_codecommit_repository.this.arn
},
]
})
}
resource "aws_codebuild_project" "this" {
name = "approval-verify"
description = "PR検証ビルド(承認プール検証用)"
service_role = aws_iam_role.codebuild.arn
artifacts {
type = "NO_ARTIFACTS"
}
environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
type = "LINUX_CONTAINER"
}
source {
type = "CODECOMMIT"
location = aws_codecommit_repository.this.clone_url_http
buildspec = <<-EOT
version: 0.2
phases:
install:
commands:
# Ansible環境(uriモジュールはansible-core同梱)
- python3 -m pip install --quiet ansible-core
build:
commands:
- echo "PR=$PULL_REQUEST_ID SOURCE=$SOURCE_COMMIT_ID DEST=$DESTINATION_COMMIT_ID"
# テスト対象の簡易Webサーバーをコンテナ内で起動(ポート8080)
- nohup python3 -m http.server 8080 >/dev/null 2>&1 &
- sleep 1
# playbook実行。終了コードを捕捉してビルドは止めない
- PLAYBOOK_LOG=$(ansible-playbook -i "localhost," -c local -e ansible_python_interpreter="$(command -v python3)" site.yml 2>&1); EXITCODE=$?
- echo "$PLAYBOOK_LOG"
post_build:
commands:
- |
if [ "$EXITCODE" -eq 0 ]; then
RESULT="PASSED"
PR_STATUS="APPROVE"
else
RESULT="FAILED"
PR_STATUS="REVOKE"
fi
LOG_TAIL=$(echo "$PLAYBOOK_LOG" | tail -c 2000)
COMMENT=$(printf 'テスト結果: %s (build: %s)\n\n```\n%s\n```' "$RESULT" "$CODEBUILD_BUILD_ID" "$LOG_TAIL")
- >
aws codecommit post-comment-for-pull-request
--pull-request-id "$PULL_REQUEST_ID"
--repository-name approval-verify
--content "$COMMENT"
--before-commit-id "$DESTINATION_COMMIT_ID"
--after-commit-id "$SOURCE_COMMIT_ID"
- REVISION_ID=$(aws codecommit get-pull-request --pull-request-id "$PULL_REQUEST_ID" --query 'pullRequest.revisionId' --output text)
- >
aws codecommit update-pull-request-approval-state
--pull-request-id "$PULL_REQUEST_ID"
--revision-id "$REVISION_ID"
--approval-state "$PR_STATUS"
- exit "$EXITCODE"
EOT
}
source_version = "refs/heads/main"
}
# ---------------------------------------------------------------
# 承認ルールテンプレート(承認プールをCodeBuildサービスロールに限定)
# ---------------------------------------------------------------
resource "aws_codecommit_approval_rule_template" "ci_only" {
name = "approval-verify-ci-only"
description = "CodeBuildサービスロールの承認のみをカウントする(必要承認数1)"
content = jsonencode({
Version = "2018-11-08"
DestinationReferences = ["refs/heads/main"]
Statements = [{
Type = "Approvers"
NumberOfApprovalsNeeded = 1
ApprovalPoolMembers = [
"arn:aws:sts::${data.aws_caller_identity.current.account_id}:assumed-role/${aws_iam_role.codebuild.name}/*"
]
}]
})
}
resource "aws_codecommit_approval_rule_template_association" "this" {
approval_rule_template_name = aws_codecommit_approval_rule_template.ci_only.name
repository_name = aws_codecommit_repository.this.repository_name
}
# ---------------------------------------------------------------
# EventBridge: PR作成/更新 → CodeBuild起動
# ---------------------------------------------------------------
resource "aws_iam_role" "events" {
name = "approval-verify-events-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "events" {
name = "start-build"
role = aws_iam_role.events.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["codebuild:StartBuild"]
Resource = aws_codebuild_project.this.arn
}]
})
}
resource "aws_cloudwatch_event_rule" "pr" {
name = "approval-verify-pr-events"
description = "PR作成・ソースブランチ更新でCodeBuildを起動する"
event_pattern = jsonencode({
source = ["aws.codecommit"]
"detail-type" = ["CodeCommit Pull Request State Change"]
resources = [aws_codecommit_repository.this.arn]
detail = {
event = ["pullRequestCreated", "pullRequestSourceBranchUpdated"]
pullRequestStatus = ["Open"]
isMerged = ["False"]
destinationReference = ["refs/heads/main"]
}
})
}
resource "aws_cloudwatch_event_target" "codebuild" {
rule = aws_cloudwatch_event_rule.pr.name
arn = aws_codebuild_project.this.arn
role_arn = aws_iam_role.events.arn
input_transformer {
input_paths = {
pullRequestId = "$.detail.pullRequestId"
sourceCommit = "$.detail.sourceCommit"
destinationCommit = "$.detail.destinationCommit"
}
input_template = <<-EOT
{
"sourceVersion": <sourceCommit>,
"environmentVariablesOverride": [
{"name": "PULL_REQUEST_ID", "type": "PLAINTEXT", "value": <pullRequestId>},
{"name": "SOURCE_COMMIT_ID", "type": "PLAINTEXT", "value": <sourceCommit>},
{"name": "DESTINATION_COMMIT_ID", "type": "PLAINTEXT", "value": <destinationCommit>}
]
}
EOT
}
}
# ---------------------------------------------------------------
# 開発者ロール: 広いCodeCommit権限 + 承認/上書きの明示Deny
# ---------------------------------------------------------------
resource "aws_iam_role" "dev_restricted" {
name = "approval-verify-dev-restricted"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "dev_restricted" {
name = "dev-restricted-policy"
role = aws_iam_role.dev_restricted.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCodeCommitPowerUser"
Effect = "Allow"
Action = ["codecommit:*"]
Resource = aws_codecommit_repository.this.arn
},
{
Sid = "DenyApprovalBypass"
Effect = "Deny"
Action = [
"codecommit:UpdatePullRequestApprovalState",
"codecommit:OverridePullRequestApprovalRules",
]
Resource = aws_codecommit_repository.this.arn
},
]
})
}
やってみた
Terraformのデプロイは完了済みの状態からスタートします。
設定された承認ルールテンプレートを確認する
まずデプロイで作られた承認ルールテンプレートを見てみます。承認プールのメンバーにCodeBuildのサービスロール(assumed-role/approval-verify-codebuild-role/*)だけを指定しているのが肝です。承認プールを指定すると、このメンバーからの承認のみがルールを満たす条件としてカウントされます。

リポジトリには、テスト対象のplaybook(site.yml)だけを置いた状態です。

失敗するPRを入れてみる
リポジトリに対して、テストがわざと失敗するPRを作成します。site.ymlのアクセス先を、誰もListenしていないポート9000に変更しました。

PRを作成すると、一覧の承認ステータスは「1中0のルールが満たされました」となっています。

PRの作成をトリガーにEventBridge経由でCodeBuildが自動起動し、テストが失敗しました。

失敗結果がPRコメントに投稿される
CodeBuildがテスト結果をPRコメントに投稿します。投稿者はCodeBuildのサービスロールです。

承認ルールの評価もapproved: falseです。
aws codecommit evaluate-pull-request-approval-rules \
--pull-request-id 2 --revision-id "$REV"
{
"evaluation": {
"approved": false,
"overridden": false,
"approvalRulesSatisfied": [],
"approvalRulesNotSatisfied": [
"approval-verify-ci-only"
]
}
}
人間がApproveしても承認は満たされない
次に、権限を持つ人間がPRをApproveしてみます。承認の記録自体は残りますが、評価はapproved: falseのままです。
aws codecommit update-pull-request-approval-state \
--pull-request-id 2 --revision-id "$REV" --approval-state APPROVE
aws codecommit get-pull-request-approval-states \
--pull-request-id 2 --revision-id "$REV"
// 承認の記録自体は残る
{
"approvals": [
{
"userArn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/xxxxxxxx/xxxxxxxx",
"approvalState": "APPROVE"
}
]
}
aws codecommit evaluate-pull-request-approval-rules \
--pull-request-id 2 --revision-id "$REV"
// それでも評価は false(承認ルールは満たされない)
{
"evaluation": {
"approved": false,
"approvalRulesNotSatisfied": ["approval-verify-ci-only"]
}
}
コンソールでも、承認済み(承認: 1)にもかかわらず、テンプレート適用の承認ルールは「満たされていないルール」のままです。

承認ルールの上書き(オーバーライド)による迂回
一方で、承認タブには「承認ルールを上書き」というボタンがあります。

このPRで上書きを実行すると、すべての承認ルールが無効化され、テストが失敗したままでもマージできる状態になります。

上書き後はマージボタンが活性化しました。

ちなみに、Terraformで用意しておいた上書き禁止のIAMロール(approval-verify-dev-restricted)にスイッチロールして上書きを試してみました。このロールはcodecommit:*を許可しつつ、承認と上書きの2アクションだけを明示的にDenyしています。Denyのステートメントは次のとおりです。
{
"Sid": "DenyApprovalBypass",
"Effect": "Deny",
"Action": [
"codecommit:UpdatePullRequestApprovalState",
"codecommit:OverridePullRequestApprovalRules"
],
"Resource": "arn:aws:codecommit:ap-northeast-1:xxxxxxxxxxxx:approval-verify"
}
このロールで別のPRを開き、上書きボタンを押してみます。

上書きの確認ダイアログで「上書き」を実行すると……

is not authorized to perform: codecommit:OverridePullRequestApprovalRules ... with an explicit deny in an identity-based policy というエラーになり、上書きできませんでした。

このように、開発者ロールにcodecommit:OverridePullRequestApprovalRules(とcodecommit:UpdatePullRequestApprovalState)のDenyを付けておくことで、上書きによる迂回も防止できました。
話を戻して、検証を続けるため上書きを取り消します。

取り消すと、承認ルール要件が再び適用され、元のブロック状態に戻ります。

修正して再Pushすると成功する
site.ymlのポートを8080に戻して再Pushします。PRの更新をトリガーにビルドが再実行され、今度は成功しました。

PRコメントもPASSEDに変わります。

承認者がCodeBuildのサービスロールになり、承認ルールが「満たされているルール」になりました。マージボタンも活性化しています。

マージ戦略を選んでマージします。

マージも成功しました。

最後に
CodeCommitで、人の承認を挟まずに「CIが成功したPRだけマージできる」制御を作れました。ポイントは2点です。
- 承認プールをCodeBuildロールに限定し、人の手動Approveをカウントさせない
- 開発者ロールに
OverridePullRequestApprovalRulesのDenyを付け、上書きによる迂回も塞ぐ
ただしこれで防げるのはCodeCommitのPR承認・マージ経路に限ります。承認の実体はCodeBuildロールによる承認API実行なので、CodeBuildの編集権限やiam:PassRoleを持つ人はbuildspecを書き換えて承認を偽造できます。厳密に統制するなら、開発者ロールにCI基盤の管理権限を与えないことも前提になります。
本記事がどなたかの参考になれば幸いです。








