Google Cloud での Rails 8: ゼロから AI へ

1. はじめに

Google Cloud 上の Rails 8 ワークショップへようこそ。このハンズオン Codelab では、シンプルなローカル SQLite ベースラインから、Google Cloud 上のプロダクション グレードのエンタープライズ対応リファレンス アーキテクチャまで、最新の Rails 8 アプリケーションを移行します。

このワークショップでは、アプリをデプロイするだけでなく、意見の分かれる本番環境グレードのクラウド モダナイゼーション(リフト&シフトを正しく行う)を実行します。

一般的な「自分のマシンで動作する」ローカル設定(エフェメラル ディスク、埋め込み SQLite、インプロセス ジョブ)から始めて、Google Cloud 上のエンタープライズで堅牢なクラウド アーキテクチャに段階的に進化させます。

  1. 🐘 ローカル SQLite → Cloud SQL(PostgreSQL): 再起動時に DB が消去されることはなくなりました。Cloud SQL Auth Proxy(mTLS、パブリック IP なし)を介して安全に接続されたマネージド PostgreSQL に移行します。
  2. 🪣 エフェメラル ローカル ストレージ → プライベート GCS: サーバーレス ディスクは一時的なものです。IAM 署名付き URL(iam: true)を使用して、ActiveStorage をプライベート Google Cloud Storage に接続します。公開バケットや漏洩したキーはありません。
  3. 🚀 インプロセス ジョブからマルチコンテナ Cloud Run へ: Rails モノリスとサーバーレス クラウドを橋渡しします。標準の Docker Compose を使用して、Solid Queue を専用のワーカー コンテナとして Puma とともに Cloud Run マルチコンテナ Pod で実行します。マイクロサービスのスプロールやスレッドの枯渇は発生しません。DHH も満足するでしょう。

🍌 2026 年に生きているため、Google Antigravity を使用してスタック全体を強化します。これは後付けではなく、このワークショップをより効果的に(そして楽しく)実施するためのプロンプトと スキルが用意されています。

🏛️ ターゲット リファレンス アーキテクチャ(パターン 1: クリーンなフラット ベクター エンタープライズ)

次に、構築してデプロイする標準の Google Cloud リファレンス アーキテクチャ(バリアント 1: クリーンなフラット ベクター エンタープライズ)を示します。これは、すべてのマネージド Google Cloud 永続性と AI サービスに接続された本番環境のマルチコンテナ Cloud Run サービスを示しています。

バリエーション 1: クリーンなフラット ベクター エンタープライズ - Google Cloud 上の Rails 8 のアーキテクチャ

🎞️ アーキテクチャの進化: ゼロからクラウドネイティブへ

各ワークショップのマイルストーンで、エフェメラルな単一マシンのベースラインからフルマネージドの Google Cloud ブループリントにスタックが段階的に最新化される様子をご覧ください。

ワークショップのアーキテクチャの進化

学習内容

  • Google Antigravity とペアプログラミングして、Rails 8 と Google Cloud の謎を解き明かす方法。
  • 自動リリース前診断(just workshop-test)を実行する方法。
  • google プロバイダを使用してブロックすることなくローカル開発を継続しながら、Terraform を使用して Google Cloud クラウド インフラストラクチャを非同期でプロビジョニングする方法。
  • セキュリティ アンチパターンを排除する方法: 0.0.0.0/0 を開く代わりに、非公開の GCS バケット(iam: true)と Cloud SQL Auth Proxy mTLS トンネルを使用します。
  • Google Cloud Secret Manager からシークレットを直接挿入する方法。
  • Solid Queue を介して非同期の生成 AI バックグラウンド ジョブ(NanoBanana カバー生成ツール、バイリンガル ポッドキャスト合成)をオーケストレートする方法。

では始めましょう。

2. ステップ 0: 前提条件、Antigravity の設定、請求の確認

💡 シナリオ: あなたとチームは、ミッション クリティカルな Rails 8 アプリケーションを構築しています。コードに触れたり、クラウド リソースを起動したりする前に、ツールチェーンを確立し、Google Antigravity を接続して、Google Cloud の認証情報とお支払いの基盤を確認する必要があります。

1. 前提条件のチェックリスト

始める前に、使用する環境内に以下のツールが揃っていることを確認してください。

  • just(1.21 以降):(just --version)タスク ランナー - このワークショップのすべてのコマンドはjustレシピです(AI ハーネスにインストールを依頼するか、skills/rails8app-workshop をご覧ください)。
  • Git(2.30 以降): バージョン管理、ブランチ、リポジトリのクローン作成に(git --version)を使用します。
  • Google Cloud SDK(gcloud CLI): インストール済みで最新の状態であること。
  • Terraform CLI(1.5 以降): 宣言型インフラストラクチャ プロビジョニング用。
  • docker & docker-compose: ローカルにインストールして実行されています。
  • Ruby 3.4.5:(ruby -v - blog/.ruby-version によって固定されています。AI ハーネスに 3.4.5 のインストールを依頼してください)。Rails 8 は Gemfile で宣言され、bundle install によって自動的にインストールされます。
  • Google Antigravity 2.0: 自律型 AI ペア プログラミング アシスタント(Google Antigravity 2.0 をダウンロード)。個人の Gmail でログインします。API トークンは必要ありません。

2. Google Cloud 認証、専用構成、ADC

既存の企業、個人、マルチ アカウントの gcloud 設定との競合を避けるため、このワークショップ専用の名前付き構成を作成することを強くおすすめします。

# 1. Create and activate dedicated workshop configuration (Fixes Issue #38)
gcloud config configurations create rails8-on-gcp-workshop --activate 2>/dev/null || \
  gcloud config configurations activate rails8-on-gcp-workshop

# 2. Authenticate your account and Application Default Credentials (ADC)
gcloud auth login $GOOGLE_CLOUD_ACCOUNT
gcloud auth application-default login

# 3. Configure active project and region
gcloud config set project $GOOGLE_CLOUD_PROJECT
gcloud config set compute/region europe-west1

💡 専用構成が必要な理由gcloud config configurations create rails8-on-gcp-workshop を使用すると、このワークショップ専用にすべての CLI 設定(アカウント、割り当てプロジェクト、デフォルト リージョン)が分離されます。完了したら、gcloud config configurations activate default を使用していつでも通常のセットアップに戻すことができます。

アクティブな gcloud 構成

3. 🚨 GCP 請求の確認

⚠️ 注: Google Cloud SQL と Cloud Run のデプロイには、有効なリンクされたお支払いアカウントまたは有効なワークショップ教育クレジットが必要です。設定が正しいことを確認しましょう。これを行うには、課金確認チェックを実行します。

gcloud beta billing projects describe $GOOGLE_CLOUD_PROJECT

billingEnabled: true が返されることを確認します。すべての請求先アカウントを一覧表示することもできます。

gcloud billing accounts list

少なくとも 1 つに OPEN: True が表示されていることを確認します。有効なアカウントがない場合は、Google Cloud コンソールの [お支払い] ページで請求先アカウントをリンクするか、ワークショップ クレジット クーポンを利用します。

有効な GCP 請求先アカウント

4. リポジトリのクローンを作成して Antigravity とペア設定する

リポジトリのクローンを作成し、ディレクトリに入ります。main に完全に留まっていることに注意してください。

git clone https://github.com/palladius/rails8-app-on-gcp.git
cd rails8-app-on-gcp

このディレクトリを Google Antigravity で開きます。Antigravity はリポジトリを自動的に検査し、AGENTS.md を読み取って、ペア プログラマーとして待機します。

5. 🪫 Antigravity の無料クレジットがなくなった場合

⚠️ 今すぐ利用した Cloud クレジットでは、Antigravity は補充されません。これらは 2 つの別々の請求対象です。Antigravity の割り当ては Google AI プラン(Google One)に設定され、バウチャーと Cloud クレジットは Cloud 請求先アカウントに設定されます。請求先アカウントを GCP プロジェクトにリンクしても、Antigravity プロンプトが 1 つ追加されることはありません。

エージェントが割り当てメッセージで停止した場合は、この手順に沿って対応します。最初の 2 つは無料で即座に実行できます。

  1. モデルプールを切り替えます。割り当てはモデル ファミリーごとに追跡され、Antigravity には 2 つの独立したプール(Gemini モデルと Claude/GPT モデル)が表示されます。Claude モデルと GPT-OSS モデルは無料枠で利用できるため、Gemini プールが枯渇しても、他のプールはそのまま残ることがよくあります。プロンプト ボックスの下にあるモデルセレクタを使用します。反重力モデル セレクタ - Gemini、Claude、GPT-OSS プールを切り替えます
  2. Pro ではなく Flash モデルを選択します。レート制限は、エージェントがリクエストごとに実行する作業量と相関関係があるため、軽量モデルでは残りの作業量が伸びます。
  3. 独自の Gemini API キーで Antigravity CLI を使用します。Bring-your-own-key は Antigravity IDE ではサポートされていません。CLI が代替手段としてドキュメントに記載されています。Google AI Studio で無料のキーを作成し(プロジェクトは必要ですが、請求先アカウントは不要)、Antigravity CLI をダウンロードして構成します。
  4. お待ちいただくか、アップグレードしてください。無料枠の割り当ては毎週更新されます(エラー メッセージにリセット日が記載されています)。Google AI Pro/Ultra は、パソコン版アプリ内でベースラインを引き上げる唯一の方法です。これらのプランは 5 時間ごとに更新され、超過分に購入した AI クレジットを使用できます。
  5. これは Antigravity ワークショップですが、ほとんどの AI コーディング ハーネスも動作するはずです。リポジトリの AGENTS.md ディレクトリと skills/ ディレクトリをポイントしてください。

6. 自動化されたステップ 0 の検証

ステップ 0 の環境が評価スイートに 100% 準拠していることを確認します。

just workshop-eval 0

✨ 驚きの瞬間: Antigravity とワークショップ評価エンジンにより、CLI バージョン、Ruby 環境、ADC 認証が 2 秒以内に検証されます。

3. ステップ 1: Terraform インフラストラクチャのキックオフと事前診断

💡 戦略: Google Cloud SQL PostgreSQL などのマネージド データベースのプロビジョニングには、約 10 ~ 12 分かかります。後で待機するのではなく、ローカルで開発している間に、Terraform を使用して不変のインフラストラクチャをバックグラウンドで今すぐ起動します。

1. 自動事前フライト診断スイートを実行する

クラウド インフラストラクチャを起動する前に、包括的なプリフライト テストスイートを実行します。

just workshop-test

このスクリプト(bin/workshop_diagnostics.rb):

  • GOOGLE_CLOUD_ACCOUNT ID 構成を検証します。
  • 有効な課金とプロジェクトのリンクを確認します。
  • Vertex AI のアプリケーションのデフォルト認証情報(ADC)を検証します。
  • ActiveStorage カナリア シードイメージ(blog/app/assets/images/gcs_dev_image.jpg)を確認します。

ワークショップ テストの診断出力のみ

.env がない場合は、ドキュメント化されたテンプレートからコピーします。

cp .env.dist .env
# Edit .env and configure GOOGLE_CLOUD_ACCOUNT with your Google/Gmail account

💡 ヒント: just workshop-test から、プロジェクトで課金 API が無効になっているというレポートが届いた場合は、次の方法で簡単に有効にできます。

gcloud services enable cloudbilling.googleapis.com

2. ⏱️ Terraform インフラストラクチャを非同期で起動する

ワンステップの Terraform レシピを実行します。これにより、リモート バックエンド状態バケットが作成され、インフラストラクチャのデプロイがトリガーされます。

just terraform-apply

Terraform の初期化が成功しました

手動による代替方法(または `iac/bin/terraform-apply.sh` を直接実行)

# Fall back to active gcloud config if environment variables are unset
GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project)}"
GOOGLE_CLOUD_REGION="${GOOGLE_CLOUD_REGION:-europe-west1}"

# Create remote state bucket idempotently
gcloud storage buckets create "gs://${GOOGLE_CLOUD_PROJECT}-tfstate" --location="$GOOGLE_CLOUD_REGION" 2>/dev/null || \
  gcloud storage buckets describe "gs://${GOOGLE_CLOUD_PROJECT}-tfstate" >/dev/null

cd iac
terraform init -backend-config="bucket=${GOOGLE_CLOUD_PROJECT}-tfstate"
terraform apply -auto-approve -var="project_id=${GOOGLE_CLOUD_PROJECT}" -var="region=${GOOGLE_CLOUD_REGION}"
cd ..

Terraform がプロビジョニングするもの:

  • Google Cloud Storage バケット: プライベート アクセスと IAM 認証情報の署名(iam: true)を使用して作成されます。
  • カナリア テスト画像: gcs_dev_image.jpg をバケットにアップロードして、エンドツーエンドの BLOB 検証を有効にします。
  • Google Cloud SQL PostgreSQL インスタンス: バックグラウンド プロビジョニングを開始します(約 10 ~ 12 分)。

Google Cloud コンソールの Cloud SQL PostgreSQL インスタンス

💡 注: 自動バックアップがないという警告は無視してかまいません。このワークショップのデータベースでは必要ありません(ただし、自動化された毎日のバックアップは、実際の本番環境データベースでは不可欠なベスト プラクティスです)。

Terraform apply が正常に完了し、出力が表示された

📝 Cloud Run の URL をメモします。Terraform の適用が成功し、インフラストラクチャの出力が表示されます。cloud_run_url をメモします(またはクリップボードにコピーします)。これは、後のステップでライブサービスを検証する際に必要になります。

3. 自動化されたステップ 1 の検証(省略可)

ステップ 1 の前提条件と構成が合格していることを確認します。

just workshop-eval 1

⏱️ 注: この評価では、ライブ クラウド リソースがチェックされるため、完了までに数分かかることがあります。これは完全に任意です。ターミナルで実行している間、待たずに次のステップにすぐに進むことができます。

4. ステップ 2: ローカル ベースライン、Mailpit、管理者のオンボーディング

出発点は、SQLite、Mailpit メール インターセプト、ディスクベースの ActiveStorage を使用して localhost で実行されるクリーンでモダンな Rails 8 ブログ アプリケーションです。

1. アプリをローカルで起動する

📁 ディレクトリに注意してください。このリポジトリには、blog/ の Rails アプリと iac/ の Terraform が含まれています。ステップ 1 でリポジトリのルートに移動したため、ステップ 2 のすべてのコマンドはルートから実行されます。

blog/ - Gemfile、bin/rails、compose.yaml はすべてここに存在します。ルートから実行すると、no configuration file provided: not found または Gemfile がないというエラーが発生します。

Rails アプリに移動し、固定された Ruby を使用していることを確認してから、gem をインストールしてデータベースを設定します。

cd blog
ruby -v          # must print 3.4.5 — see Step 0 if it does not
bundle install
bin/rails db:setup

🧯 への書き込み権限エラー

/var/lib/gems/

または

ruby -v

システム Ruby(3.3.x など)が表示されるバニラ Linux システムの Ruby は .ruby-version ファイルを完全に無視し、gem をシステム全体にインストールしようとします。アクティブなターミナル シェルに rbenv が読み込まれていることを確認します。

eval "$(rbenv init - bash)"   # or: eval "$(rbenv init - zsh)"
ruby -v                       # confirm it now outputs 3.4.5!

注: ステップ 0 で 3.4.5 のインストールをスキップした場合は、rbenv install 3.4.5 を実行します。または、以下のモード A(Docker Compose)(just compose-up)を使用します。これにより、ホスト gem に触れることなく、固定された Ruby スタックがコンテナ内で完全に実行されます。

💡 考えるのが面倒ですか?リポジトリ ルートの just install と just compose-up は、cd blog を実行します。

2. 🌱 スマート シードの自動検出と管理者のブートストラップ(問題 #21 と #25)

データベース シード(db/seeds.rb)には、スマート環境の自動検出機能があります。

  • アクティブなデータベース アダプタ(SQLite と Postgres)とストレージ構成を検査します。
  • ステージ 0(ローカルホスト)を検出し、最初の管理者ユーザーを自動的に作成して、教育用投稿をシードします。
    • [LOCAL BASELINE] Welcome to Rails 8 on Localhost!
    • ローカル ディスク ストレージが一時的なものであることを示す透かし入りのローカル sad 画像添付ファイル(local_sad_image.png)がデフォルトで用意されています。
  • ActionMailer 経由でパスワードの再設定メールが自動的にトリガーされます。

シードを確認または再実行します(.env の GOOGLE_CLOUD_ACCOUNT が自動的に使用されます)。

bin/rails db:seed    # or from repo root: just seed

Docker Compose を使用してローカル開発スタックを起動します。

docker compose up -d     # from blog/ — or `just compose-up` from the repo root

Docker Compose で Rails、Mailpit、ローカル スタックを起動する

アプリケーション、ローカル Mailpit SMTP サーバー、Adminer データベース ビューアが、分離されたコンテナで同時に実行されるようになりました。

  • Rails アプリ: http://localhost:3000
  • Mailpit ウェブ UI: http://localhost:8025
  • Adminer DB UI: http://localhost:8081

🧯 ポート 3000 の競合または

server.pid

エラーが発生しましたか?以前にネイティブ開発サーバー(bin/dev)を起動した場合は、Ctrl+C で停止します。Docker がポート 3000 がすでに割り当てられていると報告したり、server.pid の残骸が見つかったりした場合は、次のコマンドでクリーンアップします。

just compose-down
rm -f blog/tmp/pids/server.pid
just compose-up

3. Mailpit のエクスペリエンスと管理者ログイン

  1. Mailpit で送信メールをキャッチする: ブラウザで http://localhost:8025 を開きます。ローカルの受信トレイを確認してください。db:seed の間、ActionMailer は管理者のオンボーディング メールを送信しました。Mailpit は外部メールサーバーや認証情報に触れることなく、ローカルで安全にキャプチャしました。

傍受したパスワードの再設定メールを表示する Mailpit ウェブ UI 2. ブログにログインする: ブラウザで http://localhost:3000 を開きます。

  • Mailpit のメール内のパスワードの再設定リンクを直接クリックして、パスワードを設定できます。
  • または、Google Cloud アカウントのメールアドレス(.env)とデフォルトのシード パスワード(Ch4ng3m3!!1)を使用してログインします。
  1. ビジュアル テレメトリー バッジを確認する:
    • UI に黄色の環境バナーとバッジ(Notice: Ephemeral Database Active (POSTGRESQL) と [EPHEMERAL DB / STORAGE] 💾 Local)が表示されます。
    • 投稿の透かし: 🏠 スタンプ(カバー画像の右下にある nanobanana_stamp_local.png)。これにより、アセットとデータベースが現在エフェメラル ローカル ストレージにバインドされていることをすぐに視覚的に確認できます。

エフェメラル テレメトリーの通知とバッジが表示されているローカルブログのホームページ

💡 省略可: Rails コンソール ワークアウト Rails が CLI からシードされたデータをどのように操作するのか知りたいですか?インタラクティブ コンソールに移動します。

bin/rails console

環境変数を使用して、シードされた管理者ユーザーを動的に検査します。

user = User.find_by(email_address: ENV.fetch("GOOGLE_CLOUD_ACCOUNT"))
# Or simply grab the first user:
user = User.first
puts "Admin email: #{user.email_address} (created via: #{user.created_via})"
exit

4. 自動化されたステップ 2 の検証

ローカル ベースラインと管理者の設定を確認します。

just workshop-eval 2

workshop-eval 2 の検証出力のみ

5. 注意点: ステートレス コンテナ

Cloud Run コンテナはステートレスでエフェメラルです。SQLite データベースとローカル storage/ ディレクトリを Cloud Run に直接デプロイすると、コンテナがゼロにスケールされるか、再起動されるたびに、すべての投稿とアップロードされた画像が完全に消去されます。

次のステップでは、このエフェメラル構成を Cloud Run にデプロイして、ステートレス ショックを実際に体験します。

5. ステップ 3: デプロイ 1 - ステートレス ショック(3 分で早期の WOW を実現)

クラウド データベースで 15 ~ 20 分待ってからウェブで確認するのではなく、最新のサーバーレス開発は、単一コンテナの Rails アプリケーションを 3 分以内に Google Cloud Run に直接デプロイするという早期の成功から始まります。

1. タイムマシンの巻き戻し

開始構成が 100% エフェメラル(ローカル SQLite データベースとローカル ディスク ファイル ストレージ)であることを保証するには、Zero-Branch Time-Machine エンジンを使用します。

just workshop-rewind 1

💡 何が行われたのでしょうか?workshop-rewind 1 は、main ブランチを離れることなく、stage-1-stateless 構成オーバーレイを blog/config/ に適用しました。アプリは、コンテナ ディスク上の SQLite とローカル ファイル システム上の ActiveStorage で構成されています。

2. Cloud Run への単一コンテナ Puma のデプロイ

ソースコードから Cloud Run に直接デプロイします。Modern Rails 8 は、公式のプロダクション レディ(な) Dockerfile を自動的に生成します(マルチステージ ビルド、jemalloc、非 root セキュリティ)。blog/ フォルダからデプロイすると、Google Cloud Build はこのネイティブ Dockerfile を直接検出し、マネージド サーバーレス サービスをプロビジョニングします。

# Ensure you are inside the Rails application directory
cd blog

# Ensure default region is set
export GOOGLE_CLOUD_REGION="europe-west1"

# Deploy single-container service from source (with dummy key fallback if starting without credentials)
gcloud run deploy blog \
  --source . \
  --region $GOOGLE_CLOUD_REGION \
  --allow-unauthenticated \
  --set-env-vars GOOGLE_CLOUD_ACCOUNT=$GOOGLE_CLOUD_ACCOUNT,SECRET_KEY_BASE_DUMMY=1

次のような進行状況の出力が表示されます。

gcloud run deploy building and deploying container

⏳ Cloud Build がコンテナをパッケージ化し、Cloud Run リビジョンをプロビジョニングしている間、数分かかります。

🧯 トラブルシューティングと一般的な注意点

PERMISSION_DENIED: Build failed because the default service account is missing required IAM permissions

? 2 つの異なる原因で同じエラーが発生します。

  1. gcloud で API が有効になりました。デプロイで The following APIs are not enabled ... cloudbuild.googleapis.com が出力され、Y と回答した場合、新しい権限の伝播が完了する前にビルドが開始された可能性があります。gcloud 独自のプロンプトで「これには数分かかります」と警告されます。1 分待ってから、まったく同じコマンドを再実行します。通常は成功します。通常、ステップ 1 の terraform apply でこの API が有効になるため、プロンプトは表示されません。
  2. デフォルトの Compute SA にロールがありません。最新の GCP プロジェクトでは最小権限が適用されるため、ソースのデプロイが中断されます。次のコマンドで修正します。
    just project-status   # grants storage.admin, logging.logWriter, artifactregistry.writer, cloudbuild.builds.builder
    
    just workshop-test もこの情報をレポートするようになったため、デプロイが失敗し続ける場合は再実行する価値があります。

デプロイ中:

  1. Cloud Run が Rails コンテナ イメージをビルドします。
  2. 公開された安全な TLS ドメイン https://blog-[hash]-[region].a.run.app が割り当てられます。
  3. トラフィックがポート 8080 の Puma にルーティングされ始めます。

3. ✨ 早期の WOW モーメント

生成された Cloud Run URL をブラウザで開きます。

  1. これで、最新の Rails 8 アプリケーションが Google Cloud で稼働しました。
  2. 管理者認証情報(GOOGLE_CLOUD_ACCOUNT と APP_ADMIN_PASSWORD)でログインします。ログインボタンが見つからない場合は、[新しい投稿] をクリックすると、ユーザー名とパスワードの入力が求められます。Rails 8 のログインページに一時的なテレメトリー バナーが表示されている
  3. [新しい投稿] をクリックし、「My First Cloud Run Post」というタイトルの記事を作成して、写真を添付し、[投稿を作成] をクリックします。
  4. 投稿が完全な書式設定で公開され、画像がレンダリングされます。
  5. ビジュアル テレメトリーに注目してください。
    • ヘッダー バッジ: [EPHEMERAL DB / STORAGE] 💾 Local
    • 画像の透かし: 🏠 スタンプ(右下の 127.0.0.1 エフェメラル ディスク バッジ)。

🐝 ライブ ワークショップ Hive リーダーボードに参加しましょう!オンラインで、試験監督がリーダーボードを表示していて、それに参加したい場合は、Cloud Run URL をこちらに追加してください。👉 リーダーボードに Cloud Run を登録する

Google Cloud Run コンソール ブログ サービスのリビジョンの詳細

4. 💥 注意点: ステートレス ショックと「Puma ワークアラウンド」の落とし穴

Cloud Run は、ステートレスのサーバーレス プラットフォームです。ウェブ トラフィックがゼロになると、Cloud Run はコンテナ インスタンスをゼロにスケールダウンしてコストを節約します。新しい HTTP リクエストが届いたとき、または新しいコンテナ リビジョンがデプロイされたとき、Cloud Run は新しいクリーンなコンテナ イメージを起動します。

スタックしたジョブのバナー

単一コンテナ デプロイで投稿を作成したり、画像を添付したりすると、Rails は ActiveJob タスク(画像ディメンション分析やメタデータ インデックス作成など)をキューに入れます。しかし、バックグラウンド ワーカーを実行しているユーザーがいないため、警告バナーが表示されます。

通知: 現在 8 個のバックグラウンド ジョブが実行待ちです

魅力的な解決策: Puma 内で Solid Queue を実行する

賢いデベロッパーなら、次のように言うでしょう。「待って!Rails 8 では、Puma ウェブサーバー プロセス内で Solid Queue を直接実行できます。

SOLID_QUEUE_IN_PUMA=true

環境変数です。"

Cloud Run でこの回避策をテストしてみましょう。

# Force a revision update to enable Solid Queue inside Puma
gcloud run services update blog \
  --region $GOOGLE_CLOUD_REGION \
  --update-env-vars SOLID_QUEUE_IN_PUMA=true

ブラウザに戻り、ページを更新します。

💥 ステートレス ショック:

  1. 朗報: Solid Queue ワーカーが Puma 内でアクティブになりました。新しいジョブはすぐにドレインされます。
  2. 冷水シャワー(落とし穴):
    • Cloud Run が新しいリビジョンをデプロイしたため、以前のコンテナ インスタンスが置き換えられました。
    • 作成した記事とディスク上の SQLite データベース ファイルが完全に消去されました。
  3. アーキテクチャの教訓: Puma 内でバックグラウンド ワーカーを実行すると、貴重なウェブスレッドの CPU/RAM が消費され、永続性も解決されません。

5. 自動化されたステップ 3 の検証

Cloud Run のデプロイとアラート処理を確認します。

just workshop-eval 3

✨ レッスン: SOLID_QUEUE_IN_PUMA などの単一コンテナの回避策はローカル開発には便利ですが、最新のクラウド アーキテクチャでは次のようになります。

  • メディア ファイルには Google Cloud Storage(GCS)が必要です(ステップ 4)。
  • リレーショナル データとジョブキューには、マネージド Google Cloud SQL が必要です(ステップ 5)。
  • 負荷の高いバックグラウンド ワーカーは、専用のサイドカー コンテナ(ステップ 6)に配置します。

6. ステップ 4: デプロイ 3 - GCS 永続ストレージと POLA 警告

このステップでは、ActiveStorage を Google Cloud Storage(GCS)に切り替え、IAM Credentials API(iam: true)を介して有効期間の短い署名付き URL を使用することで、メディアとファイル ストレージをコンテナ ディスクから切り離します。

構成を変更する前に、Rails が GCS と通信するために必要な 3 つの要素を理解しておきましょう。

① プロジェクト ID + バケット名 - Rails は、BLOB を保存する場所を認識する必要があります。アプリは環境から GOOGLE_CLOUD_PROJECT を読み取り、バケット名を ${GOOGLE_CLOUD_PROJECT}-activestorage-prod として導出します。ステップ 1 の Terraform でこのバケットはすでに作成されているため、追加の作業は必要ありません。

② 適切な権限を持つサービス アカウント - Cloud Run は Compute Engine のデフォルトのサービス アカウント(PROJECT_NUMBER-compute@developer.gserviceaccount.com)として実行されます。この SA には、バケットに対する次の 2 つの IAM ロールが必要です。

  • roles/storage.objectAdmin - blob のアップロードと提供
  • 自身に対する roles/iam.serviceAccountTokenCreator - IAM Credentials signBlob API を呼び出して、有効期間の短い署名付き URL を生成する

⚠️ 魅力的なショートカット —

public: true GCS を最も迅速に動作させる方法は、storage.yml で public: true を設定し、バケットに対する allUsers:objectViewer を付与することです。画像はすぐに読み込まれ、ログインは不要です。この操作は行わないでください。一般公開バケットでは、アップロードされたユーザー メディアがすべてオープン インターネットに永久に漏洩します。このブループリントでは、iam: true を使用してバケットを 100% 非公開にしています。すべてのイメージ URL は、IAM Credentials API によってオンザフライで生成される署名付きの有効期限付きトークンです。

③

ACTIVE_STORAGE_SERVICE=google - ローカル ディスクから GCS に Rails を実行時に切り替える単一の環境変数。コードの変更やアプリケーション ロジックの再デプロイは不要で、環境変数を切り替えるだけです。

1. タイムマシンの巻き戻し

タイムマシンをステージ 2 に進めます。

just workshop-rewind 2

blog/config/storage.yml を調べると、アプリが 3 つの名前付き GCS 環境(dev / test / prod)を定義しており、すべて同じ安全なパターンを使用していることがわかります。

<% gcp_project = ENV.fetch("GOOGLE_CLOUD_PROJECT") { Rails.application.credentials.dig(:gcs, :project) } %>
<% gcs_signer_sa = ENV.fetch("GCS_SIGNER_SA_EMAIL") { "rails-cloudrun-sa@#{gcp_project}.iam.gserviceaccount.com" } %>

google_prod: &google_prod
  service: GCS
  project: <%= gcp_project %>
  bucket: <%= gcp_project %>-activestorage-prod  # ← derived from project ID, no extra env var!
  iam: true        # Sign URLs via IAM Credentials signBlob API (no private key JSON required!)
  gsa_email: <%= gcs_signer_sa %>

# Convenience alias — ACTIVE_STORAGE_SERVICE=google points here
google:
  <<: *google_prod

GCS IAM 署名アーキテクチャ

2. IAM Storage と署名権限の付与

Cloud Run ランタイム サービス アカウントに、URL に署名してオブジェクトをアップロードする権限があることを確認します。

export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
export RUN_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
export GCS_BUCKET="${GOOGLE_CLOUD_PROJECT}-activestorage-prod"

# Grant Storage Object Admin
gcloud storage buckets add-iam-policy-binding gs://$GCS_BUCKET \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/storage.objectAdmin"

# Grant Service Account Token Creator (required for IAM signed URLs)
gcloud iam service-accounts add-iam-policy-binding $RUN_SA \
  --member="serviceAccount:$RUN_SA" \
  --role="roles/iam.serviceAccountTokenCreator"

3. 3 番目のデプロイ(Cloud Run、GCS 接続)

GCS を有効にして再デプロイします。blog/ フォルダ内にいることを確認します(そうしないと、Cloud Run は Rails 8 Dockerfile ではなく Buildpacks を使用してビルドを試み、ビルドが失敗します)。

既存の環境変数(マスターキー、IAP 構成など)を保持するには、--update-env-vars(--set-env-vars ではない)を使用します。

cd blog
gcloud run deploy blog \
  --source . \
  --region $GOOGLE_CLOUD_REGION \
  --update-env-vars ACTIVE_STORAGE_SERVICE=google

💡 ヒント: デプロイが失敗した場合は、ターミナル出力に表示される Cloud Build ログのリンクをクリックします(下の図を参照)。Antigravity にエラーの解読と修正を依頼することもできます。

ターミナル出力の Cloud Build 失敗ログのリンク

4. ✨ 生存画像とクラウド スタンプ

  1. ブログの URL を更新します。
  2. 「クラウドを生き抜く」というタイトルの新しいブログ投稿を作成し、写真をアップロードします。
  3. 画像の右下を確認します。
    • 🏠 スタンプがなくなりました。
    • カラフルな Cloud GCS スタンプ(nanobanana_stamp_cloud.png)に置き換えられます。
  4. 別のコンテナの再起動を強制します(これにより、4 回目のリビジョンがトリガーされます)。
    gcloud run services update blog --region $GOOGLE_CLOUD_REGION --update-env-vars RESTART_TRIGGER=$(date +%s)
    
  5. ページを更新します。SQLite データベースがリセットされている間も、画像バイナリは Google Cloud Storage に安全に保存されています。CLI で確認します。
    gcloud storage ls -l -r gs://$GCS_BUCKET/
    

本番環境バケット(gs://$GCS_BUCKET)の Google Cloud Storage ブラウザ コンソールに表示される可能性のある、アップロードされた BLOB とメディア オブジェクトのリストは次のとおりです。

永続イメージ BLOB を示す Google Cloud Storage コンソールのバケットの詳細

5. ⚠️ 注意: スタックしたジョブの警告バナー

画像をアップロードすると、ActiveStorage は分析ジョブ(ActiveStorage::AnalyzeJob)をキューに追加して、ディメンションとメタデータを抽出します。

ただし、Cloud Run は現在 1 つのウェブ コンテナ(puma)のみを実行しています。Rails 8 では、Solid Queue がバックグラウンド ジョブをデータベースに保存しますが、bundle exec rails solid_queue:start を実行するユーザーはいません。

ブログページの最上部を見ると、blog/app/views/layouts/_check_stuck_jobs.html.erb によってレンダリングされた明るい警告バナーが表示されます。

この問題は、ステップ 6 で適切に解決します。ここでは、ウェブ トラフィックとは別に Solid Queue を実行する専用の worker コンテナを備えた、本番環境グレードのマルチコンテナ サイドカー アーキテクチャに移行します。

6. 自動化されたステップ 4 の検証

GCS 構成とスタックしたジョブのテレメトリーを確認します。

just workshop-eval 4

7. ステップ 5: 事前チェックリスト: Cloud SQL、シークレット、DB 接続

ステップ 3 と 4 でステートレス コンテナと Google Cloud Storage を習得している間に、Terraform はバックグラウンドで本番環境のバックエンドの準備を完了しています。

このステップは、3 つのコンテナからなる本番環境のサイドカー アーキテクチャ(ステップ 6 のデプロイ 4)をデプロイする前に、データベースの健全性、接続性、シークレット アクセスを検証するための高速な事前フライト検査として機能します。

0: ブートストラップ環境変数

必要なすべてのシェル変数を Terraform 出力から直接エクスポートします。

# Core project variables (should already be set from Step 0)
export GOOGLE_CLOUD_REGION="${GOOGLE_CLOUD_REGION:-europe-west1}"

# Compute runtime service account
export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
export RUN_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"

# Cloud SQL instance name (from Terraform output, NOT hardcoded)
export SQL_INSTANCE_NAME=$(cd iac && terraform output -raw sql_instance_name 2>/dev/null || gcloud sql instances list --format='value(name)' --limit=1)

# Database password (from Terraform output)
export DB_PASSWORD=$(cd iac && terraform output -raw db_password 2>/dev/null || echo "CHANGE_ME")

# GCS bucket names
export GCS_BUCKET="${GOOGLE_CLOUD_PROJECT}-activestorage-prod"

echo "✅ Pre-flight environment variables ready:"
echo "   RUN_SA=$RUN_SA"
echo "   SQL_INSTANCE_NAME=$SQL_INSTANCE_NAME"
echo "   GCS_BUCKET=$GCS_BUCKET"

1. Cloud SQL ヘルスチェック

マネージド PostgreSQL インスタンスの初期プロビジョニングが完了し、正常な状態であることを確認します。

echo "Checking instance: $SQL_INSTANCE_NAME"
gcloud sql instances describe $SQL_INSTANCE_NAME --format="value(state)"

出力は RUNNABLE である必要があります。

2. (省略可)ノートパソコンからデータベース接続をテストする

本番環境では、Cloud Run コンテナは、データベースを一般公開することなく(0.0.0.0/0)、暗号化されたサイドカー プロキシ(127.0.0.1:5432)を介して Cloud SQL に安全に接続します。

ℹ️ 注: この手順は完全に省略可能です。Cloud Run では、公式の Cloud SQL Auth Proxy サイドカー コンテナが mTLS 接続を自動的に処理します。ワークショップを完了するために、ローカルに何かをインストールする必要はありません。

マシンに cloud-sql-proxy と psql がインストールされていて、ノートパソコンからの直接接続を確認する場合は、次の操作を行います。

# Optional: test connectivity via Google-managed mTLS proxy
gcloud sql connect $SQL_INSTANCE_NAME --user=rails_user --database=rails_production

(パスワードの入力を求められたら、

$DB_PASSWORD

。それ以外の場合は、下のステップ 3 に直接進んでください)。

3. Terraform でプロビジョニングされたシークレットの検査

プレーン テキストのデータベース パスワード、API キー、Rails マスターキーを git やコンテナ環境変数に保存しないでください。最新のクラウド アーキテクチャでは、ゼロトラスト ランタイム インジェクションに Google Cloud Secret Manager を使用します。

ℹ️ Terraform がステップ 1 で処理しました:

  1. rails-master-key - 実行時に Rails 認証情報を復号化します。
  2. rails-db-password - Cloud SQL ユーザーと一致する暗号学的に安全なランダム パスワード。
  3. rails-admin-password - 最初の管理者ユーザーの安全なパスワード。

Secret Manager にシークレットが存在し、アクティブであることを確認します。

gcloud secrets list --filter="name:rails-"

構成されたアプリケーション シークレットの Google Cloud Secret Manager コンソール リスト

🔑 master.key をノートパソコンに同期する(ローカル同期)

blog/config/master.key はセキュリティ上の理由から gitignore されているため、ローカル クローンにまだ存在しない可能性があります。次の使いやすい自己修復スニペットを実行して Secret Manager から取得し、ローカルの Rails ツールで認証情報を復号できるようにします。

if [ ! -f blog/config/master.key ]; then
  echo "📥 Pulling rails-master-key from Secret Manager to local blog/config/master.key..."
  gcloud secrets versions access latest --secret=rails-master-key > blog/config/master.key
  echo "🔑 Created local blog/config/master.key! 🎉"
else
  echo "✅ Local blog/config/master.key already exists! 🚀"
fi

🧪 Playground: カスタム シークレットを作成して、その簡単さを試してみませんか?Secret Manager は Rails 専用ではありません。次の簡単な CLI コマンドで Secret を作成できます。

echo -n "MySuperSecretValue123!" | gcloud secrets create workshop-fun-secret --data-file=-
echo "✨ Created workshop-fun-secret in Secret Manager! 🪄"

4. Secret アクセサーの権限を確認する

Cloud Run コンテナがこれらのシークレットを環境変数またはボリューム マウントとして起動時にマウントするには、ランタイム サービス アカウント($RUN_SA)に各シークレットに対する roles/secretmanager.secretAccessor ロールが必要です。

Terraform はステップ 1 でこの権限をすでに付与しています。rails-master-key の IAM ポリシー バインディングを確認します。

# Verify that Cloud Run runtime SA has Secret Accessor role
gcloud secrets get-iam-policy rails-master-key \
  --flatten="bindings[].members" \
  --format="table(bindings.role,bindings.members)" \
  --filter="bindings.members:$RUN_SA"

以下のように表示されます。

ROLE                                 MEMBERS
roles/secretmanager.secretAccessor   serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com

Cloud SQL Auth Proxy のセキュリティの比較

5. 自動プリフライト検証

自動化されたステップ 5 のテストスイートを実行して、データベースが実行可能で、シークレットが空ではなく、IAM 権限が緑色であることを確認します。

just workshop-eval 5

8. ステップ 6: デプロイ 4 - エンタープライズ マルチコンテナ サイドカー(ゴールド スタンダード)

これで、究極のリファレンス アーキテクチャである Multi-Container Cloud Run を組み立てます。

1. 判断基準の復元

リポジトリ構成を正規の main 状態に復元します。

just workshop-restore-gold

2. 3 コンテナのプロダクション ブループリントを調べる

blog/compose.prod.yaml を開き、アーキテクチャを確認します。

Cloud Run マルチコンテナの本番環境アーキテクチャ

graph LR
  Client[Internet / HTTPS Client] -->|Port 8080| Web[Web Container: Puma]
  Web -->|Localhost:5432| Proxy[Sidecar: Cloud SQL Auth Proxy]
  Worker[Worker Container: Solid Queue] -->|Localhost:5432| Proxy
  Proxy -->|IAM mTLS Tunnel| CloudSQL[(Google Cloud SQL PostgreSQL)]

本番環境のデプロイでは、同じローカル ネットワークを共有する 3 つの連携コンテナが実行されます。

  1. web: Puma を介してポート 8080 で HTTP トラフィックを処理します。
  2. worker: bundle exec rails solid_queue:start を実行する専用のバックグラウンド処理コンテナ。
  3. cloudsql-proxy: 127.0.0.1:5432 にバインドされた公式の Google Cloud SQL Auth Proxy サイドカー(gcr.io/cloud-sql-connectors/cloud-sql-proxy:2)。

3. Cloud Run ジョブを使用したデータベース移行の実行

ウェブ トラフィックを新しいデプロイに転送する前に、Cloud SQL に対してデータベースの移行と初期シードを実行する必要があります。

⚖️ コンテナで移行を実行する方法: 2 つの戦略

コンテナ化されたクラウド環境でデータベース移行を実行する一般的な方法は 2 つあります。

  • オプション A: Docker エントリポイント内の自動(コンテナの起動時に entrypoint.sh が db:migrate を実行)
    • メリット: 完全に自動化されているため、デベロッパーはコマンドの実行を忘れる心配がありません。
    • デメリット: サーバーレス自動スケーリングでは危険です。トラフィックが多いときに Cloud Run が同時に 10 個のインスタンスにスケールアップすると、10 個のコンテナが同時に移行を実行しようとして、データベースのロック、スキーマの競合状態、ブートのタイムアウトが発生します。
  • オプション B: Cloud Run ジョブによる明示的な方法(アーキテクチャの選択)
    • メリット: 完全な制御、トランザクションの安全性、競合状態の排除。ウェブ トラフィックの処理から完全に切り離された専用の 1 回限りのコンテナ タスクとして実行されます。移行が失敗しても、ウェブ サービングは影響を受けません。
    • デメリット: デプロイ前に CLI ジョブまたは CI/CD ステップをトリガーする必要があります。

本番環境の信頼性を最大限に高めるため、オプション B を選択します。

⚠️ 重大: DATABASE_URL 形式と Ruby 3.4.5 RFC 3986 の厳密性

Cloud Run ジョブは、ネイティブの Unix ソケット プロキシ(/cloudsql/PROJECT:REGION:INSTANCE)を使用して Cloud SQL に接続します。DATABASE_URL は、トリプルスラッシュ形式を使用する必要があります。

postgresql:///rails_production?user=rails_user&password=PASS&host=/cloudsql/PROJECT:REGION:INSTANCE
# Core environment variables for database migration & initial admin seed
export GOOGLE_CLOUD_ACCOUNT="${GOOGLE_CLOUD_ACCOUNT:-$(gcloud config get-value account)}"
echo "Admin user will be seeded for: $GOOGLE_CLOUD_ACCOUNT"

# Get the latest blog image from Cloud Run (jobs don't support --source)
export BLOG_IMAGE=$(gcloud run services describe blog --region=$GOOGLE_CLOUD_REGION --format='value(spec.template.spec.containers[0].image)')

# Build the DATABASE_URL in triple-slash format (required for Unix socket proxy)
export DB_PASSWORD=$(gcloud secrets versions access latest --secret=rails-db-password)
export CLOUDSQL_CONNECTION="${GOOGLE_CLOUD_PROJECT}:${GOOGLE_CLOUD_REGION}:${SQL_INSTANCE_NAME}"
export DB_URL="postgresql:///rails_production?user=rails_user&password=${DB_PASSWORD}&host=/cloudsql/${CLOUDSQL_CONNECTION}"

# Create migration job — note ALL 4 database URL env vars and GOOGLE_CLOUD_ACCOUNT for seed user creation
gcloud run jobs create rails-migrate \
  --image=$BLOG_IMAGE \
  --command "bin/rails" \
  --args "db:prepare" \
  --set-env-vars="DATABASE_URL=${DB_URL},DATABASE_QUEUE_URL=${DB_URL},DATABASE_CACHE_URL=${DB_URL},DATABASE_CABLE_URL=${DB_URL},GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT},GOOGLE_CLOUD_ACCOUNT=${GOOGLE_CLOUD_ACCOUNT}" \
  --set-secrets="RAILS_MASTER_KEY=rails-master-key:latest" \
  --set-cloudsql-instances="${CLOUDSQL_CONNECTION}" \
  --service-account=$RUN_SA \
  --region $GOOGLE_CLOUD_REGION 2>/dev/null || true

# Execute migration job
gcloud run jobs execute rails-migrate --region $GOOGLE_CLOUD_REGION --wait

💡 Rails 8 マルチデータベース: アプリは database.yml ごとに 4 つのデータベース(プライマリ、キュー、キャッシュ、ケーブル)を使用します。4 つの DATABASE_*_URL 環境変数はすべて同じ Cloud SQL インスタンスを指している必要があります。そうでない場合、Solid Queue/Cache/Cable テーブルは作成されません。

db:prepare の後、Solid Queue/Cache/Cable スキーマを読み込みます。

# Schema load for queue, cache, and cable databases (creates solid_queue_jobs, etc.)
gcloud run jobs update rails-migrate \
  --command "bash" \
  --args "-c,DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bin/rails db:schema:load:queue db:schema:load:cache db:schema:load:cable" \
  --region $GOOGLE_CLOUD_REGION

gcloud run jobs execute rails-migrate --region $GOOGLE_CLOUD_REGION --wait

gcloud run jobs executions describe JOBNAME の出力上の図は、 の出力例を示しています。

gcloud run jobs executions describe $JOBNAME

rails-migrate ジョブが成功したことを示す Google Cloud Run Jobs コンソールGoogle Cloud Run ジョブ コンソール

rails-migrate

実行が成功した

4. 4 回目のデプロイ: Docker Compose を使用してマルチコンテナ Cloud Run をデプロイする

次に、compose.prod.yaml から本物の 3 コンテナの本番環境スタック(web、worker、cloudsql-proxy)を直接デプロイします。

# 1. Self-healing environment exports (guarantees no missing master key or DB password)
export RAILS_MASTER_KEY="${RAILS_MASTER_KEY:-$(cat config/master.key 2>/dev/null || gcloud secrets versions access latest --secret=rails-master-key)}"
export DB_PASSWORD=$(gcloud secrets versions access latest --secret=rails-db-password)
export CLOUDSQL_INSTANCE_CONNECTION_NAME="${GOOGLE_CLOUD_PROJECT}:${GOOGLE_CLOUD_REGION}:${SQL_INSTANCE_NAME}"

# 2. Database credentials & GCS attachments
export DB_USER="rails_user"
export DB_NAME="rails_production"
export GCS_BUCKET="${GCS_BUCKET}"
export ACTIVE_STORAGE_SERVICE="google"
export GOOGLE_CLOUD_ACCOUNT="${GOOGLE_CLOUD_ACCOUNT:-$(gcloud config get-value account)}"
export GEMINI_API_KEY="${GEMINI_API_KEY:-dummy-key}"

# 3. Deploy the full multi-container sidecar architecture to Cloud Run
gcloud run compose up compose.prod.yaml \
  --region=$GOOGLE_CLOUD_REGION \
  --allow-unauthenticated

3 つのサイドカー コンテナが表示されている Google Cloud Run コンソールの [コンテナ] タブGoogle Cloud Run コンソール: 3 つのサイドカー コンテナすべてを表示する [コンテナ] タブ(

cloudsql-proxy

、

web

、

worker

)

💡 UI の好奇心 / コンソールの不具合: コンソールのプルダウンで、Cloud Run が最上位のエントリを Ingress container: cloudsql-proxy とラベル付けしていることに気づくかもしれません。ご安心ください。これは、コンテナの依存関係の順序付けが原因で発生する Google Cloud コンソールの視覚的な UI の不具合(#149 で追跡)です。実際には、HTTP ポート 8080 をリッスンして Ingress ウェブ トラフィックの 100% を処理しているのは web コンテナのみであり、cloudsql-proxy はポート 5432 でプライベート PostgreSQL トラフィックのみを厳密に処理しています。

5. ✨ 驚きの瞬間: 警告なしでクラウドの完全な永続性を実現!

深呼吸をして、画面を見てください。警告バナーがすべて消えています。🪄

警告なしの Cloud Run と Cloud Persistent Post に関するプロダクション ブログ本番環境の Rails 8 ブログ: 完璧な UI、警告バナーの非表示、永続的な Cloud SQL 状態!

アプリケーションは正式に本番環境グレードのクラウドネイティブになりました。

  • 安全な mTLS トンネリングを介して、マネージド Google Cloud SQL(PostgreSQL)によってバックアップされます。
  • IAM BLOB 署名を使用して、非公開の Google Cloud Storage にメディアを安全に保存します。
  • エフェメラル ストレージの警告とジョブの停止アラートがゼロ。

Cloud Run の URL を開きます。

  1. テレメトリー バッジを確認します。
    • データベース シードで [CLOUD PERSISTENT 🐘 ☁️] が検証されました。
    • フッターの Google Cloud Run バッジと GCS バッジの両方が緑色に光ります。
    • スタックしたジョブの警告バナーが完全に削除されました。
  2. ブログ投稿とコメントを作成する。
  3. 何度でも再起動または再デプロイできます。データ、投稿、コメント、アセットは Cloud SQL と GCS に永続的に保存されます。

6. 自動化されたステップ 6 の検証

マルチコンテナ構成を確認します。

just workshop-eval 6

9. ステップ 7: 生成 AI パイプライン、Podcastifier、GCS トレジャー ハント 🏴‍☠️

専用コンテナで Solid Queue が実行され、Google Cloud Storage がアクティブになっているため、非同期の生成 AI を活用できます。

1. NanoBanana ヴィンテージ カバー生成ツール(すぐに使用できる生成 AI パイプライン)

NanoBanana のマスコット

幸いなことに、この機能はすでにコードベースに完全に実装されています。コードを記述する必要はありません。ここでは、非同期クラウド GenAI パイプラインをトリガーして動作を確認します。

🧠 舞台裏の仕組み

  • 認証: API キーは不要です。Cloud Run ランタイム サービス アカウントを介して、アプリケーションのデフォルト認証情報(Vertex AI では roles/aiplatform.user)を自動的に使用します。(または、GEMINI_API_KEY が指定されている場合は、Gemini API を直接呼び出します)。
  • パイプライン: カバー画像なしで記事が作成されると、after_commit コールバックが GenerateCoverImageJob を Solid Queue にキューに追加します。
  • モデル: Google のマルチモーダル モデル(Gemini 2.5 Flash Image / Vertex AI の Imagen)に、1960 年代のイタリア映画のポスター(「Locandina di un film 1960」)を生成するようプロンプトを送信します。ポスターには、カメオのバナナと光沢のあるルビーの「8」が描かれています。
  • 動的出所透かし: libvips を使用して、隅にビジュアル バッジを動的にスタンプします(GCS のカラフルな雲のスタンプ)。
  • Turbo Stream Hot-Swap: 画像が添付されると、Turbo::StreamsChannel.broadcast_refresh_to(post) は手動でページを再読み込みすることなく、読者のブラウザをライブで更新します。

NanoBanana AI 非同期アーキテクチャのシーケンス図

📐 Mermaid シーケンス図のソースを表示する

sequenceDiagram
  autonumber
  actor User as 👤 Reader / Author
  participant Web as 🌐 Web (Puma)
  participant DB as 🐘 Cloud SQL (Solid Queue)
  participant Worker as ⚙️ Worker (Solid Queue)
  participant Vertex as 🧠 Vertex AI (Gemini Flash / Imagen)
  participant GCS as ☁️ Google Cloud Storage

  User->>Web: POST /posts (Title & Body, No Cover)
  Web->>DB: 1. Save Post & Enqueue GenerateCoverImageJob
  Web-->>User: Redirect to Post (turbo_stream subscribed)

  Worker->>DB: 2. Dequeue GenerateCoverImageJob
  Worker->>Vertex: 3. Prompt for 1960s Italian Poster (ADC auth)
  Vertex-->>Worker: 4. Raw Image Buffer
  Worker->>Worker: 5. Stamp Provenance Watermark (libvips)
  Worker->>GCS: 6. Attach Blob via ActiveStorage (IAM signBlob)
  Worker->>Web: 7. Turbo::StreamsChannel.broadcast_refresh_to(post)
  Web-->>User: ⚡ Live UI Hot-Swap: Poster magically appears!

🧪 ライブで試す

トリガーする方法は 2 つあります。

  • オプション A(ウェブ UI): [New post] をクリックし、「Ruby on Rails 8 を使用したサーバーレス アーキテクチャ」などのタイトルを入力して、コンテンツを記述します。カバー画像ファイルの入力は空のままにします。[投稿を作成] をクリックします。
  • オプション B(CLI ランナー): ターミナルから実行します。
    bin/rails runner 'Post.create!(title: "Serverless Architecture with Ruby on Rails 8", body: "Exploring asynchronous AI pipelines with Solid Queue and NanoBanana!")'
    

15 ~ 30 秒ほど待ってから、Cloud Run で Solid Queue ワーカーのログを確認します。ビンテージ ポスターが投稿に直接表示されます。

📸 TODO(riccardo): AI で生成された 1960 年代のイタリア映画のポスター(カメオ出演のバナナとルビー 8 が特徴)を含むブログ投稿のスクリーンショットを追加する

2. バイリンガル ポッドキャスト化ツール クエスト(反重力コーディングの実践演習)

⚠️ 重要な注意事項: 上記のカバー ジェネレータとは異なり、この機能はまだ実装されていません。

blog/app/jobs/podcastifier_job.rb を開くと、空のスキャフォールド スタブが表示されます。

class PodcastifierJob < ApplicationJob
  queue_as :default

  def perform(post_id)
    # TODO(student): COMPLETE_ME — Pair program with Google Antigravity to implement
    # Italian TTS podcast generation using voice 'it-IT-Wavenet-A' via Cloud TTS / ADC!
  end
end

このハンズオン演習の目的は、Google Antigravity とペアプログラミングを行い、次の機能を完成させることです。

  1. プロンプト:「app/jobs/podcastifier_job.rb で PodcastifierJob を実装するのを手伝って。投稿を取得し、Google Cloud Text-to-Speech(ADC 認証情報を使用して言語コード「it-IT」の音声「it-IT-Wavenet-A」)を使用してイタリア語の音声概要を翻訳または合成し、ActiveStorage を使用して post.podcast_audio_it に音声ファイルを添付する必要があります。」
  2. ビューにプレーヤーを追加: app/views/posts/show.html.erb で、@post.podcast_audio_it.attached? のときに HTML5 プレーヤーを追加します。
  3. 合成をトリガー: 保存すると、Solid Queue はテキスト読み上げジョブをバックグラウンドで非同期的に実行します。

Podcastifier のバイリンガル UI とデュアル オーディオ プレーヤー

🎧 Podcastifier の出力例:

💡 サポートが必要な場合や、行き詰まった場合完全で実戦テスト済みのリファレンス ソリューションは、ブランチ solutions/podcastifier で入手できます。git diff solutions/podcastifier を使用していつでも検査できます。

3. [省略可] 🏴‍☠️ GCS 宝探し(コンソール Blob 復元)

ℹ️ 前提条件の確認: ローカル

cloud-sql-proxy

CLI が必要 このオプションの演習では、暗号化された mTLS トンネルを介してローカル ノートパソコンを Google Cloud SQL に直接接続します。CLI プロキシがインストールされているかどうかを確認します。

which cloud-sql-proxy
  • インストールされている場合: 以下の手順に進みます。
  • インストールされておらず、このクエストを実行したい場合: 次のコマンドでインストールします。
    # Linux (x86_64)
    curl -o /tmp/cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.14.0/cloud-sql-proxy.linux.amd64
    chmod +x /tmp/cloud-sql-proxy && sudo mv /tmp/cloud-sql-proxy /usr/local/bin/cloud-sql-proxy
    # macOS (Homebrew)
    brew install cloud-sql-proxy
    
  • それ以外の場合: ステップ 7 の自動検証に進んでください。

コンテナの再起動によってエフェメラル SQLite データベースが消去される前に、ステップ 4 でアップロードした写真のことを覚えていますか?その画像ファイルは、プライベート GCS バケットに「孤立した BLOB」として安全に保存されています。

Rails コンソールを使用して、この投稿を復元し、Cloud SQL の投稿に関連付けます。

# Connect to your production database via Rails console and Cloud SQL Proxy
cloud-sql-proxy --port 5432 ${GOOGLE_CLOUD_PROJECT}:${GOOGLE_CLOUD_REGION}:${SQL_INSTANCE_NAME} &
DATABASE_URL=postgresql://rails_user:${DB_PASSWORD}@127.0.0.1:5432/rails_production bin/rails c

Rails コンソール内:

# 1. Search for orphaned GCS blobs in ActiveStorage
orphan_blobs = ActiveStorage::Blob.where.missing(:attachments)
puts "🏴‍☠️ Found #{orphan_blobs.count} orphaned GCS blobs!"

# 2. Attach the surviving blob to your latest post
if orphan_blobs.any?
  post = Post.last
  post.cover_image.attach(orphan_blobs.first)
  post.save!
  puts "🎉 Rescued blob #{orphan_blobs.first.filename} attached to '#{post.title}'!"
end
exit

📸 TODO(riccardo): GCS Treasure Hunt スニペットを実行し、孤立した ActiveStorage BLOB を復元するインタラクティブ Rails コンソールのスクリーンショットを追加

ブログを更新します。ステップ 4 のステートレス ショックでアップロードした写真が復活し、Cloud SQL の投稿に永続的に添付されます。

4. 自動化されたステップ 7 の検証

生成 AI のジョブとアセットを確認します。

just workshop-eval 7

10. ステップ 8: 挑戦したい分野を選ぶ / 上級クエスト 🏆

これで標準リファレンス アーキテクチャを習得できました。卒業クエストを選択しましょう。

🛡️ クエスト 1: ゼロトラスト Google IAP(Identity-Aware Proxy)

  • 難易度: 中(エンタープライズ セキュリティ)
  • 目標: 確認済みの Google アカウントまたは Gmail アカウントのみがアクセスできるように Cloud Run アプリケーションをロックダウンし、パスワード ログインを完全に排除します。
  • 仕組み:
    1. 外部 HTTPS アプリケーション ロードバランサは、トラフィックが Cloud Run に到達する前に Google OAuth を終了し、ID を検証します。
    2. Google は、確認済みの ID ヘッダー(X-Goog-Authenticated-User-Email)を転送します。
    3. Rails は、blog/app/controllers/concerns/iap_authenticatable.rb を介して確認済みの Google ID に自動的にログインします。
      module IapAuthenticatable
        extend ActiveSupport::Concern
        included { before_action :authenticate_via_iap }
      
        private
        def authenticate_via_iap
          return unless (raw = request.headers["X-Goog-Authenticated-User-Email"]).present?
          email = raw.sub(/^accounts\.google\.com:/, "")
          user = User.find_or_create_by!(email_address: email) { |u| u.password = SecureRandom.hex(16) }
          start_new_session_for(user) unless authenticated?
        end
      end
      
    4. iac/iap.tf で enable_iap = true を使用して Terraform モジュールを有効にし、許可する Google アカウントを指定します。
      iap_allowed_users = [var.google_cloud_account]
      

IAP バナーで認識されたログイン

📊 クエスト 2: 本番環境の SRE テレメトリーと Cloud Logging

  • 難易度: 中(オブザーバビリティ)
  • 目標: トレース相関 ID を含む構造化 JSON アプリケーション ログを Google Cloud Logging に直接ストリーミングし、Cloud Error Reporting を使用して本番環境の例外をリアルタイムでキャッチします。
  • 仕組み:
    • GCP トレースラベルを含む構造化 JSON ログを出力するように blog/config/environments/production.rb を構成します。
    • 意図的にテストの例外をトリガーし、Google Cloud Error Reporting が瞬時にグループ化して通知する様子を確認します。

📸 TODO(riccardo): グループ化された本番環境の例外を示す Google Cloud Error Reporting と、トレースラベルを含む Cloud Logging の構造化 JSON ペイロードのスクリーンショットを追加

🧠 Quest 3: pgvector セマンティック検索と Gemini RAG

  • 難易度: 難しい(生成 AI の最終評価)
  • 目標: Cloud SQL の PostgreSQL に保存されているベクトル エンベディングを使用して、記事を概念的に検索します。
  • 仕組み:
    • Cloud SQL PostgreSQL インスタンスで vector 拡張機能を有効にします。
      CREATE EXTENSION IF NOT EXISTS vector;
      
    • blog/Gemfile に neighbor gem を追加し、Post#after_save で Gemini(text-embedding-004)を使用してテキスト エンベディングを生成します。
    • コサイン距離クエリ(<=>)を実行して、Turbo Streams でセマンティック検索バーを強化します。

📸 TODO(riccardo): ライブブログのセマンティック検索バーがコサイン類似度スコアで概念的な一致を返すスクリーンショットを追加

⚡ クエスト 4: SEO とパフォーマンスの監査アシスタント

  • 難易度: 簡単 / 楽しい(デベロッパー エクスペリエンス)
  • 目標: Antigravity ツールと Speedgrapher ツールを使用して、Core Web Vitals、Largest Contentful Paint(LCP)、Flesch-Kincaid / Fog の読みやすさ指標を監査します。

📸 TODO(riccardo): Speedgrapher / Lighthouse の SEO と Core Web Vitals の監査概要のスクリーンショットを追加

🤔 好奇心ボックス: Cloud Run でライブ Rails コンソールを取得する

💡 質問: 従来の VM または Heroku スタイルの PaaS プラットフォームでは、デベロッパーは一般的に実行中のインスタンスに SSH 接続するか、heroku run rails console を使用して本番環境データをライブで検査または修正します。Cloud Run ネイティブの同等の機能は何ですか?また、どのように強化しますか?

理由

gcloud run exec

は Cloud Run サービスには存在しません

Cloud Run はサーバーレス プラットフォームです。コンテナは、安定した ID やアドレス指定可能なシェルを持たない、ゼロ、1 つ、または数百の同一インスタンスとして実行されることがあります。これは、特定の Pod にアタッチ可能な名前付きソケットがある Kubernetes(kubectl exec)とは根本的に異なります。

# ❌ This does NOT exist for Cloud Run Services:
gcloud run services exec blog --region europe-west1 -- rails console

# ✅ This exists for Cloud Run JOBS (non-interactive, one-shot tasks):
gcloud run jobs execute my-job --region europe-west1 --wait

4 つの GCP ネイティブ パターン

パターン

コマンド

インタラクティブですか?

セキュリティ

1. ローカル プロキシ + リモート DB

DATABASE_URL=... rails console

✅ フル

🔒 Cloud SQL Auth Proxy mTLS

2. Cloud Run ジョブ(1 回限りのジョブ)

gcloud run jobs execute --wait

❌ スクリプトのみ

🔒 IAM ロールが必要

3. Web Console gem + IAP

ブラウザ → /rails/console

✅ フル

🛡️ IAP ゼロトラスト

4. Cloud Shell + Proxy

Cloud Shell → bin/rails c

✅ フル

🔒 Google ID

パターン 1(データ検査に推奨): Cloud SQL Auth Proxy を介して Cloud SQL PostgreSQL と直接通信するローカル Rails コンソールを実行します。

# In one terminal: start the auth proxy
cloud-sql-proxy rails8-fl-20260907b-xm8ko4:europe-west1:your-instance

# In another terminal: open a read-write or sandbox console
DATABASE_URL=postgresql://rails_user:${DB_PASSWORD}@127.0.0.1:5432/rails_production \
  bin/rails console --sandbox

ネットワークを公開することなく、IRB の完全なインタラクティブ性を実現します。Cloud SQL Proxy は、Unix ドメイン ソケットまたはループバックを介して mTLS を使用します。

パターン 3(ウェブ コンソール + IAP ゼロトラスト、強化されたブラウザ パス):

web-console gem は、ブラウザベースの IRB セッションを提供します。保護されていない場合、これは重大なセキュリティの脆弱性となります。一般公開しないでください。ただし、Google IAP(Quest 1)と組み合わせると、攻撃対象領域が縮小します。許可リストに登録された検証済みの Google ID のみが /rails/console エンドポイントにアクセスできます。

# Gemfile — only load in development/staging, never production without IAP!
gem "web-console", group: :development

# config/environments/production.rb — if IAP is enabled:
config.web_console.allowed_ips = ["0.0.0.0/0"]  # IAP terminates auth before us

⚠️ パターン 3 では、最初に Quest 1(IAP)を有効にする必要があります。これがないと、公開 URL の /rails/console はサーバー室の鍵を開けたままにするのと同じです。IAP を使用すると、Google グレードのゼロトラストによって強化された強力なオペレーター ツールになります。

Antigravity に質問する: 「

rails console --sandbox

Cloud SQL Auth Proxy を使用して、Cloud SQL 本番環境データベースに対して安全に実行します。」

🏆 The Hive での卒業: 試験監督による検証済みの Proof-of-Work

選択したクエストを完了したら、The Hive Leaderboard でステップ 8 の卒業トロフィー 🏆 を受け取ります。

  1. GitHub でクエストを送信する:
    • 公式リポジトリで新しい Issue を開きます: github.com/palladius/rails8-app-on-gcp/issues/new
    • タイトルの形式: 🎓 [Step 8 Completed] :
    • 本文には、達成したことの簡単な説明と、ライブ Cloud Run デプロイへのリンクを含めます。
  2. Cloud Run アプリを問題に接続する:
    • 問題番号(88 など)または完全な URL(https://github.com/palladius/rails8-app-on-gcp/issues/88 など)をメモします。
    • STEP_8_GHI 環境変数を Cloud Run サービスに挿入します。
      gcloud run services update blog \
        --region europe-west1 \
        --update-env-vars STEP_8_GHI=<YOUR_ISSUE_NUMBER>
      
    • /status.json エンドポイントでクエストが報告されることを確認します。
      curl -s https://<YOUR_APP>.run.app/status.json | jq .quest
      
      次のように表示されます。
      {
        "step_8_completed": true,
        "ghi_issue": 88,
        "ghi_url": "https://github.com/palladius/rails8-app-on-gcp/issues/88"
      }
      
  3. 試験監督の審査を受けて、ゴールデン トロフィーを獲得しましょう:
    • The Hive Leaderboard を確認します。行に琥珀色のバッジ ⏳ GHI #XX review pending が表示されます。
    • ワークショップの試験官(@palladius、@emilianodellacasa、@ricc)にデプロイの検査を依頼します。
    • 試験監督が問題に LGTM とコメントすると、The Hive で進捗状況が自動的に 8/8 🏆 に昇格し、金または紫に光るバッジと、キャップストーンの問題に直接リンクする永続的なトロフィーが表示されます。

11. 結論とクリーンアップ 🎓

おめでとうございます!🎉

Google Cloud Platform で本番環境グレードのエンタープライズ対応 Rails 8 アプリケーションを構築してデプロイしました。

  • Zero-Branch Progression: main で Time-Machine オーバーレイを使用して最新のワークフローを習得しました。
  • Cloud Storage: IAM BLOB 署名(iam: true)を備えたスケーラブルなプライベート オブジェクト ストレージ。
  • Cloud SQL: Cloud SQL Auth Proxy mTLS トンネルで保護されたマネージド PostgreSQL。
  • Secret Manager: プレーン テキストの認証情報や .env ファイルの漏洩はありません。
  • Cloud Run マルチコンテナ: 分離された Puma ウェブ、Solid Queue ワーカー、プロキシ サイドカー。
  • 生成 AI: Vertex AI を使用した非同期のビンテージ ポスター生成と TTS ポッドキャスト合成。
  • クエスト: ゼロトラスト IAP、SRE オブザーバビリティ、pgvector エンベディング

🧹 リソースのクリーンアップ

ワークショップの完了後に継続的な課金が発生しないように、Google Cloud リソースをクリーンアップしてください。環境に最適なアプローチを選択します。

他のリソースやデータが存在する既存の Google Cloud プロジェクトまたは共有 Google Cloud プロジェクトを使用している場合は、このオプションを使用します。このワークショップでプロビジョニングされたリソース(Cloud Run、Cloud SQL、Secret Manager、GCS バケット)のみを削除し、プロジェクトの残りの部分はそのまま残します。

cd iac
terraform destroy -auto-approve

オプション 2: プロジェクトの完全削除(痕跡を残さない 🌪️)

このオプションは、専用のワークショップ プロジェクト(ワークショップ クレジットやサンドボックスなどを使用)を作成し、ログ、メタデータ、サービス アカウント、請求先アカウントのリンクなど、トレースが残らないようにする場合に使用します。

gcloud projects delete $GOOGLE_CLOUD_PROJECT --quiet