CodeCommit の PR を Amazon Bedrock で自動レビューする仕組みを作ってみた

記事タイトルとURLをコピーする

はじめに

おなかが痛くてもコーヒーは飲む、近藤恭平です。

CodeCommit, EventBridge, Lambda, Bedrock を組み合わせて、PR が作成・更新されるたびに AI が自動でコードレビューしてコメントを投稿する仕組みを作ってみました。AWS のマネージドサービスだけで完結するシンプルな構成です。

PR 自動コメントの挙動

実際に PR を作成すると、以下のようなレビューコメントが自動で投稿されます。

PR 自動コメントの挙動

SQL インジェクションやハードコードされた認証情報といったセキュリティ上の問題を検出し(「レビュー結果の確認」のセクションで全文を添付しています)、改善案まで提示してくれています。

アーキテクチャ

アーキテクチャ

処理の流れは以下のとおりです。

  1. CodeCommit で PR が作成・更新される
  2. EventBridge がイベントを検知し、Lambda を起動する
  3. Lambda が CodeCommit API で PR の変更内容を取得する
  4. 取得した diff を Bedrock(Amazon Nova Pro)に送信してレビューを依頼する
  5. レビュー結果を CodeCommit の PR コメントとして投稿する

使用するサービスは 4 つだけです。インフラはすべて AWS CDK で定義しています。

構成

IaC は AWS CDK(TypeScript)、Lambda は Python で実装しています。ディレクトリ構成は以下のとおりです。

pr-review-in-codecommit/
  bin/
    pr-review-in-codecommit.ts   # CDK エントリポイント
  lib/
    pr-review-in-codecommit-stack.ts  # スタック定義
  lambda/
    src/
      Dockerfile                 # Lambda コンテナイメージ
      index.py                   # ハンドラー

ここからは各リソースの定義について説明していきます。

CodeCommit

レビュー対象のリポジトリを CDK で新規作成します。

const repo = new codecommit.Repository(this, "Repository", {
  repositoryName: "pr-review-demo",
});

EventBridge

CodeCommit の「トリガー」機能は push イベントのみ対応しており、PR イベントは取得できません。PR の作成・更新を検知するには EventBridge を使う必要があります。

const rule = new events.Rule(this, "PrEventRule", {
  eventPattern: {
    source: ["aws.codecommit"],
    detailType: ["CodeCommit Pull Request State Change"],
    detail: {
      event: ["pullRequestCreated", "pullRequestSourceBranchUpdated"],
      repositoryNames: [repo.repositoryName],
    },
  },
});

rule.addTarget(new targets.LambdaFunction(reviewFn));

pullRequestCreated は PR 新規作成時、pullRequestSourceBranchUpdated はソースブランチへの追加 push 時に発火します。

Lambda

Docker イメージで Lambda をデプロイしています。タイムアウトは 5 分、メモリは 256MB に設定しています。

const reviewFn = new lambda.DockerImageFunction(this, "PrReviewFunction", {
  code: lambda.DockerImageCode.fromImageAsset(
    path.join(__dirname, "..", "lambda", "src")
  ),
  timeout: cdk.Duration.minutes(5),
  memorySize: 256,
  environment: {
    BEDROCK_MODEL_ID: "amazon.nova-pro-v1:0",
  },
});

Lambda の IAM ロールには、CodeCommit の読み取り・コメント投稿権限と、Bedrock のモデル呼び出し権限を付与します。

// CodeCommit 権限
reviewFn.addToRolePolicy(
  new iam.PolicyStatement({
    actions: [
      "codecommit:GetPullRequest",
      "codecommit:GetDifferences",
      "codecommit:GetBlob",
      "codecommit:PostCommentForPullRequest",
    ],
    resources: [repo.repositoryArn],
  })
);

// Bedrock 権限
reviewFn.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["bedrock:InvokeModel"],
    resources: [
      `arn:aws:bedrock:${this.region}::foundation-model/*`,
      `arn:aws:bedrock:us:${this.account}:inference-profile/*`,
    ],
  })
);

Lambda ハンドラーの処理の流れは以下のとおりです。

  1. EventBridge イベントから PR ID・リポジトリ名・コミット ID を取得する
  2. get_pull_request() で PR のタイトルと説明を取得する
  3. get_differences() で変更ファイル一覧を取得する(ページネーション対応)
  4. 各ファイルの before/after を get_blob() で取得し、diff テキストを組み立てる
  5. Bedrock の Converse API でレビューを依頼する
  6. post_comment_for_pull_request() で PR にコメントを投稿する

Bedrock

モデル呼び出しには Converse API を使用しています。Converse API はモデルに依存しない統一インターフェースのため、モデル ID を変更するだけで Claude や Nova など別のモデルに切り替えることができます。Converse API と InvokeModel API の違いについては以下の記事を参照ください。

blog.serverworks.co.jp

response = bedrock.converse(
    modelId=BEDROCK_MODEL_ID,
    messages=[
        {
            "role": "user",
            "content": [{"text": prompt}],
        }
    ],
    inferenceConfig={"maxTokens": 4096},
)

return response["output"]["message"]["content"][0]["text"]

プロンプトでは、バグ・セキュリティ・パフォーマンス・可読性・ベストプラクティスの 5 つの観点でレビューするよう指示しています。問題がなければ「LGTM」を返すようにしています。

挙動の確認

ここからは、実際にデプロイして動作を確認していきましょう。

デプロイ

以下のコマンドでスタックをデプロイします。

npx cdk deploy

リポジトリのセットアップ

デプロイが完了したら、作成された CodeCommit リポジトリをクローンします。

git clone https://git-codecommit.us-east-1.amazonaws.com/v1/repos/pr-review-demo
cd pr-review-demo

main ブランチに初期コミットを作成します。

git checkout -b main
echo '# PR Review Demo' > README.md
git add README.md && git commit -m "Initial commit"
git push -u origin main

PR の作成

feature ブランチを作成し、レビュー対象のコードを追加します。今回はレビューで指摘されそうな問題をいくつか意図的に仕込んだコードを用意しました。

import os
import subprocess


def get_user_data(user_id):
    query = "SELECT * FROM users WHERE id = " + user_id
    return execute_query(query)


def execute_query(query):
    pass


def run_command(user_input):
    result = subprocess.run(user_input, shell=True, capture_output=True)
    return result.stdout


def read_config():
    password = "admin123"
    api_key = "sk-1234567890abcdef"
    return {"password": password, "api_key": api_key}


def process_items(items):
    result = []
    for i in range(len(items)):
        result.append(items[i] * 2)
    return result

主な問題点は以下のとおりです。

  • SQL インジェクション: get_user_data() でユーザー入力を直接 SQL に結合している
  • コマンドインジェクション: run_command()shell=True のままユーザー入力を実行している
  • 認証情報のハードコード: read_config() にパスワードと API キーが直書きされている
  • 冗長な記述: process_items()range(len(...)) を使っている(リスト内包表記で書ける)

これらを Bedrock (Nova Pro) がどこまで検出できるか確認してみましょう。

git checkout -b feature/add-sample-code
# (上記の app.py を作成)
git add . && git commit -m "初期プログラムを追加しました。"
git push -u origin feature/add-sample-code

AWS CLI で PR を作成します。

aws codecommit create-pull-request \
  --title "Add sample application code" \
  --description "初期プログラムを追加しました。" \
  --targets repositoryName=pr-review-demo,sourceReference=feature/add-sample-code,destinationReference=main

レビュー結果の確認

PR のアクティビティに以下のレビューコメントが自動で投稿されました。

レビューコメント

このプルリクエストの変更内容をレビューします。

  1. バグや潜在的な問題 get_user_data 関数で SQL クエリを組み立てる際に、SQL インジェクションの脆弱性があります。user_id を直接クエリに挿入するのは危険です。 execute_query 関数は現在 pass で実装されており、実際のデータベースクエリを実行していません。 run_command 関数では、user_input を直接 subprocess.run に渡しており、コマンドインジェクションの脆弱性があります。 process_items 関数では、range(len(items)) の代わりに直接 items をイテレートした方が Python 的です。
  2. セキュリティ上の懸念 read_config 関数でハードコーディングされたパスワードと API キーはセキュリティリスクです。環境変数を使用する方が良いでしょう。 run_command 関数で shell=True を使用しているため、コマンドインジェクションの脆弱性があります。
  3. パフォーマンスの問題 process_items 関数でリストを生成する際に、result.append を使用していますが、リスト内包表記を使用した方がパフォーマンスが良いです。
  4. コードの可読性・保守性 get_user_data と execute_query の関係が不明確です。execute_query が実際に何をするのかドキュメント化する必要があります。 hello 関数は実際に使用されていません。不要であれば削除すべきです。
  5. ベストプラクティスへの準拠 SQL クエリではパラメータ化されたクエリを使用すべきです。 コマンドラインの実行では shell=False を使用し、引数をリストとして渡すべきです。 ハードコーディングされた秘密鍵は環境変数を使用するべきです。 改善案
# app.py (改善後)
import os
import subprocess
import sqlite3  # 例として sqlite3 を使用


def get_user_data(user_id):
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id =?", (user_id,))
    result = cursor.fetchall()
    conn.close()
    return result


def run_command(user_input):
    # セキュリティ上の理由から shell=False を使用
    result = subprocess.run(user_input, shell=False, capture_output=True, text=True)
    return result.stdout


def read_config():
    password = os.getenv("PASSWORD", "default_password")
    api_key = os.getenv("API_KEY", "default_api_key")
    return {"password": password, "api_key": api_key}


def process_items(items):
    return [item * 2 for item in items]


def hello():
    print("Hello, world!")

README.md の変更

# PR Review Demo
# updated
README.md の変更は簡潔すぎるため、もう少し具体的な内容を追加することをお勧めします。

以上の点を踏まえて、重要な問題があるため、LGTM とは言えません。上記の改善案を適用することをお勧めします。

結果の振り返り

事前に仕込んでおいた 4 つの問題点に対して、レビュー結果を振り返ります。

仕込んだ問題 検出 レビューでの指摘内容
SQL インジェクション o パラメータ化されたクエリの使用を提案
コマンドインジェクション o shell=False への変更と引数のリスト渡しを提案
認証情報のハードコード o 環境変数の使用を提案
冗長な記述 o リスト内包表記への書き換えを提案

仕込んだ問題はすべて検出されました。加えて、execute_query() の未実装や hello() の未使用といった点も指摘されており、改善案のコード付きで回答が返ってきています。

まとめ

CodeCommit, EventBridge, Lambda, Bedrock の 4 サービスで、PR の自動レビューシステムを構築しました。

今回仕込んだセキュリティ上の問題やコードスタイルの指摘はすべて検出され、改善案付きで返ってきました。

コンテキストを共有しないというメリットもあると思いますが、ローカルでコードを書く際に使用する基盤モデルと、レビューを行う基盤モデルで別のモデルを使用することにもメリットがあると感じました。

ここまで読んでいただきありがとうございました!

2021年4月新卒入社。目に見えて、動かせるものが好き。