CodeCommitのPRマージをCodeBuildの成功だけで承認させ、手動承認を防ぐ構成を構築した

CodeCommitのPRマージをCodeBuildの成功だけで承認させ、手動承認を防ぐ構成を構築した

AWS CodeCommitの承認ルールで、承認プールをCodeBuildのサービスロールに限定し、人の承認を挟まずにCIが成功したPRだけマージできる構成を検証し、あわせて、承認ルールの上書きによる迂回をIAMのDenyで防げることも紹介しています。
2026.09.15

はじめに

クラウド事業統括本部の浅野です。

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で実行できるものであれば、テスト内容は何でも構いません)。

全体の構成は次のとおりです。

2026-09-15-codecommit-ci-only-approval-pool-01

テスト対象の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/*)だけを指定しているのが肝です。承認プールを指定すると、このメンバーからの承認のみがルールを満たす条件としてカウントされます。

2026-09-15-codecommit-ci-only-approval-pool-02

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

2026-09-15-codecommit-ci-only-approval-pool-03

失敗するPRを入れてみる

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

2026-09-15-codecommit-ci-only-approval-pool-04

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

2026-09-15-codecommit-ci-only-approval-pool-05

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

2026-09-15-codecommit-ci-only-approval-pool-06

失敗結果がPRコメントに投稿される

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

2026-09-15-codecommit-ci-only-approval-pool-07

承認ルールの評価も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)にもかかわらず、テンプレート適用の承認ルールは「満たされていないルール」のままです。

2026-09-15-codecommit-ci-only-approval-pool-08

承認ルールの上書き(オーバーライド)による迂回

一方で、承認タブには「承認ルールを上書き」というボタンがあります。

2026-09-15-codecommit-ci-only-approval-pool-09

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

2026-09-15-codecommit-ci-only-approval-pool-10

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

2026-09-15-codecommit-ci-only-approval-pool-11

ちなみに、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を開き、上書きボタンを押してみます。

2026-09-15-codecommit-ci-only-approval-pool-12

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

2026-09-15-codecommit-ci-only-approval-pool-13

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

2026-09-15-codecommit-ci-only-approval-pool-14

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

話を戻して、検証を続けるため上書きを取り消します。

2026-09-15-codecommit-ci-only-approval-pool-15

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

2026-09-15-codecommit-ci-only-approval-pool-16

修正して再Pushすると成功する

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

2026-09-15-codecommit-ci-only-approval-pool-17

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

2026-09-15-codecommit-ci-only-approval-pool-18

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

2026-09-15-codecommit-ci-only-approval-pool-19

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

2026-09-15-codecommit-ci-only-approval-pool-20

マージも成功しました。

2026-09-15-codecommit-ci-only-approval-pool-21

最後に

CodeCommitで、人の承認を挟まずに「CIが成功したPRだけマージできる」制御を作れました。ポイントは2点です。

  • 承認プールをCodeBuildロールに限定し、人の手動Approveをカウントさせない
  • 開発者ロールにOverridePullRequestApprovalRulesのDenyを付け、上書きによる迂回も塞ぐ

ただしこれで防げるのはCodeCommitのPR承認・マージ経路に限ります。承認の実体はCodeBuildロールによる承認API実行なので、CodeBuildの編集権限やiam:PassRoleを持つ人はbuildspecを書き換えて承認を偽造できます。厳密に統制するなら、開発者ロールにCI基盤の管理権限を与えないことも前提になります。

本記事がどなたかの参考になれば幸いです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事