🍪

Devinが作ったPRも安全に検証したくて、PR専用環境を自動構築できる仕組みを作った

に公開

はじめに

皆様は開発での動作確認はどのようにしていますか?弊社では開発者も増え、機能開発も増えてきた昨今、検証環境との衝突が絶えなくなっており、機能の動作確認待ちが増えていました。

動作確認を誰にも邪魔されず、相乗り環境でも他の開発者の機能の影響が無いように。。。など気を遣うシーンがだんだん増えてきていた昨今、エンジニア同士の会話で、簡単な仕組みでいいからプレビュー環境が欲しいという声が上がっていました。

以前の職場でもSREの方が複製環境を作っていたことを思い出し、自身でそれっぽい環境を作ってみることにしました。

背景 1️⃣:エンジニア同士の検証環境の待ちが増えてきた

弊社のプロダクトでは、AI行政という行政特化型のAIプロダクトを開発しています。
https://ai.pubtech.jp/

自治体の皆様に導入いただいたり、行政の現場に導入するとなると基本的な機能を揃える必要があります。開発初期だからという理由もあるのですが、検証環境の待ちが絶えず、検証が十分に行えないままリリーススケジュールが遅れてしまう等の課題がありました。

中には簡単な機能なのにコンフリクトが発生してしまって待つ必要があるということです。

背景 2️⃣:Devinを導入したら新しい課題が生まれた

私たちのチームでは2024年後半からDevin(AIコーディングエージェント) を本格的に活用し始めました。

Devinは優秀ですよね。タスクを投げれば、自律的にコードを書いてPRを作ってくれる。でも、使い始めてすぐに気づきました。

「このPR、本当に動くのか?動作確認は誰がするのか?」

AIが書いたコードは、見た目は正しそうでも実際に動かしてみないとわからない。ビルドは通っても、ランタイムエラーが出ることもある。既存の機能を壊していることもある。

develop環境にマージするリスク

じゃあdevelop環境やstaging環境にマージして確認すればいいか?というと、それも怖い。

  • チームメンバー全員が使っている環境
  • QAやCSチームがstaging環境でテストしている
  • 壊れたら他の人の作業が止まる

Devinが作ったPRを「とりあえずマージして確認」なんて、怖くてできません。

ローカルで確認する手間

「じゃあローカルで確認する?」

  1. ブランチをチェックアウト
  2. pnpm install で依存関係更新
  3. DBマイグレーション
  4. 環境変数の設定
  5. サーバー起動

Devinが1日に5つPRを作ってきたら、これを5回やるんですか?少し厳しいですよね。

欲しかったもの

そこで欲しくなったのが、こういう環境でした:

  • 壊れても誰にも迷惑がかからない、完全に隔離された環境
  • URLにアクセスするだけで動作確認できる
  • PRごとに独立していて、複数のPRを同時に確認できる
  • 自動で作られて、自動で消える
  • プロダクト的にFEとBEが別れているので結合的に動作確認したい

これを実現するために作ったのが 「dev-dup」 (develop duplicate) です。


作ったもの:ラベルを貼るだけでPRごとに専用環境が立ち上がる

仕組みはシンプルです。

  1. PRにdev-dupラベルを貼る
  2. 約5~10分待つ
  3. PR専用の環境(DB + API + フロントエンド)が立ち上がる
  4. 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:  # 手動実行も可能

クリーンアップフロー

  1. dev-dupラベルが付いたオープン中のPRを取得(例: 548, 549, 891)
  2. 全てのdev_dup_*リソースを列挙
  3. アクティブなPR番号以外のリソースを削除
    • DB: dev_dup_550 など不要なDBを削除
    • Cloud Run: dev-dup-pr550-api など不要なサービスを削除
    • Load Balancer: 不要なNEG、Backend Service、URL Mapルールを削除

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. ラベル1つで完全な開発環境を自動構築
  2. 並列実行による高速デプロイ(約5~10分)
  3. ヘッダーベースルーティングによる複数環境の効率的な管理
  4. 週次自動クリーンアップによるコスト最適化
  5. PRコメントによるステータス可視化
  6. Devin(AIエージェント)との安全な連携基盤

これにより、レビュアーはURLにアクセスするだけで動作確認ができるようになり、CS/Engともにレビュープロセスが大幅に効率化されました。

特に、DevinのようなAIコーディングエージェントと組み合わせることで、AI生成コードを「壊れても大丈夫」な環境で安全に検証できるようになりました。これは、AI活用を進める開発チームにとって重要なインフラとなっています。

課題

完璧なものを作ったように書いていますが、まだまだ課題もあります。

  • あくまで開発環境内のリソースの複製なので新API等が増えるとその分修正する必要がある
    • 今はまだシンプルな構成なので可能ですが、マイクロサービス的に役割が違うAPIが増えてくると管理は困難になり得ます。
    • そのため、Cloud Functionのような関数処理がある場合、専用に増やす必要があります
  • develop環境のALBがTerraform管理されており、既存のALBの振り分け設定の棲み分けが必要
    • 完全に設計ミスでプレビュー環境用のALBが必要になります。近日修正予定
  • FEだけの修正なのにBEも構築されてしまう
    • 修正内容のパスを見てBEも構築する必要があるかどうか判定をしても良いなと考えています
  • どうしても構築に時間がかかる
    • 致し方ないのですが、想像以上に構築には時間がかかり、そこの待機時間がボトルネックだなと感じています
    • APIのDocker buildのキャッシュ最適化して工夫する必要がありそう

実際に運用をしてみてどう変わったのか?

これらの仕組みを入れた結果はこちらの記事で詳しく解説をしています。よろしければご覧ください。

https://zenn.dev/pubtech/articles/797c3f7a2ce4b7

参考リソース

パブリックテクノロジーズ

Discussion