Devinが作ったPRも安全に検証したくて、PR専用環境を自動構築できる仕組みを作った
はじめに
皆様は開発での動作確認はどのようにしていますか?弊社では開発者も増え、機能開発も増えてきた昨今、検証環境との衝突が絶えなくなっており、機能の動作確認待ちが増えていました。

動作確認を誰にも邪魔されず、相乗り環境でも他の開発者の機能の影響が無いように。。。など気を遣うシーンがだんだん増えてきていた昨今、エンジニア同士の会話で、簡単な仕組みでいいからプレビュー環境が欲しいという声が上がっていました。
以前の職場でもSREの方が複製環境を作っていたことを思い出し、自身でそれっぽい環境を作ってみることにしました。
背景 1️⃣:エンジニア同士の検証環境の待ちが増えてきた
弊社のプロダクトでは、AI行政という行政特化型のAIプロダクトを開発しています。

自治体の皆様に導入いただいたり、行政の現場に導入するとなると基本的な機能を揃える必要があります。開発初期だからという理由もあるのですが、検証環境の待ちが絶えず、検証が十分に行えないままリリーススケジュールが遅れてしまう等の課題がありました。
中には簡単な機能なのにコンフリクトが発生してしまって待つ必要があるということです。
背景 2️⃣:Devinを導入したら新しい課題が生まれた
私たちのチームでは2024年後半からDevin(AIコーディングエージェント) を本格的に活用し始めました。
Devinは優秀ですよね。タスクを投げれば、自律的にコードを書いてPRを作ってくれる。でも、使い始めてすぐに気づきました。
「このPR、本当に動くのか?動作確認は誰がするのか?」
AIが書いたコードは、見た目は正しそうでも実際に動かしてみないとわからない。ビルドは通っても、ランタイムエラーが出ることもある。既存の機能を壊していることもある。
develop環境にマージするリスク
じゃあdevelop環境やstaging環境にマージして確認すればいいか?というと、それも怖い。
- チームメンバー全員が使っている環境
- QAやCSチームがstaging環境でテストしている
- 壊れたら他の人の作業が止まる
Devinが作ったPRを「とりあえずマージして確認」なんて、怖くてできません。
ローカルで確認する手間
「じゃあローカルで確認する?」
- ブランチをチェックアウト
-
pnpm installで依存関係更新 - DBマイグレーション
- 環境変数の設定
- サーバー起動
Devinが1日に5つPRを作ってきたら、これを5回やるんですか?少し厳しいですよね。
欲しかったもの
そこで欲しくなったのが、こういう環境でした:
- 壊れても誰にも迷惑がかからない、完全に隔離された環境
- URLにアクセスするだけで動作確認できる
- PRごとに独立していて、複数のPRを同時に確認できる
- 自動で作られて、自動で消える
- プロダクト的にFEとBEが別れているので結合的に動作確認したい
これを実現するために作ったのが 「dev-dup」 (develop duplicate) です。
作ったもの:ラベルを貼るだけでPRごとに専用環境が立ち上がる
仕組みはシンプルです。
- PRに
dev-dupラベルを貼る - 約5~10分待つ
- PR専用の環境(DB + API + フロントエンド)が立ち上がる
- URLにアクセスして動作確認

安心して 「安全に迷惑をかけずにまず動かしてみる」 ができるようになりました。
システムアーキテクチャ
全体構成
ワークフロー詳細
1. トリガー条件
on:
pull_request:
types: [labeled, unlabeled]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number for environment suffix'
required: true
type: string
-
通常: PRに
dev-dupラベルを付与すると自動実行 -
手動:
workflow_dispatchで任意のPR番号を指定して実行可能
2. 並列実行ジョブ(工夫した点)
最初は素朴に全ステップを順次実行していたのですが、デプロイに15分以上かかっていました。これではラベルを貼ってからコーヒーを淹れに行って戻ってきてもまだ終わっていない状態で、さすがに待てない。
そこで依存関係を整理して、並列実行できるものは徹底的に並列化しました。
工夫ポイント:
- DB作成・Vercelデプロイ・Dockerビルドは互いに独立なので、3つ同時に実行
- Cloud Runデプロイは「DBが存在する」「Dockerイメージがある」ことが前提なので、両方の完了を待ってから実行
- Load Balancerは「Cloud RunのURLがわかっている」「VercelのURLがわかっている」ことが必要なので最後
結果、15分 → 5〜7分に短縮。待ち時間が半分以下になりました。
GitHub Actionsのneedsを使って依存関係を表現しています:
jobs:
database-setup:
# 並列実行(依存なし)
vercel-deploy:
# 並列実行(依存なし)
docker-build:
# 並列実行(依存なし)
cloud-run-deploy:
needs: [database-setup, docker-build] # 両方の完了を待つ
load-balancer-setup:
needs: [cloud-run-deploy, vercel-deploy] # 両方の完了を待つ
各ステップの技術詳細
Step 1: PRコメント初期化
デプロイの進捗状況をPRコメントでリアルタイムに可視化します。
- name: Create initial PR comment
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
<!-- deploy-preview-env-status -->
## 🚀 Preview Environment Deployment Status
| Step | Status | Details |
|------|--------|---------|
| 🗄️ **Database Setup** | ⏳ In Progress | Setting up database |
| 🎨 **Frontend (Vercel)** | ⏳ In Progress | Deploying to Vercel |
| 🐳 **Docker Images** | ⏳ In Progress | Building Docker images |
| ☁️ **Cloud Run** | ⌛ Waiting | Waiting for dependencies |
| 🔄 **Load Balancer** | ⌛ Waiting | Waiting for Cloud Run |
各ジョブの完了時に、HTMLコメントマーカー(<!-- deploy-preview-env-status -->)を使って同じコメントを更新します。
Step 2: データベースセットアップ
PR専用のPostgreSQLデータベースをCloud SQLに作成します。
- name: Create database
run: |
gcloud sql databases create "${{ env.DB_NAME }}" \
--instance "${{ env.INSTANCE_NAME }}" || {
echo "Database already exists, continuing..."
}
- name: Setup Cloud SQL Proxy and run migrations
run: |
# Cloud SQL Proxyを起動
curl -o /tmp/cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64
chmod +x /tmp/cloud_sql_proxy
/tmp/cloud_sql_proxy -instances=$PROJECT_ID:$REGION:$INSTANCE_NAME=tcp:5432 &
sleep 5
# Prismaマイグレーション実行
pnpm db:migrate:deploy
pnpm db:generate
pnpm db:seed || echo "Seeding failed, continuing..."
env:
DATABASE_URL: postgres://user:password@127.0.0.1:5432/${{ env.DB_NAME }}
ポイント:
- 既存DBがあればスキップ(冪等性確保)
- Cloud SQL Proxy経由でセキュアに接続
- Prisma Migration + Seedingを実行
※意外と重要な点ですが、開発用のSeedデータをきちんと準備することが大事です。そうでないとイチからデータを準備する必要があるので、モック的なデータが必要になります。
Step 3: Vercelデプロイ
フロントエンドをVercelにデプロイし、PR専用のエイリアスを設定します。
- name: Deploy to Vercel
run: |
# Vercelにデプロイ
VERCEL_OUTPUT=$(npx vercel deploy \
--token=${{ secrets.VERCEL_TOKEN }} \
--target develop \
--meta gitCommitRef="$BRANCH_NAME" \
--meta gitCommitSha="${{ github.sha }}")
# デプロイURLを抽出
VERCEL_URL=$(echo "$VERCEL_OUTPUT" | grep -oE 'https://[^[:space:]]*\.vercel\.app')
# PR専用エイリアスを設定
ALIAS_URL="pr${PR_NUMBER}.vercel.app"
npx vercel alias set "$VERCEL_URL" "$ALIAS_URL" \
--token=${{ secrets.VERCEL_TOKEN }} \
--scope=team_xxx
Step 4: Dockerイメージビルド
APIとContent APIのDockerイメージをビルドし、Artifact Registryにプッシュします。
- name: Build and Push API image
run: |
docker build -f apps/api/Dockerfile \
-t "asia-northeast1-docker.pkg.dev/$PROJECT_ID/repo/api:$IMAGE_TAG" .
docker push "asia-northeast1-docker.pkg.dev/$PROJECT_ID/repo/api:$IMAGE_TAG"
Step 5: Cloud Runデプロイ
PR専用のCloud Runサービスを作成します。
- name: Deploy API service
run: |
gcloud run deploy "${{ env.SERVICE_SUFFIX }}-api" \
--image "asia-northeast1-docker.pkg.dev/$PROJECT_ID/repo/api:$IMAGE_TAG" \
--region "${{ env.REGION }}" \
--platform managed \
--no-allow-unauthenticated \
--service-account=cloud-run-service-account@$PROJECT_ID.iam.gserviceaccount.com \
--env-vars-file env-vars.yaml \
--use-http2 \
--memory=500Mi \
--cpu=1 \
--max-instances=1 \
--port=8080 \
--ingress=internal-and-cloud-load-balancing \
--add-cloudsql-instances=$PROJECT_ID:$REGION:$INSTANCE_NAME
重要な設定:
| 設定 | 値 | 理由 |
|---|---|---|
--no-allow-unauthenticated |
認証必須 | セキュリティ確保 |
--ingress=internal-and-cloud-load-balancing |
内部+LB経由のみ | 直接アクセス防止 |
--max-instances=1 |
最大1インスタンス | コスト最適化 |
--add-cloudsql-instances |
Cloud SQL接続 | DB接続用 |
Step 6: Load Balancer設定
ヘッダーベースルーティングで、複数のプレビュー環境を1つのLoad Balancerで管理します。
6.1 Network Endpoint Group (NEG) 作成
- name: Create Network Endpoint Group
run: |
gcloud compute network-endpoint-groups create "$NEG_NAME" \
--region "${{ env.REGION }}" \
--network-endpoint-type=serverless \
--cloud-run-service "${{ env.SERVICE_SUFFIX }}-api"
6.2 Backend Service 作成
- name: Create Backend Service
run: |
# Backend Service作成
gcloud compute backend-services create "$BS_NAME" \
--global \
--load-balancing-scheme=EXTERNAL_MANAGED \
--protocol HTTP \
--port-name http \
--timeout=30s \
--custom-request-header="Host:hogehoge.jp"
# NEGをBackend Serviceに追加
gcloud compute backend-services add-backend "$BS_NAME" \
--global \
--network-endpoint-group "$NEG_NAME" \
--network-endpoint-group-region "${{ env.REGION }}"
6.3 URL Map更新(ヘッダーベースルーティング)
URL Map更新スクリプト(Python)の核心部分:
def create_header_based_route_rule(header_value, prefixes, backend_service_name, priority, project_id, config):
"""ヘッダーマッチングとプレフィックスマッチングを組み合わせたルートルールを作成"""
route_rules = []
for prefix in prefixes:
match_rule = {
"headerMatches": [
{
"headerName": "x-env",
"exactMatch": str(header_value) # PR番号
}
],
"prefixMatch": prefix # 例: "/api/", "/trpc/"
}
route_rule = {
"matchRules": [match_rule],
"priority": priority,
"service": f"https://www.googleapis.com/compute/v1/projects/{project_id}/global/backendServices/{backend_service_name}"
}
route_rules.append(route_rule)
return route_rules
優先度の自動計算:
def get_safe_priority_range(config, num_rules_needed):
"""既存ルールと衝突しない優先度範囲を計算"""
existing_priorities = set()
for path_matcher in config.get("pathMatchers", []):
for route_rule in path_matcher.get("routeRules", []):
if "priority" in route_rule:
existing_priorities.add(route_rule["priority"])
# 衝突しない優先度を探索
start_priority = 1
while any(start_priority + i in existing_priorities for i in range(num_rules_needed)):
start_priority += 1
return start_priority
アクセス方法
フロントエンド
https://pr{PR番号}.vercel.app
例: https://pr548.vercel.app
自動クリーンアップ
不要になったプレビュー環境は、週次のcronジョブで自動削除されます。
トリガー
on:
schedule:
- cron: '0 0 */7 * *' # 7日ごとの0:00 UTC
workflow_dispatch: # 手動実行も可能
クリーンアップフロー
-
dev-dupラベルが付いたオープン中のPRを取得(例: 548, 549, 891) - 全ての
dev_dup_*リソースを列挙 - アクティブなPR番号以外のリソースを削除
- DB:
dev_dup_550など不要なDBを削除 - Cloud Run:
dev-dup-pr550-apiなど不要なサービスを削除 - Load Balancer: 不要なNEG、Backend Service、URL Mapルールを削除
- DB:
PRコメントステータス表示
デプロイの進捗はPRコメントでリアルタイムに確認できます。
デプロイ中
## 🚀 Preview Environment Deployment Status
**PR #548** - Deployment started at 2025-12-24T10:30:00Z
| Step | Status | Details |
|------|--------|---------|
| 🗄️ **Database Setup** | ✅ Completed | Database `dev_dup_548` created |
| 🎨 **Frontend (Vercel)** | ✅ Completed | [Preview URL](https://pr548.vercel.app) |
| 🐳 **Docker Images** | ✅ Completed | Images built (tag: `pr548`) |
| ☁️ **Cloud Run** | ⏳ In Progress | Deploying API service |
| 🔄 **Load Balancer** | ⌛ Waiting | Waiting for Cloud Run |
デプロイ完了
| Step | Status | Details |
|------|--------|---------|
| 🗄️ **Database Setup** | ✅ Completed | Database `dev_dup_548` created |
| 🎨 **Frontend (Vercel)** | ✅ Completed | [Preview URL](https://pr548.vercel.app) |
| 🐳 **Docker Images** | ✅ Completed | Images built (tag: `pr548`) |
| ☁️ **Cloud Run** | ✅ Completed | API Service: `dev-dup-pr548-api` |
| 🔄 **Load Balancer** | ✅ Completed | ALB configured successfully |
セキュリティ考慮事項
認証・認可
| レイヤー | 保護方式 |
|---|---|
| Cloud Run |
--no-allow-unauthenticated(IAM認証必須) |
| Load Balancer | Managed SSL証明書 |
| Cloud SQL | Cloud SQL Proxy経由の接続 |
| GitHub Actions | Workload Identity Federation |
ネットワーク分離
# Cloud Runは内部 + LB経由のみアクセス可能
--ingress=internal-and-cloud-load-balancing
直接Cloud RunのURLにアクセスすることはできず、必ずLoad Balancer経由でアクセスする必要があります。
最小権限の原則
# 専用のサービスアカウントを使用
--service-account=cloud-run-service-account@$PROJECT_ID.iam.gserviceaccount.com
コスト最適化
リソース制限
--memory=500Mi # メモリ制限
--cpu=1 # CPU制限
--max-instances=1 # 最大インスタンス数
プレビュー環境は開発用途のため、リソースを最小限に抑えています。
自動クリーンアップ
週次の自動クリーンアップにより、不要なリソースが蓄積することを防止しています。
従量課金
- Cloud Run: リクエストベースの課金
- Cloud SQL: 共有インスタンスを使用
- Vercel: プレビュー環境は無料枠内
安全性の担保
dev-dupプレビュー環境は以下の点で他環境から完全に隔離されています:
-
専用データベース:
dev_dup_{PR番号}という別DBを使用- 特にmigrationを何回しても最悪初期化すればよいので壊し放題です
-
専用APIサーバー: Cloud Run上の独立したインスタンス
- Serverlessアーキテクチャの利点です。Cloud Runであればいくら増やしても課金も利用ベースなので心配ないです。
-
ネットワーク分離: Load Balancerのヘッダールーティングで分離
- FEとの連携が必要ですが、DNSをいちいち増やすことに比べたらだいぶ楽だと思います。
これにより、Devinが生成したコードに問題があっても:
- ❌ データが壊れることはない
- ❌ 他の開発者の環境に影響しない
- ❌ ステージング環境が不安定になることもない
実際の活用例
# Devinが作成したPR #892 の動作確認
# フロントエンド確認
open https://pr892.vercel.app
# API動作確認
curl https://hogehoge.jp/api/health \
-H "x-env: 892"
# 新しいエンドポイントのテスト
curl https://hogehoge.jp/api/new-feature \
-H "x-env: 892" \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
まとめと課題
dev-dupプレビュー環境システムにより、以下を実現しました:
- ラベル1つで完全な開発環境を自動構築
- 並列実行による高速デプロイ(約5~10分)
- ヘッダーベースルーティングによる複数環境の効率的な管理
- 週次自動クリーンアップによるコスト最適化
- PRコメントによるステータス可視化
- Devin(AIエージェント)との安全な連携基盤
これにより、レビュアーはURLにアクセスするだけで動作確認ができるようになり、CS/Engともにレビュープロセスが大幅に効率化されました。
特に、DevinのようなAIコーディングエージェントと組み合わせることで、AI生成コードを「壊れても大丈夫」な環境で安全に検証できるようになりました。これは、AI活用を進める開発チームにとって重要なインフラとなっています。
課題
完璧なものを作ったように書いていますが、まだまだ課題もあります。
- あくまで開発環境内のリソースの複製なので新API等が増えるとその分修正する必要がある
- 今はまだシンプルな構成なので可能ですが、マイクロサービス的に役割が違うAPIが増えてくると管理は困難になり得ます。
- そのため、Cloud Functionのような関数処理がある場合、専用に増やす必要があります
- develop環境のALBがTerraform管理されており、既存のALBの振り分け設定の棲み分けが必要
- 完全に設計ミスでプレビュー環境用のALBが必要になります。近日修正予定
- FEだけの修正なのにBEも構築されてしまう
- 修正内容のパスを見てBEも構築する必要があるかどうか判定をしても良いなと考えています
- どうしても構築に時間がかかる
- 致し方ないのですが、想像以上に構築には時間がかかり、そこの待機時間がボトルネックだなと感じています
- APIのDocker buildのキャッシュ最適化して工夫する必要がありそう
実際に運用をしてみてどう変わったのか?
これらの仕組みを入れた結果はこちらの記事で詳しく解説をしています。よろしければご覧ください。
Discussion