۱. مقدمه
به کارگاه Rails 8 روی Google Cloud خوش آمدید! در این آزمایشگاه کد عملی، شما یک برنامه مدرن Rails 8 را از یک پایه SQLite محلی ساده به یک معماری مرجع آماده برای تولید و سازمانی در Google Cloud تبدیل خواهید کرد.
در این کارگاه، ما فقط یک اپلیکیشن را مستقر نمیکنیم - ما یک مدرنسازی ابریِ خودمحور و در سطح تولید را اجرا میکنیم ( لیفت و شیفت به درستی انجام شده است! ).
شما از تنظیمات محلی معمولی «روی دستگاه من کار میکند» (دیسک موقت، SQLite تعبیهشده، کارهای در حال انجام) شروع میکنید و به تدریج آن را به یک معماری ابری سازمانی و ضدگلوله در Google Cloud تکامل میدهید:
- 🐘 از SQLite محلی → SQL ابری (PostgreSQL): دیگر پایگاه داده با راهاندازی مجدد پاک نمیشود. ما به PostgreSQL مدیریتشده که به طور ایمن از طریق پروکسی احراز هویت Cloud SQL (mTLS، بدون IP عمومی) متصل است، منتقل میشویم.
- 🪣 از ذخیرهسازی محلی موقت → GCS خصوصی: دیسک بدون سرور موقتی است. ما ActiveStorage را با استفاده از URL های امضا شده توسط IAM (
iam: true) به ذخیرهسازی ابری خصوصی Google متصل میکنیم - بدون باکتهای عمومی، بدون کلیدهای فاش شده. - 🚀 از کارهای در حال انجام → اجرای ابری چند کانتینری: ما بین Rails یکپارچه و فضای ابری بدون سرور پلی میزنیم! با استفاده از Docker Compose استاندارد، Solid Queue را به عنوان یک کانتینر کارگر اختصاصی در کنار Puma در یک پاد چند کانتینری Cloud Run اجرا میکنیم - بدون پراکندگی میکروسرویس، بدون گرسنگی نخ، DHH به آن افتخار خواهد کرد!
🍌 و از آنجایی که ما در سال ۲۰۲۶ زندگی میکنیم، کل مجموعه را با Google Antigravity تقویت میکنیم . این یک فکر بیربط نیست، ما دستورالعملها و مهارتهایی داریم که به شما کمک میکند تا این کارگاه را بهتر اجرا کنید (و از آن لذت ببرید)!
معماری مرجع هدف (نوع ۱: Clean Flat Vector Enterprise)
در اینجا معماری مرجع استاندارد گوگل کلود (Google Cloud) که شما خواهید ساخت و مستقر خواهید کرد ( گونه ۱: Clean Flat Vector Enterprise ) آمده است که سرویس Cloud Run چندکانتینری تولیدی ما را که به تمام سرویسهای مدیریتشدهی پایداری و هوش مصنوعی گوگل کلود متصل است، نشان میدهد:

🎞️ تکامل معماری: از صفر تا بومیسازی ابری
شاهد مدرن شدن تدریجی مجموعه ما از یک سیستم پایه تک ماشینی زودگذر به یک طرح کلی کاملاً مدیریتشده گوگل کلود در هر مرحله از کارگاه باشید:

آنچه یاد خواهید گرفت
- چگونه با Google Antigravity برنامه نویسی کنیم تا Rails 8 و Google Cloud را رمزگشایی کنیم.
- نحوه اجرای تشخیص خودکار پیش از پرواز (
just workshop-test). - چگونه میتوان زیرساخت گوگل کلود را به صورت غیرهمزمان با استفاده از Terraform فراهم کرد و در عین حال توسعه محلی را بدون مسدود کردن با استفاده از ارائهدهنده
googleادامه داد. - چگونه الگوهای ضد امنیتی را از بین ببریم: به جای باز کردن
0.0.0.0/0از باکتهای خصوصی GCS (iam: true) و تونلهای mTLS پروکسی احراز هویت ابری SQL استفاده کنیم. - نحوه تزریق مستقیم رمزها از Google Cloud Secret Manager
- نحوه هماهنگسازی کارهای پسزمینهای ناهمزمان GenAI (تولیدکننده کاور NanoBanana، سنتز پادکست دوزبانه) از طریق Solid Queue .
بیایید شروع کنیم!
۲. مرحله ۰: پیشنیازها، تنظیمات ضد جاذبه و تأیید صورتحساب
💡 سناریو: شما و تیمتان در حال ساخت یک برنامه کاربردی Rails 8 با ماموریت حیاتی هستید. قبل از دست زدن به کد یا راهاندازی منابع ابری، باید زنجیره ابزار خود را ایجاد کنیم، به Google Antigravity متصل شویم و اعتبارنامههای Google Cloud و پایه صورتحساب خود را تأیید کنیم.
۱. چک لیست پیشنیازها
قبل از شروع، مطمئن شوید که ابزارهای زیر را در محیط خود دارید:
-
just(۱.۲۱+): (just --version) اجراکنندهی وظیفه — تک تک دستورات این کارگاه یک دستورالعملjustاست (از هوش مصنوعی خود بخواهید که آن را نصب کند، یا بهskills/rails8app-workshopمراجعه کنید). - گیت (۲.۳۰+): (
git --version) برای کنترل نسخه، شاخهبندی و شبیهسازی مخزن. - کیت توسعه نرمافزار گوگل کلود (
gcloudCLI): نصب و بهروزرسانی شده است. - رابط خط فرمان Terraform (نسخه ۱.۵ به بالا): برای تأمین زیرساختهای اعلانی.
-
docker&docker-compose: به صورت محلی نصب و اجرا میشوند. - Ruby
3.4.5: (ruby -v— پین شده توسطblog/.ruby-version; در صورت وجود از AI harness خود بخواهید که3.4.5را نصب کند). Rails 8 درGemfileاعلام شده و به طور خودکار توسطbundle installنصب میشود. - Google Antigravity 2.0: دستیار برنامهنویسی جفتی هوش مصنوعی مستقل شما ( دانلود Google Antigravity 2.0 ). شما با Gmail شخصی خود وارد خواهید شد - نیازی به توکن API نیست.
۲. احراز هویت گوگل کلود، پیکربندی اختصاصی و 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 به تنظیمات عادی خود برگردید.

۳. 🚨 تأیید صورتحساب GCP
⚠️ توجه : استقرار Google Cloud SQL و Cloud Run نیاز به یک حساب صورتحساب مرتبط فعال یا اعتبارات آموزشی معتبر کارگاه دارد. بیایید همین حالا مطمئن شویم که تنظیمات شما صحیح است. برای انجام این کار، بررسی تأیید صورتحساب را اجرا کنید:
gcloud beta billing projects describe $GOOGLE_CLOUD_PROJECT
مطمئن شوید billingEnabled: true برگردانده شده است. همچنین میتوانید تمام حسابهای صورتحساب خود را فهرست کنید:
gcloud billing accounts list
مطمئن شوید که حداقل یکی از آنها OPEN: True را نشان میدهد. اگر هیچکدام فعال نیستند، یک حساب صورتحساب را پیوند دهید یا کوپن اعتباری کارگاه خود را در صفحه صورتحساب کنسول Google Cloud بازخرید کنید.

۴. مخزن را کلون کنید و با Antigravity جفت کنید
مخزن را کلون کنید و وارد دایرکتوری شوید. توجه داشته باشید که ما کاملاً روی main میمانیم:
git clone https://github.com/palladius/rails8-app-on-gcp.git
cd rails8-app-on-gcp
این دایرکتوری را در گوگل آنتیگراویتی باز کنید. آنتیگراویتی به طور خودکار مخزن را بررسی میکند، AGENTS.md میخواند و به عنوان برنامهنویس جفت شما در کنارتان خواهد ماند.
۵. 🪫 اگر اعتبار رایگان بازی Antigravity تمام شود
⚠️ اعتبارهای ابری که به تازگی استفاده کردهاید، Antigravity را دوباره شارژ نمیکنند. این دو، دو سطح صورتحساب جداگانه هستند: سهمیه Antigravity در طرحهای Google AI (Google One) قرار میگیرد، در حالی که کوپنها و اعتبارهای ابری در یک حساب Cloud Billing قرار میگیرند. پیوند دادن یک حساب صورتحساب به پروژه GCP شما، هیچ پیام اضافی Antigravity به شما نمیدهد.
اگر عامل با یک پیام سهمیهبندی متوقف شود، از این نردبان پایین بروید - دو مورد اول رایگان و فوری هستند:
- تغییر مدل Pool. سهمیه بر اساس خانواده مدل ردیابی میشود و Antigravity دو Pool مستقل (مدلهای Gemini در مقابل مدلهای Claude/GPT) را نشان میدهد. مدلهای Claude و GPT-OSS در سطح رایگان موجود هستند، بنابراین یک Pool تخلیه شده Gemini اغلب مدل دیگر را دست نخورده باقی میگذارد. از انتخابگر مدل در زیر کادر اعلان استفاده کنید.

- به جای Pro، یک مدل Flash انتخاب کنید. محدودیتهای نرخ با میزان کاری که اپراتور به ازای هر درخواست انجام میدهد، مرتبط است، بنابراین یک مدل سبکتر، فضای باقیمانده را افزایش میدهد.
- از Antigravity CLI با کلید API Gemini خودتان استفاده کنید . Bring-your-own-key در Antigravity IDE پشتیبانی نمیشود — CLI جایگزین مستند شده است. یک کلید رایگان در Google AI Studio ایجاد کنید (به یک پروژه نیاز دارد اما حساب کاربری ندارد )، سپس Antigravity CLI را دانلود و پیکربندی کنید.
- صبر کنید، یا ارتقا دهید . سهمیه رایگان هفتگی بهروزرسانی میشود (پیام خطا تاریخ تنظیم مجدد شما را نشان میدهد). Google AI Pro/Ultra تنها راه مستند برای افزایش خط پایه در برنامه دسکتاپ است. این طرحها هر پنج ساعت بهروزرسانی میشوند و میتوانند اعتبارهای خریداری شده هوش مصنوعی را صرف هزینههای اضافی کنند.
- اگرچه این یک کارگاه آموزشی ضد جاذبه است، اما اکثر مهارهای کدنویسی هوش مصنوعی نیز باید کار کنند - فقط مهارهای خود را به دایرکتوری
AGENTS.mdوskills/در مخزن هدایت کنید.
۶. اعتبارسنجی خودکار مرحله ۰
تأیید کنید که محیط مرحله ۰ شما ۱۰۰٪ با مجموعه ارزیابی مطابقت دارد:
just workshop-eval 0
✨ لحظه شگفتانگیز: آنتیگراویتی و موتور ارزیابی کارگاه، نسخههای CLI، محیط روبی و احراز هویت ADC شما را در کمتر از ۲ ثانیه تأیید میکنند!
۳. مرحله ۱: شروع زیرساخت Terraform و تشخیص پیش از پرواز
💡 استراتژی: آمادهسازی پایگاههای داده مدیریتشده مانند Google Cloud SQL PostgreSQL تقریباً ۱۰ تا ۱۲ دقیقه طول میکشد. به جای اینکه بیهدف منتظر بمانیم، ما زیرساخت تغییرناپذیر را از طریق Terraform همین الان در پسزمینه راهاندازی میکنیم و همزمان به صورت محلی توسعه میدهیم!
۱. اجرای مجموعه تشخیص خودکار پیش از پرواز
قبل از راهاندازی زیرساخت ابری، مجموعه تست جامع پیش از پرواز را اجرا کنید:
just workshop-test
این اسکریپت ( bin/workshop_diagnostics.rb ):
- پیکربندی هویت
GOOGLE_CLOUD_ACCOUNTشما را تأیید میکند. - صورتحساب فعال و ارتباط پروژه را تأیید میکند.
- اعتبارنامههای پیشفرض برنامه (ADC) را برای Vertex AI اعتبارسنجی میکند.
- ایمیج دانهی قناری 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
۲. ⏱️ راهاندازی ناهمگام زیرساخت Terraform
دستور العمل تک مرحلهای Terraform را اجرا کنید، که سطل حالت backend از راه دور را ایجاد کرده و استقرار زیرساخت را آغاز میکند:
just terraform-apply

جایگزین دستی (یا اجرای مستقیم `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 در نظر گرفته شده است:
- سطل ذخیرهسازی ابری گوگل: با دسترسی خصوصی و امضای اعتبارنامههای IAM (
iam: true) ایجاد شده است. - تصویر آزمایشی Canary: فایل
gcs_dev_image.jpgرا در باکت آپلود میکند تا تأیید صحت blob از ابتدا تا انتها را فعال کند. - نمونهی PostgreSQL از Google Cloud SQL: آمادهسازی پسزمینه را آغاز میکند (حدود ۱۰ تا ۱۲ دقیقه).

💡 توجه: میتوانید با خیال راحت هشدار مربوط به از دست دادن پشتیبانگیریهای خودکار را نادیده بگیرید - ما برای پایگاه داده این کارگاه به آنها نیازی نخواهیم داشت (اگرچه پشتیبانگیریهای خودکار روزانه یک روش ضروری و بهترین روش برای هر پایگاه داده عملیاتی واقعی است!).

📝 به آدرس اینترنتی Cloud Run خود توجه کنید: اعمال Terraform باید موفقیتآمیز باشد و خروجیهای زیرساخت شما را چاپ کند. cloud_run_url خود را یادداشت کنید (یا آن را در کلیپبورد خود کپی کنید) - در مراحل بعدی هنگام تأیید سرویس زنده خود به آن نیاز خواهید داشت!
۳. اعتبارسنجی خودکار مرحله ۱ (اختیاری)
تأیید کنید که پیشنیازها و پیکربندیهای مرحله ۱ شما با موفقیت انجام شده است:
just workshop-eval 1
⏱️ توجه: این ارزیابی منابع ابری زنده را بررسی میکند و ممکن است چند دقیقه طول بکشد. این ارزیابی کاملاً اختیاری است - در حالی که در ترمینال شما اجرا میشود، میتوانید بلافاصله و بدون انتظار به مرحله بعدی بروید!
۴. مرحله ۲: پایگاه داده محلی، Mailpit و آشنایی با مدیر
نقطه شروع ما یک برنامه وبلاگ مدرن و تمیز Rails 8 است که روی localhost با SQLite، Mailpit email interception و ActiveStorage مبتنی بر دیسک اجرا میشود.
۱. برنامه را به صورت محلی بوت کنید
📁 به دایرکتوری توجه کنید! این مخزن، برنامه Rails را در blog/ و Terraform را در iac/ نگهداری میکند. مرحله ۱ شما را در ریشه مخزن قرار داد، بنابراین هر دستور در مرحله ۲ از آنجا اجرا میشود
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
روبی سیستم (مثلاً نسخه ۳.۳.x) را نشان میدهد؟ روبی سیستم وانیلی لینوکس کاملاً فایلهای .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 ) استفاده کنید که پشته Ruby پین شده را به طور کامل درون کانتینرها و بدون دست زدن به host gemها اجرا میکند!
💡 ترجیح میدهید در موردش فکر نکنید؟ just install و از ریشه just compose-up cd blog برای شما انجام میدهد.
۲. 🌱 کشف خودکار سید هوشمند و بوتاسترپ مدیریتی (شماره ۲۱ و ۲۵)
سید پایگاه داده ( db/seeds.rb ) دارای قابلیت کشف خودکار هوشمند محیط است:
- این ابزار، آداپتور پایگاه داده فعال شما (SQLite در مقابل Postgres) و پیکربندی ذخیرهسازی را بررسی میکند.
- مرحله ۰ (Localhost) را تشخیص میدهد و به طور خودکار کاربر ادمین اولیه را ایجاد میکند و پست آموزشی را منتشر میکند:
-
[LOCAL BASELINE] Welcome to Rails 8 on Localhost! - پیوست تصویر غمگین محلی آماده (
local_sad_image.png) با واترمارک که به شما اطلاع میدهد فضای ذخیرهسازی دیسک محلی موقت است.
-
- این به طور خودکار یک ایمیل تنظیم مجدد رمز عبور را از طریق ActionMailer فعال میکند.
تأیید یا اجرای مجدد سید (seed) (به طور خودکار از GOOGLE_CLOUD_ACCOUNT شما از .env استفاده میکند):
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

برنامه، سرور محلی Mailpit SMTP و نمایشگر پایگاه داده Adminer اکنون در کانتینرهای جداگانه با هم اجرا میشوند:
- برنامه ریلز : http://localhost:3000
- رابط کاربری وب Mailpit : http://localhost:8025
- رابط کاربری Adminer DB : http://localhost:8081
🧯 اختلال در پورت ۳۰۰۰ یا
server.pid
خطا؟ اگر قبلاً یک سرور توسعه بومی ( bin/dev ) راهاندازی کردهاید، آن را با Ctrl+C متوقف کنید. اگر داکر شکایت کرد که پورت ۳۰۰۰ از قبل اختصاص داده شده است یا یک server.pid باقی مانده پیدا کرد، با این دستور آن را پاک کنید:
just compose-down
rm -f blog/tmp/pids/server.pid
just compose-up
۳. تجربه Mailpit و ورود به سیستم مدیریت
- دریافت ایمیلهای خروجی با Mailpit : آدرس
http://localhost:8025را در مرورگر خود باز کنید. به صندوق ورودی محلی نگاه کنید! در طولdb:seed، ActionMailer یک ایمیل آشنایی با مدیر ارسال کرد که Mailpit آن را به صورت محلی و بدون دست زدن به سرورهای ایمیل خارجی یا اعتبارنامهها، با خیال راحت دریافت کرد:
۲. وارد وبلاگ شوید : http://localhost:3000 را در مرورگر خود باز کنید.
- شما میتوانید مستقیماً از طریق ایمیل Mailpit روی لینک تنظیم مجدد رمز عبور کلیک کنید تا رمز عبور خود را تنظیم کنید.
- روش دیگر، ورود به سیستم با استفاده از ایمیل حساب Google Cloud خود (از
.env) و رمز عبور پیشفرضCh4ng3m3!!1.
- به نشانهای تلهمتری بصری توجه کنید:
- به بنر و نشانهای محیطی زرد رنگ در رابط کاربری توجه کنید:
Notice: Ephemeral Database Active (POSTGRESQL)و[EPHEMERAL DB / STORAGE] 💾 Local. - به واترمارک پست توجه کنید: مهر 🏠 (
nanobanana_stamp_local.pngدر گوشه پایین سمت راست تصویر جلد). این مهر تأیید بصری فوری میدهد که داراییها و پایگاه داده شما در حال حاضر به حافظه محلی موقت متصل هستند.
- به بنر و نشانهای محیطی زرد رنگ در رابط کاربری توجه کنید:

💡 نکته حرفهای اختیاری: تمرین کنسول ریلز کنجکاو هستید که ریلز چگونه با دادههای سید شده شما از 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
۴. اعتبارسنجی خودکار مرحله ۲
تنظیمات اولیه و ادمین محلی خود را تأیید کنید:
just workshop-eval 2

۵. نکتهی کلیدی: کانتینرهای بدون تابعیت
کانتینرهای Cloud Run بدون وضعیت و زودگذر هستند. اگر پایگاه داده SQLite و دایرکتوری storage/ محلی خود را مستقیماً در Cloud Run مستقر کنیم، هر زمان که یک کانتینر به صفر تغییر مقیاس دهد یا مجدداً راهاندازی شود، تمام پستها و تصاویر آپلود شده برای همیشه پاک میشوند.
در مرحلهی بعدی، ما عمداً این پیکربندی موقت را در Cloud Run مستقر خواهیم کرد تا شاهد شوک بیدولتی از نزدیک باشیم!
۵. مرحله ۳: استقرار ۱ - شوک بیدولتی (عجیب و غریب اولیه در ۳ دقیقه!)
به جای اینکه ۱۵ تا ۲۰ دقیقه برای پایگاههای داده ابری منتظر بمانیم تا چیزی را در وب ببینیم، توسعه مدرن بدون سرور با یک پیروزی زودهنگام آغاز میشود: استقرار برنامه تک کانتینری Rails ما مستقیماً در Google Cloud Run در کمتر از ۳ دقیقه!
۱. ماشین زمان به عقب برمیگردد
برای تضمین اینکه پیکربندی اولیه ما ۱۰۰٪ زودگذر است (پایگاه داده SQLite محلی و ذخیرهسازی فایل دیسک محلی)، از موتور ماشین زمان Zero-Branch استفاده کنید:
just workshop-rewind 1
💡 چه اتفاقی افتاد؟ workshop-rewind 1 بدون ترک شاخه main ، پیکربندی stage-1-stateless را روی blog/config/ اعمال کرد. برنامه شما با SQLite روی دیسک کانتینر و ActiveStorage روی سیستم فایل محلی پیکربندی شده است.
۲. استقرار پومای تککانتینری در Cloud Run
مستقیماً از کد منبع به Cloud Run مستقر شوید. Modern Rails 8 به طور خودکار یک Dockerfile رسمی و آماده برای تولید (با ساختهای چند مرحلهای، jemalloc و امنیت غیر ریشهای) را از جعبه تولید میکند. وقتی از blog/ folder مستقر میشوید، 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
شما باید خروجی پیشرفتی مشابه این را ببینید:

⏳ این کار باید چند دقیقه طول بکشد تا Cloud Build کانتینر شما را بستهبندی کند و نسخه Cloud Run را آماده کند.
🧯 عیبیابی و هشدارهای رایج
PERMISSION_DENIED: Build failed because the default service account is missing required IAM permissions
دو علت مختلف باعث ایجاد یک خطای یکسان میشوند :
- gcloud همین الان یک API را برای شما فعال کرد. اگر پیغام deploy عبارت
The following APIs are not enabled ... cloudbuild.googleapis.comچاپ کرد و شما بهYپاسخ دادید، ممکن است ساخت قبل از اتمام انتشار مجوزهای جدید شروع شده باشد - اعلان خود gcloud هشدار میدهد که "این کار چند دقیقه طول میکشد". یک دقیقه صبر کنید و دقیقاً همان دستور را دوباره اجرا کنید ؛ معمولاً موفقیتآمیز است.terraform applyمرحله 1 معمولاً این API را برای شما فعال میکند، بنابراین اصلاً نباید اعلان را ببینید. - SA محاسبات پیشفرض واقعاً فاقد نقش است. پروژههای مدرن GCP حداقل امتیاز را روی آن اعمال میکنند که باعث اختلال در استقرار منبع میشود. این مشکل را با موارد زیر برطرف کنید:
just project-status # grants storage.admin, logging.logWriter, artifactregistry.writer, cloudbuild.builds.builderjust workshop-testهم الان این را گزارش میدهد، بنابراین اگر استقرار مدام با شکست مواجه میشود، ارزش دارد که دوباره اجرا شود.
در طول استقرار:
- Cloud Run تصویر کانتینر Rails شما را میسازد.
- این یک دامنه عمومی و امن TLS اختصاص میدهد:
https://blog-[hash]-[region].a.run.app. - ترافیک از طریق پورت ۸۰۸۰ به سمت پوما هدایت میشود.
۳. ✨ لحظه اولیه WOW
آدرس اینترنتی Cloud Run تولید شده را در مرورگر خود باز کنید!
- اپلیکیشن مدرن Rails 8 شما روی فضای ابری گوگل (Google Cloud) فعال است!
- با اطلاعات کاربری ادمین خود (
GOOGLE_CLOUD_ACCOUNTوAPP_ADMIN_PASSWORD) وارد شوید. اگر دکمهی ورود را پیدا نکردید، روی «نوشتهی جدید» کلیک کنید. ابتدا از شما نام کاربری و رمز عبور خواسته میشود:
- روی «پست جدید» کلیک کنید، مقالهای با عنوان «اولین پست من با اجرای ابری» بنویسید، یک عکس ضمیمه کنید و روی «ایجاد پست» کلیک کنید.
- پست شما با قالببندی کامل منتشر شده و تصویر شما رندر شده است.
- به دورسنجی بصری توجه کنید:
- نشان سربرگ:
[EPHEMERAL DB / STORAGE] 💾 Local - واترمارک تصویر: مهر 🏠 (نشان دیسک موقت
127.0.0.1در گوشه پایین سمت راست).
- نشان سربرگ:
🐝 به جدول امتیازات کارگاه زنده بپیوندید! اگر آنلاین هستید و ناظر شما جدول امتیازات را نشان میدهد و میخواهید به آن بپیوندید، آدرس اینترنتی Cloud Run خود را اینجا اضافه کنید: 👉 Cloud Run خود را در جدول امتیازات ثبت کنید.

۴. 💥 نکته: شوک بیتابعیتی و تلهی «راهحل پوما»
Cloud Run یک پلتفرم بدون وضعیت و بدون سرور است. وقتی ترافیک وب به صفر میرسد، Cloud Run برای صرفهجویی در هزینه، تعداد نمونههای کانتینر را به صفر کاهش میدهد. وقتی یک درخواست HTTP جدید میرسد یا یک نسخه جدید از کانتینر پیادهسازی میشود، Cloud Run یک تصویر کانتینر کاملاً جدید و تمیز را آغاز میکند.
بنر مشاغل گیر کرده
وقتی در یک پیادهسازی تککانتینری، پستی ایجاد میکنید یا تصویری را پیوست میکنید، Rails وظایف ActiveJob (مانند تحلیل ابعاد تصویر یا نمایهسازی فراداده) را در صف قرار میدهد. اما از آنجایی که هیچکس در حال اجرای یک worker پسزمینه نیست، بنر هشدار را مشاهده خواهید کرد:

راه حل وسوسه انگیز: صف طولانی در پوما
یک توسعهدهنده باهوش ممکن است بگوید: «صبر کنید! ریلز ۸ به ما اجازه میدهد با فعال کردن ...، صف جامد را مستقیماً درون فرآیند وب سرور پوما اجرا کنیم.»
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
حالا به مرورگر خود برگردید و صفحه را رفرش کنید :
💥 شوک بیتابعیتی:
- خبر خوب: کارمند صف جامد (Solid Queue) اکنون در پوما فعال است! هر شغل جدیدی فوراً خالی میشود.
- دوش آب سرد (نکته جالب!):
- از آنجا که Cloud Run نسخه جدیدی را مستقر کرد، نمونه کانتینر قبلی جایگزین شد!
- مقالهای که نوشتید و فایل پایگاه داده SQLite روی دیسک کاملاً پاک شدند !
- درس معماری: اجرای workerهای پسزمینه درون پوما، CPU/RAM رشتههای وب باارزش را مصرف میکند و هنوز مشکل پایداری را حل نمیکند .
۵. اعتبارسنجی خودکار مرحله ۳
استقرار Cloud Run و مدیریت هشدارهای آن را تأیید کنید:
just workshop-eval 3
✨ درس: راهحلهای تککانتینری مانند SOLID_QUEUE_IN_PUMA برای توسعه محلی مفید هستند، اما در معماری ابری مدرن:
- فایلهای رسانهای به فضای ذخیرهسازی ابری گوگل (GCS) نیاز دارند (مرحله ۴).
- دادههای رابطهای و صفهای کاری به Google Cloud SQL مدیریتشده نیاز دارند (مرحله 5).
- کارگران سنگین پسزمینه در یک محفظه مخصوص واگن کناری قرار میگیرند (مرحله ۶).
۶. مرحله ۴: استقرار ۳ — GCS Persistent Storage و POLA Warning
در این مرحله، با تغییر ActiveStorage به Google Cloud Storage (GCS) ، با استفاده از URLهای امضا شده با طول عمر کوتاه از طریق API اعتبارنامههای IAM ( iam: true )، فضای ذخیرهسازی رسانه و فایل را از دیسک کانتینر جدا میکنیم.
قبل از اینکه به هرگونه پیکربندی دست بزنید، سه موردی را که Rails باید با GCS در میان بگذارد، درک کنید:
① شناسه پروژه + نام سطل - ریلز باید بداند که blobها را کجا ذخیره کند. برنامه GOOGLE_CLOUD_PROJECT را از محیط میخواند و نام سطل را به صورت ${GOOGLE_CLOUD_PROJECT}-activestorage-prod استخراج میکند. Terraform شما از مرحله 1 قبلاً این سطل را ایجاد کرده است - نیازی به کار اضافی نیست.
② یک حساب کاربری سرویس با مجوزهای مناسب - Cloud Run به عنوان حساب کاربری سرویس پیشفرض Compute Engine ( PROJECT_NUMBER-compute@developer.gserviceaccount.com ) اجرا میشود. این SA به دو نقش IAM در باکت نیاز دارد:
-
roles/storage.objectAdmin— برای آپلود و ارائه blobها -
roles/iam.serviceAccountTokenCreatorروی خودش - برای فراخوانی API مربوط به اعتبارنامههای IAMsignBlobو تولید URLهای امضا شده با عمر کوتاه
⚠️ میانبر وسوسهانگیز —
public: true سریعترین راه برای راهاندازی GCS، تنظیم public: true در storage.yml و اعطای مجوز allUsers:objectViewer به باکت است. تصاویر فوراً بارگذاری میشوند و نیازی به امضا نیست. این کار را نکنید. یک باکت عمومی، تمام رسانههای کاربر آپلود شده را برای همیشه به اینترنت آزاد نشت میدهد. طرح ما از iam: true برای حفظ ۱۰۰٪ خصوصی بودن باکت استفاده میکند - هر URL تصویر یک توکن امضا شده و منقضی شونده است که توسط API اعتبارنامههای IAM به صورت آنی تولید میشود.
۳. (۳)
ACTIVE_STORAGE_SERVICE=google — متغیر محیطی واحدی که Rails را در زمان اجرا از دیسک محلی به GCS منتقل میکند. بدون تغییر کد، بدون تغییر منطق برنامه — فقط یک متغیر محیطی را تغییر میدهد.
۱. ماشین زمان به عقب برمیگردد
ماشین زمان را به مرحله ۲ ببرید:
just workshop-rewind 2
blog/config/storage.yml را بررسی کنید - خواهید دید که برنامه سه محیط 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

۲. اعطای مجوزهای ذخیرهسازی و امضای IAM
مطمئن شوید که حساب سرویس 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"
۳. سومین استقرار در فضای ابری با اتصال GCS
با فعال بودن GCS، دوباره مستقر کنید. مطمئن شوید که داخل پوشه blog/ هستید (در غیر این صورت، Cloud Run سعی میکند به جای Rails 8 Dockerfile با استفاده از Buildpacks برنامه را بسازد و باعث خرابی در ساخت شود).
برای حفظ متغیرهای env موجود (کلید اصلی، پیکربندی 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 logs که به راحتی در خروجی ترمینال شما ارائه شده است کلیک کنید (مطابق شکل زیر). همچنین میتوانید از Antigravity برای رمزگشایی و رفع خطا کمک بخواهید!

۴. ✨ تصویر بازمانده و مهر ابری
- آدرس وبلاگ خود را بهروزرسانی کنید.
- یک پست وبلاگ جدید با عنوان «زنده ماندن در فضای ابری» ایجاد کنید و عکسی آپلود کنید.
- به گوشه پایین سمت راست تصویر نگاه کنید:
- مهر 🏠 ناپدید شد!
- این نماد با مهر رنگی Cloud GCS (
nanobanana_stamp_cloud.png) جایگزین شده است.
- یک کانتینر دیگر را مجبور به راهاندازی مجدد کنید (که باید باعث ایجاد چهارمین ویرایش شود):
gcloud run services update blog --region $GOOGLE_CLOUD_REGION --update-env-vars RESTART_TRIGGER=$(date +%s) - صفحه را رفرش کنید: در حالی که پایگاه داده SQLite بازنشانی میشود، فایل باینری تصویر در فضای ابری گوگل (Google Cloud Storage) صحیح و سالم است ! از طریق رابط خط فرمان (CLI) تأیید کنید:
gcloud storage ls -l -r gs://$GCS_BUCKET/
در اینجا لیستی از blobها و اشیاء رسانهای آپلود شده وجود دارد که ممکن است در کنسول مرورگر Google Cloud Storage در سطل تولید خود ( gs://$GCS_BUCKET ) مشاهده کنید:

۵. ⚠️ نکته: بنر هشدار دهندهی گیر کردن در مشاغل
وقتی تصویر را آپلود کردید، ActiveStorage یک کار تحلیلی ( ActiveStorage::AnalyzeJob ) را برای استخراج ابعاد و فرادادهها در صف قرار داد.
با این حال، Cloud Run در حال حاضر فقط یک کانتینر وب واحد ( puma ) را اجرا میکند. در Rails 8، Solid Queue کارهای پسزمینه را در پایگاه داده ذخیره میکند، اما هیچکس bundle exec rails solid_queue:start اجرا نمیکند!
به بالای صفحه وبلاگ خود نگاه کنید: یک بنر هشدار دهنده روشن خواهید دید که توسط blog/app/views/layouts/_check_stuck_jobs.html.erb رندر شده است.
ما قصد داریم این مشکل را به درستی در مرحله ۶ حل کنیم، زمانی که به یک معماری چند کانتینری sidecar در سطح تولید با یک کانتینر worker اختصاصی که Solid Queue را مستقل از ترافیک وب اجرا میکند، دست یابیم!
۶. اعتبارسنجی خودکار مرحله ۴
پیکربندی GCS و تلهمتری مربوط به کارهای گیر کرده را بررسی کنید:
just workshop-eval 4
۷. مرحله ۵: چک لیست قبل از شروع: SQL ابری، اسرار و اتصال پایگاه داده
در حالی که در مراحل ۳ و ۴ بر کانتینرهای بدون وضعیت و فضای ذخیرهسازی ابری گوگل مسلط شدید، Terraform آمادهسازی بکاند تولید شما را در پسزمینه به پایان رسانده است !
قبل از استقرار معماری سایدکارِ کاملِ تولیدِ ۳ کانتینری ما (استقرار ۴ کانتینر در مرحله ۶)، این مرحله به عنوان یک بازرسی سریع قبل از پرواز برای تأیید سلامت پایگاه داده، اتصال و دسترسی مخفی عمل میکند.
۰. متغیرهای محیطی بوتاسترپ
تمام متغیرهای پوسته مورد نیاز را مستقیماً از خروجیهای 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"
۱. بررسی سلامت SQL ابری
تأیید کنید که نمونه PostgreSQL مدیریتشده شما، آمادهسازی اولیه را به پایان رسانده و سالم است:
echo "Checking instance: $SQL_INSTANCE_NAME"
gcloud sql instances describe $SQL_INSTANCE_NAME --format="value(state)"
خروجی باید RUNNABLE باشد.
۲. (اختیاری) آزمایش اتصال به پایگاه داده از طریق لپتاپ
در عمل، کانتینرهای Cloud Run به طور ایمن از طریق یک پروکسی رمزگذاری شده sidecar ( 127.0.0.1:5432 ) به Cloud SQL متصل میشوند، بدون اینکه پایگاه داده در معرض اینترنت عمومی ( 0.0.0.0/0 ) قرار گیرد.
ℹ️ توجه: این مرحله کاملاً اختیاری است! در 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
در غیر این صورت، میتوانید مستقیماً به مرحله ۳ در زیر بروید!)
۳. بررسی اسرار ارائه شده توسط Terraform
هرگز رمزهای عبور پایگاه داده، کلیدهای API یا کلیدهای اصلی Rails را به صورت متن ساده در git یا متغیرهای محیطی کانتینر ذخیره نکنید. در معماری ابری مدرن، ما از Google Cloud Secret Manager برای تزریق زمان اجرای zero-trust استفاده میکنیم.
ℹ️ Terraform این کار را در مرحله ۱ برای شما انجام داد:
-
rails-master-key— برای رمزگشایی اعتبارنامههای Rails در زمان اجرا. -
rails-db-password— یک رمز عبور تصادفی رمزنگاریشده امن که با کاربر Cloud SQL مطابقت دارد. -
rails-admin-password— یک رمز عبور ایمن برای کاربر مدیر اولیه شما.
تأیید کنید که اسرار شما در Secret Manager موجود و فعال هستند:
gcloud secrets list --filter="name:rails-"

🔑 همگامسازی master.key با لپتاپ شما (همگامسازی محلی)
از آنجا که blog/config/master.key برای امنیت gitignored شده است، ممکن است کلون محلی شما هنوز آن را نداشته باشد. این قطعه کد خود-درمانگر دوستانه را اجرا کنید تا آن را از 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
🧪 زمین بازی: میخواهید یک راز سفارشی ایجاد کنید تا ببینید چقدر آسان است؟ Secret Manager فقط برای Rails نیست! میتوانید هر راز را با یک دستور ساده CLI ایجاد کنید:
echo -n "MySuperSecretValue123!" | gcloud secrets create workshop-fun-secret --data-file=-
echo "✨ Created workshop-fun-secret in Secret Manager! 🪄"
۴. تأیید مجوزهای دسترسی مخفی
برای اینکه کانتینرهای Cloud Run بتوانند این رمزها را به عنوان متغیرهای محیطی یا ولومها در هنگام بوت شدن نصب کنند، حساب سرویس زمان اجرا ( $RUN_SA ) به نقش roles/secretmanager.secretAccessor روی هر رمز نیاز دارد.
Terraform قبلاً این را در مرحله 1 اعطا کرده است! اتصال سیاست IAM را روی rails-master-key تأیید کنید:
# 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

۵. اعتبارسنجی خودکار پیش از پرواز
مجموعه تست خودکار مرحله ۵ را اجرا کنید تا تأیید کنید که پایگاه داده شما قابل اجرا است، اسرار خالی نیستند و مجوزهای IAM سبز هستند:
just workshop-eval 5
۸. مرحله ۶: استقرار ۴ — محفظههای جانبی چندکانتینری سازمانی (استاندارد طلایی)
اکنون ما معماری مرجع نهایی را گردآوری میکنیم: اجرای ابری چندکاناله !
۱. بازگرداندن استاندارد طلا
پیکربندی مخزن خود را به حالت main متعارف بازگردانید:
just workshop-restore-gold
۲. بررسی طرح تولید ۳ کانتینری
blog/compose.prod.yaml را باز کنید و معماری را بررسی کنید:

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)]
استقرار در محیط عملیاتی، سه کانتینر هماهنگ را که شبکه محلی یکسانی را به اشتراک میگذارند، اجرا میکند:
-
web: ترافیک HTTP را روی پورت ۸۰۸۰ از طریق Puma ارائه میدهد. -
worker: کانتینر پردازش پسزمینه اختصاصی کهbundle exec rails solid_queue:startمیکند. -
cloudsql-proxy: پروکسی رسمی Google Cloud SQL Auth Proxy در کنار (gcr.io/cloud-sql-connectors/cloud-sql-proxy:2) که به127.0.0.1:5432محدود شده است.
۳. اجرای مهاجرتهای پایگاه داده از طریق اجرای ابری Job
قبل از مسیریابی ترافیک وب به استقرار جدیدمان، باید مهاجرتهای پایگاه داده و بذر اولیه را در برابر Cloud SQL اجرا کنیم.
⚖️ نحوه اجرای مهاجرتها در کانتینرها: دو استراتژی
در محیطهای ابری کانتینری، دو رویکرد رایج برای اجرای مهاجرت پایگاه داده وجود دارد:
- گزینه الف: خودکار درون داکر Entrypoint (
entrypoint.shdb:migrateرا هنگام بوت کانتینر اجرا میکند)- مزایا: کاملاً خودکار؛ توسعهدهندگان لازم نیست اجرای یک دستور را به خاطر بسپارند.
- معایب: در مقیاسپذیری خودکار بدون سرور خطرناک است! وقتی Cloud Run تا 10 نمونه را به طور همزمان تحت ترافیک بالا مقیاسبندی میکند، 10 کانتینر برای اجرای همزمان مهاجرتها با هم رقابت میکنند که باعث قفل شدن پایگاه داده، شرایط رقابت در طرحواره یا زمانهای توقف بوت میشود.
- گزینه ب: اجرای صریح از طریق Cloud Run Job (انتخاب معماری ما)
- مزایا: کنترل کامل، ایمنی تراکنشها و عدم وجود شرایط رقابتی. به عنوان یک وظیفه کانتینر اختصاصی و یکباره اجرا میشود که کاملاً از سرویسدهی ترافیک وب جدا است. در صورت عدم موفقیت مهاجرت، سرویسدهی وب تحت تأثیر قرار نمیگیرد.
- معایب: قبل از استقرار، نیاز به راهاندازی یک کار CLI یا مرحله CI/CD دارد.
ما گزینه B را برای حداکثر قابلیت اطمینان تولید انتخاب میکنیم.
⚠️ موارد مهم: قالب DATABASE_URL و سختگیریهای Ruby 3.4.5 RFC 3986
کارهای Cloud Run با استفاده از پروکسی سوکت بومی یونیکس ( /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: این برنامه از ۴ پایگاه داده (primary، queue، cache، cable) به ازای هر database.yml استفاده میکند. هر ۴ DATABASE_*_URL env باید به یک نمونه 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
نمایش کنسول مشاغل Google Cloud Run
rails-migrate
اجرا با موفقیت انجام شد
۴. استقرار چهارم: استقرار اجرای ابری چند کانتینری از طریق Docker Compose
اکنون پشته اصلی تولید ۳ کانتینری ( web ، worker ، cloudsql-proxy ) را مستقیماً از compose.prod.yaml مستقر کنید:
# 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
کنسول اجرای گوگل کلود: تب «کانتینرها» که هر سه کانتینر کناری را نمایش میدهد (
cloudsql-proxy
،
web
،
worker
)
💡 کنجکاوی رابط کاربری / مشکل کنسول: در منوی کشویی کنسول، ممکن است متوجه شوید که Cloud Run ورودی بالا را با عنوان Ingress container: cloudsql-proxy برچسبگذاری کرده است. نگران نباشید! این صرفاً یک مشکل رابط کاربری بصری در کنسول Google Cloud است (که در شماره ۱۴۹ پیگیری شده است) که ناشی از ترتیب وابستگی کانتینر است. در واقعیت، web تنها کانتینری است که به پورت HTTP 8080 گوش میدهد و ۱۰۰٪ ترافیک وب ورودی را مدیریت میکند، در حالی که cloudsql-proxy صرفاً ترافیک خصوصی PostgreSQL را روی پورت ۵۴۳۲ ارائه میدهد!
۵. ✨ لحظه شگفتانگیز: بدون هشدار و پایداری کامل در فضای ابری!
نفس عمیقی بکشید و از صفحه نمایش خود شگفت زده شوید: همه بنرهای هشدار دهنده ناپدید شده اند! 🪄
وبلاگ نسخه نهایی Rails 8: رابط کاربری بینقص، بدون بنرهای هشدار و وضعیت پایدار Cloud SQL!
این برنامه اکنون رسماً در سطح تولید و فضای ابری بومی (Cloud-Native) است:
- پشتیبانی شده توسط Google Cloud SQL مدیریت شده (PostgreSQL) از طریق تونلینگ امن mTLS.
- رسانهها با خیال راحت در فضای ابری خصوصی گوگل با امضای بلاب IAM ذخیره میشوند.
- صفر هشدار ذخیرهسازی موقت و صفر هشدار گیر کردن کارها.
Open your Cloud Run URL:
- Look at the telemetry badges:
-
[CLOUD PERSISTENT 🐘 ☁️]is verified in your database seed! - Both Google Cloud Run and GCS badges in the footer shine green!
- The stuck jobs warning banner is completely gone !
-
- Create blog posts and comments.
- Restart or redeploy as many times as you like: your data, posts, comments, and assets survive forever in Cloud SQL and GCS!
6. Automated Step 6 Validation
Verify the multi-container configuration:
just workshop-eval 6
9. Step 7: Generative AI Pipelines, Podcastifier & The GCS Treasure Hunt 🏴☠️
With Solid Queue running in a dedicated container and Google Cloud Storage active, we can unleash asynchronous Generative AI!
1. The NanoBanana Vintage Cover Generator (Ready-to-Use GenAI Pipeline)

Good news: this functionality is already fully implemented in the codebase! You don't need to write any code — your task here is to trigger and observe an asynchronous cloud GenAI pipeline in action.
🧠 How it Works Behind the Scenes
- Authentication: Zero API keys required! It uses Application Default Credentials (
roles/aiplatform.useron Vertex AI) automatically via the Cloud Run runtime service account. (Alternatively, ifGEMINI_API_KEYis provided, it calls the Gemini API directly). - The Pipeline: When an article is created without a cover image, an
after_commitcallback enqueuesGenerateCoverImageJobinto Solid Queue . - Model: It prompts Google's multimodal model ( Gemini 2.5 Flash Image / Imagen on Vertex AI) to generate an authentic vintage 1960s Italian film poster ( "Locandina di un film 1960" ) complete with a cameo banana and a shiny ruby "8".
- Dynamic Provenance Watermark: Uses
libvipsto dynamically stamp a visual badge on the corner (colorful cloud stamp on GCS). - Turbo Stream Hot-Swap: When the image is attached,
Turbo::StreamsChannel.broadcast_refresh_to(post)updates the reader's browser live without a manual page reload!

📐 View Mermaid Sequence Diagram Source
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!
🧪 آن را به صورت زنده امتحان کنید!
You can trigger it in two ways:
- Option A (Web UI): Click New post , enter a title like "Serverless Architecture with Ruby on Rails 8" , write some content, and leave the cover image file input empty . Click Create Post .
- Option B (CLI Runner): Run from your terminal:
bin/rails runner 'Post.create!(title: "Serverless Architecture with Ruby on Rails 8", body: "Exploring asynchronous AI pipelines with Solid Queue and NanoBanana!")'
Wait ~15–30 seconds: watch the Solid Queue worker logs in Cloud Run. The vintage poster will appear directly on your post!
📸 TODO(riccardo): add screenshot of a blog post with an AI-generated vintage 1960s Italian movie poster featuring a cameo banana and ruby 8
2. The Bilingual Podcastifier Quest (Hands-On Antigravity Coding Exercise)
⚠️ Important Notice: Unlike the Cover Generator above, this feature is NOT yet implemented!
If you open blog/app/jobs/podcastifier_job.rb , you will find an empty scaffold stub:
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
Your mission in this hands-on exercise is to pair-program with Google Antigravity to complete this feature:
- Prompt Antigravity: "Help me implement PodcastifierJob in
app/jobs/podcastifier_job.rb. It should fetch the Post, translate or synthesize an Italian audio overview using Google Cloud Text-to-Speech (voice 'it-IT-Wavenet-A' with language code 'it-IT' via ADC credentials), and attach the audio file topost.podcast_audio_itusing ActiveStorage!" - Add Player to View: In
app/views/posts/show.html.erb, add an HTML5player when@post.podcast_audio_it.attached?. - Trigger Synthesis: When saved, Solid Queue will execute the text-to-speech job asynchronously in the background.

🎧 Sample Podcastifier Outputs:
💡 Need Help or Got Stuck? A complete, battle-tested reference solution is available on branch solutions/podcastifier . You can inspect it anytime with git diff solutions/podcastifier .
3. [OPTIONAL] 🏴☠️ The GCS Treasure Hunt (Console Blob Recovery)
ℹ️ Prerequisite Check: Local
cloud-sql-proxy
CLI Required This optional exercise connects your local laptop directly to Google Cloud SQL via an encrypted mTLS tunnel. Check if you have the CLI proxy installed:
which cloud-sql-proxy
- If installed: Proceed below!
- If NOT installed and you want to do this quest: Install it with:
# 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 - Otherwise: Feel free to skip to the Step 7 automated validation!
Remember that photo you uploaded back in Step 4 before the container restart wiped out the ephemeral SQLite database? That image file is still sitting safely in your private GCS bucket as an "orphaned blob"!
Let's use the Rails console to rescue it and attach it to a Cloud SQL post:
# 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
Inside the Rails console:
# 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): add screenshot of the interactive Rails console executing the GCS Treasure Hunt snippet and rescuing orphaned ActiveStorage blobs
Refresh your blog: the photo uploaded during Step 4's Stateless Shock is resurrected and permanently attached to your Cloud SQL post!
4. Automated Step 7 Validation
Verify GenAI jobs and assets:
just workshop-eval 7
10. Step 8: Choose Your Own Adventure / Advanced Quests 🏆
Now that you have mastered the canonical reference architecture, choose your graduation quest!
🛡️ Quest 1: Zero-Trust Google IAP (Identity-Aware Proxy)
- Difficulty: Medium (Enterprise Security)
- The Goal: Lock down your Cloud Run application so only your verified Google / Gmail accounts can access it, eliminating password login entirely!
- چگونه کار میکند:
- An External HTTPS Application Load Balancer terminates Google OAuth and verifies identity before traffic ever reaches Cloud Run.
- Google forwards verified identity headers (
X-Goog-Authenticated-User-Email). - Rails automatically logs in the verified Google identity via
blog/app/controllers/concerns/iap_authenticatable.rb: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 - Enable the Terraform module in
iac/iap.tfwithenable_iap = trueand specify your allowed Google accounts:iap_allowed_users = [var.google_cloud_account]

📊 Quest 2: Production SRE Telemetry & Cloud Logging
- Difficulty: Medium (Observability)
- The Goal: Stream structured JSON application logs with trace correlation IDs directly to Google Cloud Logging and catch production exceptions in real-time with Cloud Error Reporting.
- چگونه کار میکند:
- Configure
blog/config/environments/production.rbto emit structured JSON logs with GCP trace labels. - Trigger an intentional test exception and watch Google Cloud Error Reporting group and notify you instantly.
- Configure
📸 TODO(riccardo): add screenshot of Google Cloud Error Reporting showing grouped production exceptions and Cloud Logging structured JSON payload with trace labels
🧠 Quest 3: pgvector Semantic Search & Gemini RAG
- Difficulty: Hard (GenAI Capstone)
- The Goal: Search articles conceptually using vector embeddings stored in PostgreSQL on Cloud SQL.
- چگونه کار میکند:
- Enable the
vectorextension on your Cloud SQL PostgreSQL instance:CREATE EXTENSION IF NOT EXISTS vector; - Add the
neighborgem toblog/Gemfileand generate text embeddings with Gemini (text-embedding-004) onPost#after_save. - Perform cosine distance queries (
<=>) to power a semantic search bar with Turbo Streams!
- Enable the
📸 TODO(riccardo): add screenshot of the live blog semantic search bar returning conceptual matches with cosine similarity scores
⚡ Quest 4: SEO & Performance Audit Assistant
- Difficulty: Easy / Fun (Developer Experience)
- The Goal: Audit Core Web Vitals, Largest Contentful Paint (LCP), and Flesch-Kincaid / Fog readability indices using Antigravity and Speedgrapher tools.
📸 TODO(riccardo): add screenshot of Speedgrapher / Lighthouse SEO and Core Web Vitals audit summary
🤔 Curiosity Box: Getting a Live Rails Console on Cloud Run
💡 The Question: On traditional VMs or Heroku-style PaaS platforms, developers commonly SSH into a running instance or use heroku run rails console to inspect or fix production data live. What is the Cloud Run-native equivalent, and how do you harden it?
چرا
gcloud run exec
doesn't exist for Cloud Run Services
Cloud Run is a serverless platform: your container may be running as zero, one, or hundreds of identical instances, with no stable identity or addressable shell. This is fundamentally different from Kubernetes ( kubectl exec ), where a specific pod has a named socket you can attach to.
# ❌ 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
The Four GCP-Native Patterns
الگو | فرمان | Interactive? | امنیت |
1. Local proxy + remote DB | | ✅ کامل | 🔒 Cloud SQL Auth Proxy mTLS |
2. Cloud Run Job (one-shot) | | ❌ Script only | 🔒 IAM role required |
3. Web Console gem + IAP | Browser → | ✅ کامل | 🛡️ IAP Zero-Trust |
4. Cloud Shell + Proxy | Cloud Shell → | ✅ کامل | 🔒 Google identity |
Pattern 1 (Recommended for data inspection): Run a local Rails console that talks directly to your Cloud SQL PostgreSQL via the Cloud SQL Auth Proxy:
# 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
You get full IRB interactivity with zero network exposure — the Cloud SQL Proxy uses mTLS over a Unix domain socket or loopback.
Pattern 3 (Web Console + IAP Zero-Trust, the hardened browser path):
The web-console gem provides a browser-based IRB session. Without protection it is a critical security vulnerability — never expose it publicly. But combined with Google IAP (Quest 1), the Attack surface collapses: only verified Google identities on your allowlist can reach the /rails/console endpoint.
# 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
⚠️ Pattern 3 requires Quest 1 (IAP) to be enabled first. Without it, /rails/console on a public URL is equivalent to leaving your server room unlocked. With IAP, it becomes a powerful operator tool hardened by Google-grade Zero-Trust.
Ask Antigravity: "Show me how to run
rails console --sandbox
against my Cloud SQL production database safely using the Cloud SQL Auth Proxy."
🏆 Graduating on The Hive: Proctor-Validated Proof-of-Work
Once you have completed your chosen quest, claim your Step 8 Graduation Trophy 🏆 on The Hive Leaderboard :
- Submit Your Quest on GitHub :
- Open a new Issue on the official repository: github.com/palladius/rails8-app-on-gcp/issues/new
- Title format:
🎓 [Step 8 Completed]: - In the body, include a brief description of what you accomplished and a link to your live Cloud Run deployment.
- Connect Your Cloud Run App to Your Issue :
- Note your Issue number (eg
88or the full URLhttps://github.com/palladius/rails8-app-on-gcp/issues/88). - Inject the
STEP_8_GHIenvironment variable into your Cloud Run service:gcloud run services update blog \ --region europe-west1 \ --update-env-vars STEP_8_GHI=<YOUR_ISSUE_NUMBER> - Verify that your
/status.jsonendpoint now reports your quest: شما باید ببینید: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" }
- Note your Issue number (eg
- Get Your Proctor Review & Unlock the Golden Trophy :
- Check The Hive Leaderboard : your row will now display an amber badge:
⏳ GHI #XX review pending. - Ask workshop proctors ( @palladius , @emilianodellacasa , or @ricc ) to inspect your deployment.
- As soon as a proctor comments
LGTMon your issue, The Hive will automatically elevate your progress to 8/8 🏆 with a golden/purple glowing badge and a permanent trophy linking directly to your capstone issue!
- Check The Hive Leaderboard : your row will now display an amber badge:
11. Conclusion and Clean Up 🎓
تبریک میگم! 🎉
You have built and deployed a production-grade, enterprise-ready Rails 8 application on Google Cloud Platform:
- Zero-Branch Progression: Mastered modern workflows with Time-Machine overlays on
main. - Cloud Storage: Scalable, private object storage with IAM blob signing (
iam: true). - Cloud SQL: Managed PostgreSQL secured with Cloud SQL Auth Proxy mTLS tunnels.
- Secret Manager: Zero plain-text credentials or
.envfile leaks. - Cloud Run Multi-Container: Isolated Puma web, Solid Queue worker, and proxy sidecars.
- Generative AI: Asynchronous vintage poster generation and TTS podcast synthesis with Vertex AI.
- Quests: Zero-Trust IAP, SRE Observability, and pgvector embeddings!
🧹 Resource Clean Up
To avoid ongoing charges after completing the workshop, make sure to clean up your Google Cloud resources! Choose the approach that best fits your environment:
Option 1: Terraform Teardown (Recommended / Default 🟢)
Use this option if you are using an existing or shared Google Cloud project where other resources or data live. It surgically destroys only the resources provisioned during this workshop (Cloud Run, Cloud SQL, Secret Manager, GCS buckets), leaving the rest of your project untouched.
cd iac
terraform destroy -auto-approve
Option 2: Total Project Deletion (Leave Zero Trace 🌪️)
Use this option if you created a dedicated workshop project (eg, using workshop credits or a sandbox) and want to guarantee that zero trace remains—including logs, metadata, service accounts, and billing links.
gcloud projects delete $GOOGLE_CLOUD_PROJECT --quiet