私の弱小ポエムブログには、時折「SEO業者」や「ドメイン買いませんか?」といった海外からの英文スパムが届きます。
…しかし、待ってください。
もしある日突然、私の魂のポエムが全米で大バズりし、「このポエムを映画化したい!」「ポエムの権利を1億円で買わせてくれ!」という海外プロデューサーからの熱烈なオファーが、スパムに紛れて届いていたらどうするでしょうか?
同期処理でサーバーを落として億万長者のチャンスを逃すわけにはいきません。大量の海外コメントを1通も取りこぼさず、ミリ秒単位でGeminiに自動翻訳させて世界へ羽ばたくため、鉄壁の非同期メッセージング基盤を構築しました。
(※今回はブログ本体との結合ではなく、裏で動く「AI翻訳パイプライン(Pub/Sub ✕ Cloud Run)」の仕組みをディープに解説します)
💡 この記事の3行まとめ(忙しい人向け)
- 解決する課題: 外部API呼び出しを含むコメント翻訳機能の同期実行による、レスポンス遅延やスパム集中時のサーバーダウンリスクを解決。
- 採用したアーキテクチャ: Google Cloud Pub/Sub と Docker エミュレータを用いた非同期・イベント駆動型の安全な翻訳メッセージングパイプライン。
- 実証成果: 大量のコメント要求時もバックプレッシャーによりコンシューマーを保護し、システム全体の耐障害性とスケーラビリティを飛躍的に向上。
🏗️ アーキテクチャ概要
🍜 直感理解!ラーメン屋で例えるバックプレッシャー & 「一人二役」の仕組み
今回のアーキテクチャについて **「バックプレッシャー(背圧制御)」**と、作ってて、少しわかりづらいかな?と思った **「1台のCloud Runが受付と翻訳の2役をこなす構造」**を図解で解説します!
1. バックプレッシャー(背圧制御)の有無による違い
大量のコメントが殺到した際、従来の同期処理ではシェフ(サーバー)が即座にパンクして炎上・クラッシュしますが、Pub/Sub + Cloud Run(本構成)では「券売機 兼 待合室」がクッションとなり、シェフが自分のキャパシティ(最大80並列の安全なペース)の範囲内で無理なく丁寧に調理できます。
2. 1台のCloud Runで、「一人二役(送信 & 受信)」のデータフロー
今回の構成では、同じ1つの Cloud Run コンテナが 「受付(Publisher)」と「AI翻訳シェフ(Subscriber)」の2役 を兼任する禁じ手を採用しています。
- Step 1 【コメント受付】: ブログ訪問者から海外コメントの投稿リクエストを受信します。
- Step 2 【トピック投函 (Publish)】: 「Pub/Sub 着ぐるみ」をまとった Cloud Run が、メッセージを Pub/Sub トピックへポンッと投函!
- Step 3 【変身(同じ人!)】: 投函完了後、着ぐるみをサッと脱ぎ捨てて役割チェンジ!(中の人は全く同じ1台のCloud Runコンテナ)
-
Step 4 【Push受取 (Subscribe)】: Pub/Sub の Push Webhook (
POST /translate) から通知が届き、同じ Cloud Run がメッセージをキャッチ! - Step 5 【AI翻訳実行】: 受信したデータを「翻訳依頼」として Gemini 3.7 Flash ロボットに渡し、AI翻訳を完了させます。
💡 前作(170)との違い & 今回あえて1台にまとめている理由
- 前作(170):
0.08 vCPU/--concurrency 1/--max-instances 5(スケールアウト型)- 本作(171):
1.0 vCPU/--concurrency 80/--max-instances 1(背圧・1台固定型)本来であれば受取と処理は分離すべきですが、今回のハンズオンであえて「受付」と「処理」を1台のCloud Runサービス(エンドポイント)に同居させているのは、受取時に起動・ウォームアップされたコンテナインスタンスが再利用される確率を高め、ワーカー側のコールドスタート(起動遅延)を最小化するハンズオン向けの最適化です。
※もちろん Cloud Run は完全なステートレス設計のため、仮にゼロスケール(0台休眠)やインスタンス入替が発生した場合でも、Pub/Sub 側がメッセージを最大7日間確実に保護・再配信するため、新旧どのインスタンスが立ち上がっても安全かつ独立して処理を完遂できます。
※ただし、1台同居では大規模スパイク時に同じ同時実行枠(80件)を奪い合う限界があります。本番環境で大規模システムを構築・運用する際は、必ず「受付用」と「処理用」のCloud Runサービスを別々に分離して構築してください!
イベント駆動型非同期AI翻訳パイプラインの意義
API呼び出しを含む時間のかかる処理を、ユーザーのリクエストを受け付けるWebサーバー内で直接実行する場合、応答の遅延がユーザービリティーを損なう可能性があるため、避けるべきです。
メッセージブローカーを仲介させることで 「時間的デカップリング(時間的疎結合)」 を行う事により、メッセージのパブリッシュ(送信)とサブスクライブ(受信・処理)を完全に非同期化することができます。これにより、仮に後続の処理コンシューマーが停止していても、ユーザーへのレスポンスは即時完了し、メッセージはキューに安全にバッファリングされます。
従来の同期処理 vs Pub/Sub非同期パイプラインの比較
従来の「同期API呼び出し」と、今回の「Pub/Sub + Cloud Run(本構成)」の2つのアプローチを単純に比較してみました!
| 評価軸 | 同期API呼び出し (直列実行) | Pub/Sub + Cloud Run (本構成) |
|---|---|---|
| 基本データモデル | HTTP同期リクエスト/レスポンス | 完全管理イベントメッセージング(Pub/Sub) |
| スケーラビリティ | 極めて低(API応答を待機するため接続が即時詰まる) | 極めて高(Pub/Subから複数コンシューマーへ動的配信) |
| データ永続化 / キュー保証 | なし(呼び出し側のエラーでデータは即時消失) | 高(Pub/Sub内で最大7日間のメッセージ永続化・再試行) |
| 耐障害性 / 疎結合度 | 低(翻訳APIのダウンがメインシステムに直接波及) | 極めて高(メッセージバッファにより後続の障害時もデータ保護) |
| ランニングコスト | 0円(別途熱源コスト不要だがスロットリング損失大) | 極小(定額固定費0円、Pub/SubとCloud Runの無料枠内) |
同期API呼び出しは、一時的なアクセススパイクや翻訳APIの障害により、システム全体のダウンタイムやメッセージ消失を引き起こします。
メッセージをPub/Subで緩衝させ、イベント駆動でCloud Runを起動する 「Pub/Sub + Cloud Run(本構成)」 にしておけば、コストは多少発生する可能性がありますが、安心を買うことができます。
なお、私のポエムブログであれば課金が発生する程のアクセスがまずないので、そもそも通常時は死角無しです😿
🔄 ローカル(Pull)と本番(Push)のハイブリッド処理モデル
このコンシューマープログラムは、開発環境と本番環境で異なるPub/Subメッセージ受信モデルを自動判別します。
- ローカル(エミュレータ環境):
isLocalが真の場合、startConsumer()により Pull 型(イベントをポーリングして購読する常駐ワーカー)で動作し、高速なデバッグと可視化を可能にします。- 本番環境(Cloud Run):
isLocalが偽の場合、無駄なバックグラウンドポーリングによるCPUリソースの浪費(およびCloud Runでの受信凍結)を防ぐため、Pull リスナーを起動しません。代わりに、Pub/Subの Push サブスクリプションからPOST /translateエンドポイントへ送信されるプッシュ通知を待機するステートレスな構成として動作し、Cloud Run のゼロスケール(完全従量課金)のメリットを最大化します。⚠️ ローカル(Pull)と本番(Push)でのデータの流れる向き(矢印)の違い
- ローカル開発(Pull型): 翻訳コンシューマー(App)が「新しいメッセージはある?」と自らエミュレータへ問い合わせに行くため、矢印の向きは
App --> Emulator(引っ張り型)となります。- 本番環境(Google Cloud / Push型): メッセージが届いた瞬間に Pub/Sub が自動で Cloud Run のエンドポイント(
POST /translate)へリクエストを送りつけるため、矢印の向きはPubSub --> Cloud Run(送りつけ型)と逆方向になります。
💻 ローカル開発環境のデータフロー(Pullモデル)
ローカル環境では、コンシューマーが主体となって定期的にエミュレータへ新しいメッセージを問い合わせる「引っ張り(Pull)」方式をとります。ネットワーク設定が不要で、デモ画面から「受信の一時停止」などの制御が簡単に行えるデバッグ優先の構成です。
☁️ 本番環境のデータフロー(Pushモデル)
本番環境では、Google CloudのPub/Subが主体となって、メッセージ受信時にCloud Runの特定のURL(/translate)を強制的に呼び出す「送りつけ(Push)」方式をとります。普段はサーバーを完全に停止(インスタンス数0)させてコストを浮かせ、リクエストが来た時だけ叩き起こすFinOps優先の構成です。
📂 すべての設定ファイルと完全なコードはこちらのGitHubリポジトリで公開しています
👉 github.com/kuiswin/171-pubsub-pipeline
(※本リポジトリのソースコードおよびスクリプトは個人・学習・検証目的でご活用いただけます。商用本番環境での利用は自己責任でお願いします。)
※本リポジトリのコードおよび記事のロジック構築には生成AIを活用しています。動作確認は行っておりますが、AI特有の誤ったコード生成(ハルシネーション)や仕様変更、意図しない誤翻訳等が含まれる可能性があります。本番環境へ適用される場合は、セキュリティやクラウド費用(FinOps)をご自身でご確認の上、自己責任にてご運用ください。
💻 ローカル環境での検証 & 稼働手順
ローカルPCの環境に、以下の構成ファイルを配置して起動確認を行います。
検証に必要なソースコードはすべてGitHub上に公開されています。
- 📄 docker-compose.yml (エミュレータと翻訳コンシューマーコンテナの定義)
- 📄 package.json (Node.js依存ライブラリ定義)
- 📄 index.js (Pub/Subのポーリング、ダッシュボード配信、翻訳ロジック)
- 📄 Dockerfile (翻訳コンシューマー起動用コンテナビルド定義)
- 📄 dashboard.html (メッセージ送受信・滞留キュー可視化WebUI)
📁 フォルダ構成
sandbox_171/
├── docker-compose.yml
└── consumer/
├── Dockerfile
├── dashboard.html
├── index.js
└── package.json
ローカルでの動作確認について
本システムでは、クラウドにデプロイする前に、ローカル環境で「ほぼ同等の挙動」を再現できるよう、Docker Composeを用いたコンテナ型のエミュレータ(Pub/Subエミュレータなど)を組み合わせて検証を行います。これにより、主要機能を安全にテストできます。
GitHub上に公開されているソースコードを一つずつコピーしてフォルダに配置し、Docker環境を手動で立ち上げることで動作確認自体は可能ですが、ファイル構成の作成や依存関係のインストール、環境変数のマッピング設定を手動で行うのは少々手間(不便)がかかります。
記事後半で紹介する私の技術ブログで、これらの依存パッケージのインストール、リポジトリのクローン、そしてローカル環境でのコンテナ起動までを「一発」で自動完了させる、超便利なワンライナーコマンドと自動セットアップ手順を公開しています!
より簡単かつスピーディーにローカルでの動作確認を行いたい方は、ぜひ私の個人ブログをご参照ください。
開発環境へのアクセス情報
コンテナが起動したら、ブラウザから以下のURLへアクセスします。
-
ローカル環境(ご自身のPCなど)で動かす場合:
- AI自動翻訳ダッシュボード: http://localhost:8080/
- ※ブラウザでアクセスすると、トピック送受信ステータスや翻訳結果がリアルタイムで一覧表示されるダッシュボード画面が表示されます。
-
【検証用】筆者の自動公開デモ環境:
-
筆者のプライベート環境では、コンテナを立ち上げるだけで自動的にドメインとSSLが割り当てられる検証環境を用意しています。以下のURLから動作検証が可能です。
-
AI自動翻訳ダッシュボード: https://translate-p8086-171.kuis.win/
-
コンテナ環境は常に立ち上げているわけではないため、アクセス時につながらない可能性があります。
-
📦 コンテナ内部の構成
今回の構成(Docker環境)内では、以下の3つのDockerコンテナが協調して動作しています。
-
pubsub-emulator【公式イメージ / 無改造】:-
イメージ:
gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators - 役割: Google Cloud SDKのPub/Subエミュレータ。ローカルでPub/Subメッセージのルーティングを行います。
-
イメージ:
-
pubsub-init【初期化用 (transient) / 完了後自動終了】:-
イメージ:
gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators -
役割: エミュレータの起動を待ち、必要なトピック (
verify-topic) とサブスクリプション (verify-sub) を自動で事前作成して終了するセットアップコンテナ。
-
イメージ:
-
consumer【独自開発 / アプリ・Web UI本体】:-
イメージ:
Dockerfileよりローカルビルド - 役割: メッセージを受信して自動双方向翻訳を実行し、可視化 Web ダッシュボードを提供するNode.jsアプリケーション本体(ポート 8080)。
-
イメージ:
💡 Pub/Subの基本概念:手紙の郵送システムで例えると
Pub/Sub(Publish / Subscribe)の仕組みは、手紙の郵送システムに例えると非常にイメージしやすくなります。
-
Publisher(パブリッシャー / 差出人) 【ブラウザ / クライアント CLI】
- 手紙(メッセージ)を書いて送る人です。
-
Topic(トピック / 郵便ポスト) 【
pubsub-emulator内に作成】- 手紙の宛先となる場所です。
-
Subscription(サブスクリプション / 配達方法) 【
pubsub-emulator内に作成】- 届いた手紙を、誰にどうやって届けるかという「配送ルール(ルート)」です。
-
Subscriber(サブスクライバー / 受取人) 【
consumer】- 最終的に手紙を受け取り、翻訳などの処理を行う人です。
💡 コラム:ローカル開発環境で「エミュレータ・初期化・アプリ」の3コンテナに分離する理由
アプリ層(consumer)としては受付と翻訳を1つにまとめていますが、ローカルのDocker環境としては「エミュレータ」「初期化処理」「アプリ本体」の3つのコンテナに明確に分離しています。すべてを1つのコンテナに詰め込む「Fat Container」にせず分離しているのには、モダンなコンテナ設計における重要な思想があります。
-
「1コンテナ=1プロセス(単一責任の原則)」の徹底
Dockerは本来、1つのプロセスを隔離された環境で実行するために設計されています。1つのコンテナで複数のプロセスを同時に管理しようとすると、プロセスの監視やゾンビプロセスの回収が困難になり、コンテナの健全性(Health Check)を正しく判定できなくなります。 -
リソース制御(CPU/メモリ)の最適化
メッセージブローカー(エミュレータ)とコンシューマー(アプリケーション)を分けることで、Docker Compose 上で各コンテナに対して個別にdeploy.resources.limitsを設定し、リソースの競合によるシステム全体のクラッシュを防ぐことができます。 -
公式イメージの「無改造利用」による透明性とセキュリティの確保
公式イメージ(例:Google Cloud CLIのエミュレータ)は、開発元のコミュニティが最適化したものをそのまま使うのが鉄則です。初期化のためにシェルを埋め込んだり、ベースイメージをカスタマイズしたりすると、バージョンアップ時の追従が困難になり、セキュリティリスクも高まります。 -
「冪等(べきとう)な初期化処理」の分離(Init Container パターン)
トピックやサブスクリプションの作成といった「起動時に1回だけ実行したい処理」をアプリケーションやブローカーの起動スクリプトに混ぜるのではなく、独立した軽量コンテナ (pubsub-init) として切り出しています。これらは**冪等(繰り返し実行しても同じ結果になる)**に作られており、必要なセットアップが完了すると即座に正常終了(Status 0でExit)します。データベースで言えば、テーブル作成CREATE TABLEと同じで、最初に1回だけ箱を作れば十分な処理です。
🚀 次のステップ:試験環境と本番環境に展開
ローカル環境での動作準備を完了させて、本番環境にもデプロイメントを実施させます。
設計思想や、一発自動デプロイスクリプトの実装方法については、記事の後半で紹介する個人ブログ側で公開していますのでアクセスしてみてください!
👉 【後編:Google Cloud本番デプロイ編】本番一発自動デプロイとFinOpsコスト防衛手順はこちら(ブログ記事リンク)