PR単位で動作確認がしたい!
始めに
Ubieでプラットフォームエンジニア兼SREをしているonoteruです。
今回は、Ubieで運用しているpreview環境と呼ばれる仕組みについて紹介します。preview環境は、プルリクエストごとに動作確認ができる環境を提供する仕組みです。この環境により、開発者の動作確認のボトルネックを解消し、開発効率の向上を実現しています。
なお、本記事はUbie Tech Advent Calendar 2025 20日目の記事となります。
背景と課題
近年、AIエージェントを活用したコーディング支援ツールが普及し、コードを書くこと自体のハードルは大きく下がってきました。しかし、コードを書くことと動作確認をすることは別の話です。実際の開発現場では、動作確認が開発フローのボトルネックになることが少なくありません。この問題は実際に手で動かして体験を確認する必要のあるような、フロントエンドを持つアプリケーションにおいて顕著に現れます。
具体例を挙げると、STG環境のような共有環境での動作確認を行う場合は、複数の開発者が同じ環境を利用しようとして順番待ちが生じ、結果的にここで時間がかかってしまうケースがあります。ローカル環境で動作確認しても良いのですが、Devinのようなリモートコーディングエージェントを多用する開発スタイルにおいては、それも開発速度を下げる要因になってしまいます。
作成したもの
上記のような課題を解決するため、PRごとに独立した動作確認環境を提供する「preview環境」をプラットフォームとして構築しました。ここで重要なポイントは、特定のアプリケーションのpreview環境をアドホックに作成したのではなく、preview環境を簡単に作成できる仕組み自体を構築したという点にあります。
Ubieではエンドユーザー向けサービスに加え、社内オペレータ向けの管理画面や社内用AIプラットフォームのDev Geniusといった、フロントエンドを持つアプリケーションが多数運用されており、preview環境のような動作確認環境が欲しいという声は各サービスチームから挙がっていました。
それらのリクエストに応じ、その度に各サービスで別々の仕組みを構築してしまうとどうなるでしょう。手間がかかるだけではなく、設定の管理方式やアクセス制御にばらつきが生じ、統制を取るのが難しくなってしまいます。
そこでアプリケーションごとの違いを吸収してpreview環境を立ち上げるための共通の仕組みを用意することで、素早く安全に各チームが運用するアプリケーションをpreview環境に乗せられるようにしました。
これはまさにプラットフォームエンジニアリング的なアプローチと言えます。
preview環境のアーキテクチャ
Ubieではプライバシーやセキュリティの観点からマルチクラスタ構成を採用しており、アーキテクチャとしては複数のマイクロサービスとモジュラモノリスの集合体となっています。詳細なアーキテクチャについては以下の記事が参考になります。
preview環境を作成するにあたり、このマルチクラスタ構成をそのまま拡張してpreviewをデプロイするクラスタを新たに作成する方針を取りました。

preview環境として動くフロントエンドサービスはServer-Side Renderingを行うため、他のクラスタで動作しているバックエンドサービスと通信します。このような通信がサービスメッシュ内で閉じている点が、Vercelのような既存のソリューションと比較して優位な点と言えます。
さらに外部からの通信用にはpreviewクラスタに1つGatewayを作成し、それをpreview環境全体で共有して使う方式にしています。
このpreviewクラスタはqa環境とstg環境のフリートにそれぞれ作成されており、preview環境を作成したいユーザは、作成時にどちらの環境のクラスタにpreviewをデプロイしたいか選択することができます。この構成により、preview環境は独立したクラスタで動作しながらも、実際のバックエンドサービスと連携して動作確認を行うことができます。これにより、ローカル環境では再現が難しい統合テストや、実際のデータを使った動作確認も可能になります。
previewへの通信
preview環境はPRごとの動作確認環境であるため、1つのアプリケーションに対して複数のバージョンのコンテナがデプロイされます。例えば、foobarアプリケーションのPR#100に対応するコンテナ(Pod)と、PR#101に対応するコンテナがデプロイされる、といった具合です。これらのPodは全てpreview用のクラスタに同居することになります。
ではpreview環境のアプリケーションを利用する人は、どのように接続先のPodを選択すれば良いでしょうか。ここには複数の選択肢があります
- ホスト名を使う
- 例: foobar-100.example.comを開くとPR#100に対応したコンテナに繋がり、foobar-101.example.comはPR#101に対応したコンテナに繋がる
- HTTPヘッダを使う
- 例: ホスト名はfoobar.example.comを共有で使い、
ubie-preview: foobar-100をヘッダに付与したリクエストはPR#100に対応したコンテナに繋がる。PR#101も同様
- 例: ホスト名はfoobar.example.comを共有で使い、
今回はHTTPヘッダ方式を採用しました。理由として認証に外部のIDPを使っているサービスがあり、そこで指定するリダイレクトURIを固定する必要があったためです。利用者はpreview作成時に指定すべきヘッダが通知され、それをChrome拡張に指定することで目当てのバージョンのコンテナにアクセスできます。(なお、この拡張は強い権限を持つためセキュリティの観点からVibe Codingで内製しました。)
それぞれのpreview単位で次のようなリソースが作成されます。
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: foobar-preview-100
namespace: jp-foobar-p-stg
spec:
hostnames:
- "foobar.example.com"
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: preview-gateway
namespace: gateway
rules:
- matches:
- headers:
- type: Exact
name: ubie-preview
value: "foobar-100"
backendRefs:
- group: ""
kind: Service
name: foobar-preview-100
port: 80
小ネタではありますがKubernetesのGatewayコントローラは賢く、hostnamesが同じHTTPRouteを複数デプロイした場合、うまくルールをマージしてくれます(Ingressもそうらしい)。一方Ubieでも使っているIstioのVirtualServiceで上記の例と同じことをするとルールの適応順に関して未定義の動作をするようです[1]
さて、preview環境は開発中のものであるためインターネットから直接アクセスできてはいけません。そのためのアクセス制御にはGoogle Cloudではお馴染みのIAP(Identity-Aware Proxy)を利用しています。
IAPを使うため、preview環境の作成時には以下のリソースも同時に作成しています。
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
name: foobar-preview-100
namespace: jp-foobar-p-stg
spec:
default:
iap:
enabled: true
oauth2ClientSecret:
name: iap-secret
clientID: myoauthclientid.apps.googleusercontent.com
targetRef:
group: ""
kind: Service
name: foobar-preview-100
このリソースはこうすることにより、IAPのclient ID/secretを全てのpreview環境で使い回し、手軽にセキュアな環境を実現しています。
preview作成フロー
preview環境の作成は、GitHubのPRにコメントを投稿するだけで行えます。

以下のようなフローでpreview環境が起動します。

- PRにコメントを付与。どの環境を使うかなどを引数で指定できます。webhookでpreviewシステムを起動させるためのCloud Runにリクエストが飛びます。
- Cloud Runが一旦previewサービスの構成情報をfirestoreに保存して終了します。これは時間のかかるコンテナイメージのビルド後に再び利用するためです。
- コンテナイメージビルドがトリガーされ、ビルドごにArtifact Registryにpushされます。
- Artifact Registryはpushを検出してCloud Runに通知を送ります。
- Cloud Runは保存しておいたpreviewサービスの構成情報をfirestoreから取り出し、preview用のKubernetesマニフェストを生成します。基本的にpreviewで選択した環境からコピーして編集します。上述したHTTPRouteなどのマニフェストは新たに生成します。ここでpreview環境固有のConfigMapの値を挿入することもできます。
- GHAが生成されたマニフェストをpreviewクラスタへとapplyします
- GHAが指定すべきHTTPヘッダと共にpreview環境作成の完了を元のPRにコメントします。
このプロセスにより、開発者は追加の設定を行うことなく、既存のサービスの設定をベースにしたpreview環境を簡単に作成できます。また、マニフェストの生成が自動化されているため、設定の一貫性も保たれます。
まとめ
本記事では、Ubieで運用しているpreview環境のアーキテクチャと実装について紹介しました。preview環境は、PRごとに独立した動作確認環境を提供することで、開発者の動作確認のボトルネックを解消し、開発効率の向上に貢献しています。
またpreview環境はURLでアクセス可能なため、成果物をチーム内で簡単に共有でき、認識合わせがよりスムーズになりました。これは、ローカル環境では実現できなかった大きな利点です。
特に、マルチクラスタ構成を活用した設計により、preview環境を独立したクラスタで運用しながらも、実際のバックエンドサービスと連携した動作確認が可能になっています。また、GitHubコメントによる簡単な操作でpreview環境を作成できる仕組みにより、開発者の認知負荷を最小限に抑えています。
今後は、バックエンドAPIサービスへの対応拡大や、より高度な設定オプションの提供など、preview環境の機能拡張を検討していく予定です。
いよいよUbie Tech Advent Calendar 2025もラストスパート!明日はmitsuboshさんの記事、お楽しみに!
Discussion
とても興味深く、また、同様の課題を抱えていたため、参考になりました。ありがとうございます。
自己理解のために質問させてください。
端的に、この仕組み、プレビュー環境というのは、「ステージングやQA相当の『動作確認がシステムレベルで取れる一式』の環境」で、環境をプルリクごとに版数指定で貸与可能とすることで「準本番相当の環境での動作確認が取れた」ことを確認するということで合っていますか?
はい、その理解で合っています
こんにちは!わたしたちも同じようなことをしようとしており、参考になりました。ありがとうございます
よかったら教えていただきたいのですが、DBなどのバックエンドサービスや非同期workerなどってどうされましたか?以下の点が気になっています
・DB: 共用DBだとschema変更などのmigrationができない。preview個別に用意するのは運用が重いし、データの整合性が取れない
・非同期worker: jobを拾いに行くときに、特定のpreview環境のもののみ拾いに行くのが難しそう
アプリケーションサーバ以外のクラウドリソース(DB, queue, object storage)はqaやstgなどのpreviewが接続する環境と同じものを共有して利用しています。
おっしゃる通り、DBではmigrationなどはできません。そういう検証が必要な場合はpreviewではなくstgやqaそのものを使うような運用にしています。
最初から完璧なものを作ろうとすると時間がかかるので一定妥協しているという感じですね