1. Introducción
Te damos la bienvenida al taller de Rails 8 en Google Cloud. En este codelab práctico, llevarás una aplicación moderna de Rails 8 desde una simple referencia local de SQLite hasta una arquitectura de referencia de nivel de producción y lista para la empresa en Google Cloud.
En este taller, no solo implementaremos una app, sino que también ejecutaremos una modernización de la nube basada en opiniones y apta para la producción (migración lift-and-shift bien hecha).
Comenzarás con la configuración local típica de "funciona en mi máquina" (disco efímero, SQLite integrado, trabajos en proceso) y la evolucionarás progresivamente hasta convertirla en una arquitectura de nube empresarial y a prueba de balas en Google Cloud:
- 🐘 De SQLite local a Cloud SQL (PostgreSQL): Ya no se borrará la base de datos cuando se reinicie. Migramos a PostgreSQL administrado conectado de forma segura a través del proxy de Cloud SQL Auth (mTLS, sin IPs públicas).
- 🪣 Del almacenamiento local efímero a GCS privado: El disco sin servidores es temporal. Conectamos ActiveStorage a Google Cloud Storage privado con URLs firmadas de IAM (
iam: true): no hay buckets públicos ni claves filtradas. - 🚀 De trabajos en proceso a Cloud Run con varios contenedores: Conectamos la aplicación monolítica de Rails y la nube sin servidores. Con Docker Compose estándar, ejecutamos Solid Queue como un contenedor de trabajador dedicado junto con Puma en un pod de varios contenedores de Cloud Run: sin proliferación de microservicios, sin inanición de subprocesos, DHH estaría orgulloso.
🍌 Y como vivimos en 2026, potenciamos toda la pila con Google Antigravity. No es una idea tardía, tenemos instrucciones y habilidades para guiarte y que puedas ejecutar (y disfrutar) mejor este taller.
🏛️ Arquitectura de referencia de destino (variante 1: Empresa de vectores planos y limpios)
Esta es la arquitectura de referencia canónica de Google Cloud que compilarás e implementarás (Variante 1: Vector empresarial plano y limpio), en la que se muestra nuestro servicio de producción de Cloud Run con varios contenedores conectado a todos los servicios administrados de persistencia y de IA de Google Cloud:

🎞️ La evolución de la arquitectura: de cero a nativa de la nube
Observa cómo nuestra pila se moderniza progresivamente, desde un modelo de referencia efímero de una sola máquina hasta un plano de Google Cloud completamente administrado en cada hito del taller:

Qué aprenderás
- Cómo programar en pareja con Google Antigravity para desmitificar Rails 8 y Google Cloud
- Cómo ejecutar diagnósticos previos al vuelo automatizados (
just workshop-test) - Cómo aprovisionar infraestructura de nube de Google Cloud de forma asíncrona con Terraform y, al mismo tiempo, continuar con el desarrollo local sin bloquear el uso del proveedor
google - Cómo eliminar los antipatrones de seguridad: buckets de GCS privados (
iam: true) y túneles mTLS del proxy de autenticación de Cloud SQL en lugar de abrir0.0.0.0/0. - Cómo insertar secretos directamente desde Google Cloud Secret Manager
- Cómo organizar trabajos en segundo plano asíncronos de IA generativa (generador de portadas de NanoBanana, síntesis de podcasts bilingües) a través de Solid Queue
¡Comencemos!
2. Paso 0: Requisitos previos, configuración de Antigravity y verificación de facturación
💡 La situación: Tú y tu equipo están creando una aplicación esencial de Rails 8. Antes de escribir código o lanzar recursos de Cloud, debemos establecer nuestra cadena de herramientas, conectar Google Antigravity y verificar nuestras credenciales de Google Cloud y la base de facturación.
1. Lista de tareas previas
Antes de comenzar, asegúrate de tener las siguientes herramientas disponibles en tu entorno:
just(1.21 y versiones posteriores): (just --version) el ejecutor de tareas: cada comando de este taller es unajustreceta (pídele a tu arnés de IA que la instale o consultaskills/rails8app-workshop).- Git (2.30 o versiones posteriores): (
git --version) para el control de versiones, la ramificación y la clonación del repositorio - SDK de Google Cloud (CLI de
gcloud): Debe estar instalado y actualizado. - CLI de Terraform (1.5 o versiones posteriores): Para el aprovisionamiento declarativo de infraestructura
dockerydocker-compose: Instalados y en ejecución de forma local.- Ruby
3.4.5: (ruby -v, fijado porblog/.ruby-version; pídele a tu arnés de IA que instale3.4.5si falta). Rails 8 se declara enGemfileybundle installlo instala automáticamente. - Google Antigravity 2.0: Tu asistente autónomo de programación en pares con IA (Descarga Google Antigravity 2.0). Accederás con tu cuenta personal de Gmail, sin necesidad de un token de API.
2. Autenticación de Google Cloud, configuración dedicada y ADC
Para evitar conflictos con las configuraciones existentes de gcloud corporativas, personales o de varias cuentas, te recomendamos que crees una configuración con nombre dedicada para este taller:
# 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
💡 ¿Por qué una configuración dedicada? El uso de gcloud config configurations create rails8-on-gcp-workshop aísla todos los parámetros de configuración de la CLI (cuenta, proyecto de cuota, región predeterminada) específicamente para este taller. Cuando termines, puedes volver a la configuración normal en cualquier momento con gcloud config configurations activate default.

3. 🚨 Verificación de facturación de GCP
⚠️ Nota: Las implementaciones de Google Cloud SQL y Cloud Run requieren una cuenta de facturación vinculada activa o créditos educativos válidos para el taller. Ahora, asegurémonos de que tu configuración sea correcta. Para ello, ejecuta la verificación de facturación:
gcloud beta billing projects describe $GOOGLE_CLOUD_PROJECT
Asegúrate de que se devuelva billingEnabled: true. También puedes enumerar todas tus cuentas de facturación:
gcloud billing accounts list
Asegúrate de que al menos uno de ellos muestre OPEN: True. Si no hay ninguna activa, vincula una cuenta de facturación o canjea el cupón de crédito del taller en la página de facturación de Google Cloud Console.

4. Clona el repositorio y vincúlalo con Antigravity
Clona el repositorio y accede al directorio. Observa que permanecemos completamente en main:
git clone https://github.com/palladius/rails8-app-on-gcp.git
cd rails8-app-on-gcp
Abre este directorio en Google Antigravity. Antigravity inspeccionará automáticamente el repositorio, leerá AGENTS.md y estará disponible como tu programador en pareja.
5. 🪫 Si Antigravity se queda sin créditos gratuitos
⚠️ Los créditos de Cloud que acabas de canjear NO recargan Antigravity. Son dos plataformas de facturación independientes: la cuota de Antigravity se encuentra en los planes de IA de Google (Google One), mientras que los cupones y los créditos de Cloud se encuentran en una cuenta de facturación de Cloud. Vincular una cuenta de facturación a tu proyecto de GCP no te dará una sola instrucción adicional de Antigravity.
Si el agente se detiene con un mensaje de cuota, sigue esta secuencia. Los dos primeros pasos son gratuitos e instantáneos:
- Cambiar el grupo de modelos La cuota se hace un seguimiento por familia de modelos, y Antigravity muestra dos grupos independientes (modelos de Gemini y modelos de Claude/GPT). Los modelos de Claude y GPT-OSS están disponibles en el nivel gratuito, por lo que un grupo de Gemini agotado a menudo deja intacto al otro. Usa el selector de modelos que se encuentra debajo del cuadro de instrucción.

- Elige un modelo Flash en lugar de Pro. Los límites de frecuencia se correlacionan con la cantidad de trabajo que realiza el agente por solicitud, por lo que un modelo más ligero aprovecha mejor lo que queda.
- Usa la CLI de Antigravity con tu propia clave de la API de Gemini. La función Bring-your-own-key no es compatible con el IDE de Antigravity. La CLI es la alternativa documentada. Crea una clave gratuita en Google AI Studio (necesitas un proyecto, pero no una cuenta de facturación) y, luego, descarga y configura la CLI de Antigravity.
- Esperar o actualizar. La cuota del nivel gratuito se actualiza semanalmente (el mensaje de error indica la fecha de restablecimiento). Google AI Pro/Ultra es la única forma documentada de aumentar el valor de referencia dentro de la app para computadoras. Estos planes se actualizan cada cinco horas y pueden gastar los créditos de IA comprados en el exceso.
- Si bien este es un taller de Antigravity, la mayoría de los arneses de codificación de IA también deberían funcionar. Solo debes dirigirlos a los directorios
AGENTS.mdyskills/del repo.
6. Validación automatizada del paso 0
Verifica que tu entorno del paso 0 cumpla con el 100% de los requisitos del conjunto de evaluación:
just workshop-eval 0
✨ El momento sorpresa: Antigravity y el motor de evaluación del taller verifican tus versiones de la CLI, el entorno de Ruby y la autenticación de ADC en menos de 2 segundos.
3. Paso 1: Inicio de la infraestructura de Terraform y diagnóstico previo al vuelo
💡 La estrategia: Las bases de datos administradas, como Google Cloud SQL PostgreSQL, tardan entre 10 y 12 minutos en aprovisionarse. En lugar de esperar sin hacer nada, lanzamos la infraestructura inmutable a través de Terraform en segundo plano mientras desarrollamos de forma local.
1. Ejecuta el paquete de diagnóstico previo al vuelo automatizado
Antes de lanzar la infraestructura de nube, ejecuta el paquete de pruebas integral:
just workshop-test
Esta secuencia de comandos (bin/workshop_diagnostics.rb) hace lo siguiente:
- Verifica la configuración de identidad de
GOOGLE_CLOUD_ACCOUNT. - Verifica la facturación activa y la vinculación del proyecto.
- Valida las credenciales predeterminadas de la aplicación (ADC) para Vertex AI.
- Confirma la imagen de la versión canary de ActiveStorage (
blog/app/assets/images/gcs_dev_image.jpg).

Si falta .env, cópiala de la plantilla documentada:
cp .env.dist .env
# Edit .env and configure GOOGLE_CLOUD_ACCOUNT with your Google/Gmail account
💡 Sugerencia: Si just workshop-test informa que la API de facturación está inhabilitada en tu proyecto, habilítala rápidamente de la siguiente manera:
gcloud services enable cloudbilling.googleapis.com
2. ⏱️ Inicia la infraestructura de Terraform de forma asíncrona
Ejecuta la receta de Terraform de un solo paso, que crea el bucket de estado del backend remoto y activa la implementación de la infraestructura:
just terraform-apply

Alternativa manual (o ejecuta `iac/bin/terraform-apply.sh` directamente)
# 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 ..
Qué aprovisiona Terraform:
- Bucket de Google Cloud Storage: Se creó con acceso privado y firma de credenciales de IAM (
iam: true). - Canary Test Image: Sube
gcs_dev_image.jpgal bucket para habilitar la verificación de BLOB de extremo a extremo. - Instancia de PostgreSQL de Google Cloud SQL: Inicia el aprovisionamiento en segundo plano (de 10 a 12 minutos).

💡 Nota: Puedes ignorar de forma segura la advertencia sobre la falta de copias de seguridad automáticas. No las necesitaremos para esta base de datos del taller (aunque las copias de seguridad diarias automáticas son una práctica recomendada esencial para cualquier base de datos de producción real).

📝 Toma nota de la URL de Cloud Run: La aplicación de Terraform debería completarse correctamente y mostrar los resultados de tu infraestructura. Anota tu cloud_run_url (o cópialo en el portapapeles). Lo necesitarás en pasos posteriores para verificar tu servicio en vivo.
3. Validación automatizada del paso 1 (opcional)
Verifica que se cumplan los requisitos previos y la configuración del paso 1:
just workshop-eval 1
⏱️ Nota: Esta evaluación verifica los recursos activos de la nube y puede tardar unos minutos en finalizar. Es completamente opcional. Mientras se ejecuta en tu terminal, puedes continuar de inmediato con el siguiente paso sin esperar.
4. Paso 2: La referencia local, Mailpit y la incorporación del administrador
Nuestro punto de partida es una aplicación de blog de Rails 8 limpia y moderna que se ejecuta en localhost con SQLite, intercepción de correos electrónicos de Mailpit y ActiveStorage basado en disco.
1. Cómo iniciar la app de forma local
📁 Ten en cuenta el directorio. Este repo contiene la app de Rails en blog/ y Terraform en iac/. En el paso 1, te quedaste en la raíz del repo, por lo que todos los comandos del paso 2 se ejecutan desde
blog/: Gemfile, bin/rails y compose.yaml viven allí. Si los ejecutas desde la raíz, fallan con no configuration file provided: not found o falta Gemfile.
Ingresa a la app de Rails, confirma que estás en la versión fijada de Ruby, instala las gemas y configura la base de datos:
cd blog
ruby -v # must print 3.4.5 — see Step 0 if it does not
bundle install
bin/rails db:setup
🧯 Error de permiso de escritura en
/var/lib/gems/
o
ruby -v
¿ruby -v muestra el Ruby del sistema (p.ej., 3.3.x)? El sistema Vanilla Linux Ruby ignora por completo los archivos .ruby-version y trata de instalar gemas en todo el sistema. Asegúrate de que rbenv se cargue en tu shell de terminal activo:
eval "$(rbenv init - bash)" # or: eval "$(rbenv init - zsh)"
ruby -v # confirm it now outputs 3.4.5!
Nota: Si omitiste la instalación de la versión 3.4.5 en el paso 0, ejecuta rbenv install 3.4.5. O bien, simplemente usa el Modo A (Docker Compose) que se muestra a continuación (just compose-up), que ejecuta la pila de Ruby anclada por completo dentro de contenedores sin tocar las gemas del host.
💡 ¿Prefieres no pensar en ello? just install y just compose-up desde la raíz del repo hacen el cd blog por ti.
2. 🌱 Descubrimiento automático de semillas inteligentes y bootstrap de administrador (problemas núm. 21 y 25)
La semilla de la base de datos (db/seeds.rb) incluye la detección automática inteligente del entorno:
- Inspecciona tu adaptador de base de datos activo (SQLite vs. Postgres) y la configuración de almacenamiento.
- Detecta la etapa 0 (localhost) y crea automáticamente el usuario administrador inicial y la publicación pedagógica inicial:
[LOCAL BASELINE] Welcome to Rails 8 on Localhost!- Adjunto de imagen triste local lista para usar (
local_sad_image.png) con una marca de agua que te informa que el almacenamiento en disco local es efímero.
- Se activa automáticamente un correo electrónico de restablecimiento de contraseña a través de ActionMailer.
Verifica o vuelve a ejecutar la semilla (usa automáticamente tu GOOGLE_CLOUD_ACCOUNT de .env):
bin/rails db:seed # or from repo root: just seed
Inicia la pila de desarrollo local a través de Docker Compose:
docker compose up -d # from blog/ — or `just compose-up` from the repo root

La aplicación, el servidor SMTP local de Mailpit y el visualizador de bases de datos de Adminer ahora se ejecutan juntos en contenedores aislados:
- App de Rails: http://localhost:3000
- IU web de Mailpit: http://localhost:8025
- IU de Adminer DB: http://localhost:8081
🧯 Conflicto con el puerto 3000 o
server.pid
error? Si antes iniciaste un servidor de desarrollo nativo (bin/dev), deténlo con Ctrl+C. Si Docker indica que el puerto 3000 ya está asignado o encuentra un server.pid restante, limpia con lo siguiente:
just compose-down
rm -f blog/tmp/pids/server.pid
just compose-up
3. Experiencia de Mailpit y acceso de administrador
- Cómo interceptar correos electrónicos salientes con Mailpit: Abre
http://localhost:8025en tu navegador. ¡Mira la carpeta Recibidos local! Durantedb:seed, ActionMailer envió un correo electrónico de incorporación de administrador que Mailpit capturó de forma segura a nivel local sin tocar servidores ni credenciales de correo electrónico externos:
2. Accede al blog: Abre http://localhost:3000 en tu navegador.
- Puedes hacer clic en el vínculo para restablecer la contraseña directamente en el correo electrónico de Mailpit y establecer tu contraseña.
- Como alternativa, accede con el correo electrónico de tu cuenta de Google Cloud (de
.env) y la contraseña predeterminada:Ch4ng3m3!!1.
- Observa las insignias de telemetría visual:
- Observa el banner y las insignias amarillas del entorno en la IU:
Notice: Ephemeral Database Active (POSTGRESQL)y[EPHEMERAL DB / STORAGE] 💾 Local. - Observa la marca de agua de la publicación: El sello de la casa 🏠 (
nanobanana_stamp_local.pngen la esquina inferior derecha de la imagen de portada). Esto proporciona una confirmación visual inmediata de que tus recursos y tu base de datos están vinculados actualmente al almacenamiento local efímero.
- Observa el banner y las insignias amarillas del entorno en la IU:

💡 Sugerencia profesional opcional: Ejercicio de la consola de Rails ¿Quieres saber cómo interactúa Rails con los datos inicializados desde la CLI? Ingresa a la consola interactiva:
bin/rails console
Inspecciona tu usuario administrador inicializado de forma dinámica con tu variable de entorno:
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. Validación automatizada del paso 2
Verifica tu configuración de referencia local y de administrador:
just workshop-eval 2

5. La trampa: Contenedores sin estado
Los contenedores de Cloud Run no tienen estado y son efímeros. Si implementamos nuestra base de datos SQLite y el directorio storage/ local directamente en Cloud Run, todas las publicaciones y las imágenes subidas se borrarán de forma permanente cada vez que un contenedor se escale a cero o se reinicie.
En el siguiente paso, implementaremos intencionalmente esta configuración efímera en Cloud Run para presenciar de primera mano el choque sin estado.
5. Paso 3: Implementación 1: El impacto sin estado (¡WOW inicial en 3 minutos!)
En lugar de esperar entre 15 y 20 minutos para que las bases de datos en la nube estén disponibles antes de ver algo en la Web, el desarrollo moderno sin servidores comienza con una victoria temprana: implementar nuestra aplicación de Rails de un solo contenedor directamente en Google Cloud Run en menos de 3 minutos.
1. El Time-Machine Rewind
Para garantizar que nuestra configuración inicial sea 100% efímera (base de datos SQLite local y almacenamiento de archivos en disco local), usa el motor Zero-Branch Time-Machine:
just workshop-rewind 1
💡 ¿Qué acaba de suceder?workshop-rewind 1 aplicó la capa de configuración stage-1-stateless a blog/config/ sin salir de la rama main. Tu app está configurada con SQLite en el disco del contenedor y ActiveStorage en el sistema de archivos local.
2. Implementa Puma de un solo contenedor en Cloud Run
Implementa directamente desde el código fuente en Cloud Run. Modern Rails 8 genera automáticamente un Dockerfile oficial listo para producción de inmediato (con compilaciones de varias etapas, jemalloc y seguridad sin raíz). Cuando implementas desde la carpeta blog/, Google Cloud Build detecta este Dockerfile nativo directamente y aprovisiona un servicio administrado sin servidores:
# 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
Deberías ver un resultado de progreso similar al siguiente:

⏳ Esto debería tardar un par de minutos mientras Cloud Build empaqueta tu contenedor y aprovisiona la revisión de Cloud Run.
🧯 Solución de problemas y advertencias habituales
PERMISSION_DENIED: Build failed because the default service account is missing required IAM permissions
? Dos causas diferentes provocan el mismo error:
- gcloud acaba de habilitar una API para ti. Si la implementación imprimió
The following APIs are not enabled ... cloudbuild.googleapis.comy respondisteY, es posible que la compilación haya comenzado antes de que se propagaran los permisos nuevos. El propio mensaje de gcloud advierte que “esto tardará unos minutos”. Espera un minuto y vuelve a ejecutar el mismo comando, ya que suele funcionar. El paso 1 deterraform applynormalmente habilita esta API por ti, por lo que no deberías ver el mensaje. - A la cuenta de servicio de Compute predeterminada realmente le faltan roles. Los proyectos de GCP modernos aplican el principio de privilegio mínimo, lo que interrumpe las implementaciones de origen. Soluciona el problema con el siguiente comando:
just project-status # grants storage.admin, logging.logWriter, artifactregistry.writer, cloudbuild.builds.builderjust workshop-testtambién informa esto ahora, por lo que vale la pena volver a ejecutarlo si la implementación sigue fallando.
Durante la implementación:
- Cloud Run compila tu imagen de contenedor de Rails.
- Se asigna un dominio TLS público y seguro:
https://blog-[hash]-[region].a.run.app. - El tráfico comienza a enrutarse a Puma en el puerto 8080.
3. ✨ El momento WOW inicial
Abre la URL de Cloud Run generada en tu navegador.
- Tu aplicación moderna de Rails 8 ya está disponible en Google Cloud.
- Accede con tus credenciales de administrador (
GOOGLE_CLOUD_ACCOUNTyAPP_ADMIN_PASSWORD). Si no encuentras el botón de acceso, haz clic en "New Post" y se te solicitarán el usuario y la contraseña primero:
- Haz clic en "Nueva entrada", escribe un artículo titulado "Mi primera entrada de Cloud Run", adjunta una imagen y haz clic en Crear entrada.
- Tu publicación se publicará con el formato completo y se renderizará tu imagen.
- Observa la telemetría visual:
- Distintivo del encabezado:
[EPHEMERAL DB / STORAGE] 💾 Local - Marca de agua de imagen: El sello de 🏠 (
127.0.0.1insignia de disco efímero en la esquina inferior derecha).
- Distintivo del encabezado:
🐝 ¡Únete a la tabla de clasificación de Hive de los talleres en vivo! Si estás en línea y tu supervisor muestra el ranking, y quieres unirte a él, agrega tu URL de Cloud Run aquí: 👉 Registra tu Cloud Run en el ranking

4. 💥 La trampa: El shock sin estado y la "solución provisoria de Puma"
Cloud Run es una plataforma sin servidores y sin estado. Cuando el tráfico web se reduce a cero, Cloud Run reduce la escala a cero instancias de contenedor para ahorrar dinero. Cuando llega una nueva solicitud HTTP o se implementa una nueva revisión del contenedor, Cloud Run inicia una imagen de contenedor nueva y limpia.
Banner de trabajos bloqueados
Cuando creas una publicación o adjuntas una imagen en una implementación de un solo contenedor, Rails pone en cola tareas de ActiveJob (como el análisis de dimensiones de imágenes o la indexación de metadatos). Sin embargo, como nadie ejecuta un trabajador en segundo plano, verás el banner de advertencia:

La solución tentadora: Ejecutar Solid Queue dentro de Puma
Un desarrollador inteligente podría decir: "¡Espera! Rails 8 nos permite ejecutar Solid Queue directamente dentro del proceso del servidor web de Puma habilitando
SOLID_QUEUE_IN_PUMA=true
variable de entorno
Probemos esta solución alternativa en 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
Ahora, vuelve a tu navegador y actualiza la página:
💥 The Stateless Shock:
- Buenas noticias: El trabajador de Solid Queue ahora está activo dentro de Puma. Los trabajos nuevos se agotan de inmediato.
- La ducha fría (¡la trampa!):
- Como Cloud Run implementó una revisión nueva, se reemplazó la instancia de contenedor anterior.
- El artículo que escribiste y el archivo de la base de datos SQLite en el disco se borraron por completo.
- La lección de arquitectura: Ejecutar trabajadores en segundo plano dentro de Puma consume valiosos recursos de CPU/RAM de subprocesos web, y aún no resuelve la persistencia.
5. Validación automatizada del paso 3
Verifica tu implementación de Cloud Run y el control de alertas:
just workshop-eval 3
✨ La lección: Las soluciones alternativas de un solo contenedor, como SOLID_QUEUE_IN_PUMA, son útiles para el desarrollo local, pero en la arquitectura de nube moderna:
- Los archivos multimedia requieren Google Cloud Storage (GCS) (paso 4).
- Los datos relacionales y las colas de trabajos requieren Google Cloud SQL administrado (paso 5).
- Los trabajadores en segundo plano pesados pertenecen a un contenedor sidecar dedicado (paso 6).
6. Paso 4: Implementación 3: Almacenamiento persistente de GCS y advertencia de POLA
En este paso, desacoplamos el almacenamiento de archivos y contenido multimedia del disco del contenedor. Para ello, cambiamos ActiveStorage a Google Cloud Storage (GCS) y usamos URLs firmadas de corta duración a través de la API de IAM Credentials (iam: true).
Antes de modificar cualquier configuración, debes comprender los tres elementos que Rails necesita para comunicarse con GCS:
① ID del proyecto y nombre del bucket: Rails debe saber dónde almacenar los BLOBs. La app lee GOOGLE_CLOUD_PROJECT del entorno y deriva el nombre del bucket como ${GOOGLE_CLOUD_PROJECT}-activestorage-prod. Tu Terraform del paso 1 ya creó este bucket, por lo que no se requiere trabajo adicional.
② Una cuenta de servicio con los permisos adecuados: Cloud Run se ejecuta como la cuenta de servicio predeterminada de Compute Engine (PROJECT_NUMBER-compute@developer.gserviceaccount.com). Esta SA necesita dos roles de IAM en el bucket:
roles/storage.objectAdmin: Para subir y entregar BLOBsroles/iam.serviceAccountTokenCreatoren sí misma: Para llamar a la API de IAM CredentialssignBloby generar URLs firmadas de corta duración
⚠️ El atajo tentador:
public: true La forma más rápida de hacer que GCS funcione es establecer public: true en storage.yml y otorgar allUsers:objectViewer en el bucket. Las imágenes se cargan al instante, sin necesidad de acceder. No lo hagas. Un bucket público filtra todo el contenido multimedia del usuario que se sube a Internet abierto, para siempre. Nuestro blueprint usa iam: true para mantener el bucket 100% privado. Cada URL de imagen es un token firmado y con fecha de vencimiento que genera sobre la marcha la API de IAM Credentials.
③
ACTIVE_STORAGE_SERVICE=google: Es la única variable de entorno que cambia Rails del disco local a GCS en el tiempo de ejecución. No se requiere ningún cambio de código ni volver a implementar la lógica de la aplicación, solo un botón de activación de la variable de entorno.
1. El Time-Machine Rewind
Avanza la máquina del tiempo a la etapa 2:
just workshop-rewind 2
Inspecciona blog/config/storage.yml. Verás que la app define tres entornos de GCS con nombre (dev / test / prod), todos con el mismo patrón seguro:
<% 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

2. Cómo otorgar permisos de IAM de almacenamiento y firma
Asegúrate de que tu cuenta de servicio de tiempo de ejecución de Cloud Run tenga permisos para firmar URLs y subir objetos:
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. Tercera implementación en Cloud Run con GCS adjunto
Vuelve a implementar con GCS habilitado. Asegúrate de estar dentro de la carpeta blog/ (de lo contrario, Cloud Run intentará compilar con Buildpacks en lugar de Dockerfile de Rails 8, lo que provocará errores de compilación).
Usa --update-env-vars (no --set-env-vars) para conservar las variables de entorno existentes (clave principal, configuración de IAP, etcétera):
cd blog
gcloud run deploy blog \
--source . \
--region $GOOGLE_CLOUD_REGION \
--update-env-vars ACTIVE_STORAGE_SERVICE=google
💡 Sugerencia: Si falla la implementación, haz clic en el vínculo de los registros de Cloud Build que se proporciona de forma conveniente en el resultado de la terminal (como se muestra en la siguiente figura). También puedes pedirle ayuda a Antigravity para descifrar y corregir el error.

4. ✨ La imagen de supervivencia y el sello de Cloud
- Actualiza la URL de tu blog.
- Crea una nueva entrada de blog titulada "Sobrevivir en la nube" y sube una foto.
- Mira la esquina inferior derecha de la imagen:
- ¡Desapareció el sello de 🏠!
- Se reemplaza por el colorido sello de Cloud GCS (
nanobanana_stamp_cloud.png).
- Fuerza otro reinicio del contenedor (que debería activar una cuarta revisión):
gcloud run services update blog --region $GOOGLE_CLOUD_REGION --update-env-vars RESTART_TRIGGER=$(date +%s) - Actualiza la página: mientras se restablece la base de datos de SQLite, el archivo binario de la imagen está sano y salvo en Google Cloud Storage. Verifica con la CLI:
gcloud storage ls -l -r gs://$GCS_BUCKET/
A continuación, se muestra una lista de los objetos binarios grandes y los objetos multimedia subidos que podrías ver en la consola del navegador de Google Cloud Storage en tu bucket de producción (gs://$GCS_BUCKET):

5. ⚠️ La trampa: Banner de advertencia sobre trabajos bloqueados
Cuando subiste la imagen, ActiveStorage puso en cola un trabajo de análisis (ActiveStorage::AnalyzeJob) para extraer dimensiones y metadatos.
Sin embargo, Cloud Run actualmente solo ejecuta un solo contenedor web (puma). En Rails 8, Solid Queue almacena trabajos en segundo plano en la base de datos, pero nadie ejecuta bundle exec rails solid_queue:start.
Mira la parte superior de la página de tu blog: verás un banner de advertencia brillante renderizado por blog/app/views/layouts/_check_stuck_jobs.html.erb.
Resolveremos este problema correctamente en el paso 6, cuando pasemos a una arquitectura de sidecar de varios contenedores de nivel de producción con un contenedor worker dedicado que ejecute Solid Queue de forma independiente del tráfico web.
6. Validación automatizada del paso 4
Verifica la configuración de GCS y la telemetría de los trabajos atascados:
just workshop-eval 4
7. Paso 5: Lista de tareas previas al vuelo: Cloud SQL, Secrets y conectividad de la base de datos
Mientras dominabas los contenedores sin estado y Google Cloud Storage en los pasos 3 y 4, Terraform terminó de cocinar tu backend de producción en segundo plano.
Antes de implementar nuestra arquitectura completa de sidecar de producción con 3 contenedores (Implementación 4 en el paso 6), este paso actúa como una inspección previa al vuelo rápida para verificar el estado, la conectividad y el acceso secreto de la base de datos.
0. Variables de entorno de arranque
Exporta todas las variables de shell necesarias directamente desde los resultados de 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. Verificación de estado de Cloud SQL
Verifica que tu instancia de PostgreSQL administrada haya terminado el aprovisionamiento inicial y esté en buen estado:
echo "Checking instance: $SQL_INSTANCE_NAME"
gcloud sql instances describe $SQL_INSTANCE_NAME --format="value(state)"
El resultado debe ser RUNNABLE.
2. (Opcional) Prueba la conectividad de la base de datos desde tu laptop
En producción, los contenedores de Cloud Run se conectan de forma segura a Cloud SQL a través de un proxy sidecar encriptado (127.0.0.1:5432) sin exponer la base de datos a Internet pública (0.0.0.0/0).
ℹ️ Nota: Este paso es completamente opcional. En Cloud Run, el contenedor secundario oficial del proxy de autenticación de Cloud SQL controla la conectividad de mTLS de forma automática. No es necesario que instales nada de forma local para completar el taller.
Si tienes instalados cloud-sql-proxy y psql en tu máquina y deseas verificar la conectividad directa desde tu laptop, haz lo siguiente:
# Optional: test connectivity via Google-managed mTLS proxy
gcloud sql connect $SQL_INSTANCE_NAME --user=rails_user --database=rails_production
(Si se te solicita una contraseña, ingresa
$DB_PASSWORD
. De lo contrario, puedes ir directamente al paso 3 a continuación).
3. Inspecciona los Secrets aprovisionados por Terraform
Nunca almacenes contraseñas de bases de datos, claves de API o claves principales de Rails en texto sin formato en Git ni en variables de entorno de contenedores. En la arquitectura de nube moderna, usamos Google Cloud Secret Manager para la inserción en el tiempo de ejecución de confianza cero.
ℹ️ Terraform se encargó de esto por ti en el paso 1:
rails-master-key: Para desencriptar las credenciales de Rails durante el tiempo de ejecución.rails-db-password: Es una contraseña aleatoria criptográficamente segura que coincide con el usuario de Cloud SQL.rails-admin-password: Una contraseña segura para tu usuario administrador inicial.
Verifica que tus secretos estén presentes y activos en Secret Manager:
gcloud secrets list --filter="name:rails-"

🔑 Sincronización de master.key con tu laptop (sincronización local)
Debido a que blog/config/master.key se ignora en git por motivos de seguridad, es posible que tu clon local aún no lo tenga. Ejecuta este fragmento de código de recuperación automática fácil de usar para extraerlo de Secret Manager, de modo que tus herramientas locales de Rails puedan desencriptar las credenciales:
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
🧪 Zona de pruebas: ¿Quieres crear un secreto personalizado solo para ver lo fácil que es? Secret Manager no es solo para Rails. Puedes crear cualquier secreto con un simple comando de la CLI:
echo -n "MySuperSecretValue123!" | gcloud secrets create workshop-fun-secret --data-file=-
echo "✨ Created workshop-fun-secret in Secret Manager! 🪄"
4. Cómo verificar los permisos de Secret Accessor
Para que los contenedores de Cloud Run activen estos secretos como variables de entorno o activaciones de volúmenes durante el inicio, la cuenta de servicio del entorno de ejecución ($RUN_SA) necesita el rol roles/secretmanager.secretAccessor en cada secreto.
Terraform ya otorgó este permiso durante el paso 1. Verifica la vinculación de la política de IAM en 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"
Deberías ver lo siguiente:
ROLE MEMBERS
roles/secretmanager.secretAccessor serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com

5. Validación previa al vuelo automatizada
Ejecuta el paquete de pruebas automatizado del paso 5 para confirmar que tu base de datos se puede ejecutar, que los secretos no están vacíos y que los permisos de IAM están en verde:
just workshop-eval 5
8. Paso 6: Implementación 4: Sidecars de varios contenedores empresariales (el estándar de oro)
Ahora, ensamblaremos la arquitectura de referencia definitiva: Cloud Run con varios contenedores.
1. Restablecimiento del estándar de oro
Restablece la configuración del repositorio al estado canónico main:
just workshop-restore-gold
2. Inspección del plano de producción de 3 contenedores
Abre blog/compose.prod.yaml y examina la arquitectura:

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)]
La implementación en producción ejecuta tres contenedores coordinados que comparten la misma red local:
web: Publica tráfico HTTP en el puerto 8080 a través de Puma.worker: Contenedor de procesamiento en segundo plano dedicado que ejecutabundle exec rails solid_queue:start.cloudsql-proxy: Complemento oficial del proxy de autenticación de Cloud SQL de Google Cloud (gcr.io/cloud-sql-connectors/cloud-sql-proxy:2) vinculado a127.0.0.1:5432.
3. Ejecuta migraciones de bases de datos a través de un trabajo de Cloud Run
Antes de enrutar el tráfico web a nuestra nueva implementación, debemos ejecutar migraciones de bases de datos y la propagación inicial en Cloud SQL.
⚖️ Cómo ejecutar migraciones en contenedores: Dos estrategias
En los entornos de nube contenerizados, existen dos enfoques comunes para ejecutar migraciones de bases de datos:
- Opción A: Automática dentro del punto de entrada de Docker (
entrypoint.shejecutadb:migrateen el inicio del contenedor)- Ventajas: Es completamente automático, por lo que los desarrolladores no tienen que recordar ejecutar un comando.
- Desventajas: Es peligroso en el ajuste de escala automático sin servidores. Cuando Cloud Run aumenta la escala a 10 instancias de forma simultánea en condiciones de tráfico alto, 10 contenedores compiten para ejecutar migraciones de forma simultánea, lo que provoca bloqueos de bases de datos, condiciones de carrera de esquemas o tiempos de espera de inicio.
- Opción B: Explícita a través del trabajo de Cloud Run (nuestra elección arquitectónica)
- Ventajas: Control completo, seguridad transaccional y cero condiciones de carrera. Se ejecuta como una tarea de contenedor dedicada de un solo uso completamente separada del tráfico web de entrega. Si falla una migración, no se ve afectada la publicación web.
- Desventajas: Requiere que se active un trabajo de CLI o un paso de CI/CD antes de la implementación.
Elegimos la opción B para obtener la máxima confiabilidad en la producción.
⚠️ Crítico: DATABASE_URL Formato y RFC 3986 de Ruby 3.4.5
Los Cloud Run Jobs se conectan a Cloud SQL con el proxy de socket Unix nativo (/cloudsql/PROJECT:REGION:INSTANCE). El DATABASE_URL debe usar el formato de triple barra:
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
💡 Multi-Database de Rails 8: La app usa 4 bases de datos (principal, de cola, de caché y de cable) por database.yml. Todas las 4 variables de entorno DATABASE_*_URL deben apuntar a la misma instancia de Cloud SQL. De lo contrario, no se crearán las tablas de Solid Queue/Cache/Cable.
Después de db:prepare, carga los esquemas de 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
En la figura anterior, puedes ver un posible resultado de
gcloud run jobs executions describe $JOBNAME
Consola de Google Cloud Run Jobs que muestra
rails-migrate
execution Succeeded
4. Cuarta implementación: Implementa Cloud Run con varios contenedores a través de Docker Compose
Ahora implementa la pila de producción genuina de 3 contenedores (web, worker, cloudsql-proxy) directamente desde 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
Consola de Google Cloud Run: Pestaña "Contenedores" que muestra los tres contenedores complementarios (
cloudsql-proxy
,
web
,
worker
)
💡 Curiosidad de la IU o error de la consola: En el menú desplegable de la consola, es posible que Cloud Run etiquete la primera entrada como Ingress container: cloudsql-proxy. ¡No te preocupes! Este es solo un error visual de la IU en la consola de Google Cloud (se realiza un seguimiento en #149) causado por el orden de dependencia del contenedor. En realidad, web es el único contenedor que escucha en el puerto HTTP 8080 y controla el 100% del tráfico web de entrada, mientras que cloudsql-proxy solo entrega tráfico privado de PostgreSQL en el puerto 5432.
5. ✨ El momento sorpresa: ¡Cero advertencias y persistencia completa en la nube!
Respira hondo y maravíllate con tu pantalla: ¡desaparecieron todos los banners de advertencia! 🪄
El blog de producción de Rails 8: IU impecable, cero banners de advertencia y estado persistente de Cloud SQL
Ahora la aplicación es oficialmente nativa de la nube y apta para producción:
- Con el respaldo de Google Cloud SQL (PostgreSQL) administrado a través de un túnel seguro de mTLS.
- Los medios se almacenan de forma segura en Google Cloud Storage privado con firma de BLOB de IAM.
- No hay advertencias de almacenamiento efímero ni alertas de trabajos atascados.
Abre tu URL de Cloud Run:
- Consulta las insignias de telemetría:
[CLOUD PERSISTENT 🐘 ☁️]se verificó en la inicialización de la base de datos.- Las insignias de Google Cloud Run y GCS en el pie de página se muestran en verde.
- El banner de advertencia sobre trabajos atascados desapareció por completo.
- Crear entradas de blog y comentarios
- Reinicia o vuelve a implementar la cantidad de veces que quieras: tus datos, publicaciones, comentarios y recursos perduran para siempre en Cloud SQL y GCS.
6. Validación automatizada del paso 6
Verifica la configuración de varios contenedores:
just workshop-eval 6
9. Paso 7: IA generativa Pipelines, Podcastifier y la búsqueda del tesoro de GCS 🏴☠️
Con Solid Queue ejecutándose en un contenedor dedicado y Google Cloud Storage activo, podemos liberar la IA generativa asíncrona.
1. El generador de portadas vintage de NanoBanana (canalización de IA generativa lista para usar)

Buenas noticias: Esta función ya está completamente implementada en la base de código. No necesitas escribir código. Tu tarea aquí es activar y observar una canalización asíncrona de IA generativa en la nube en acción.
🧠 Cómo funciona detrás de escena
- Autenticación: No se requieren claves de API. Utiliza credenciales predeterminadas de la aplicación (
roles/aiplatform.useren Vertex AI) automáticamente a través de la cuenta de servicio del entorno de ejecución de Cloud Run. (Como alternativa, si se proporcionaGEMINI_API_KEY, se llama directamente a la API de Gemini). - La canalización: Cuando se crea un artículo sin una imagen de portada, una devolución de llamada
after_commitpone en colaGenerateCoverImageJoben Solid Queue. - Modelo: Le indica al modelo multimodal de Google (Gemini 2.5 Flash Image / Imagen en Vertex AI) que genere un auténtico póster de película italiana de la década de 1960 ("Locandina di un film 1960") con un plátano como cameo y un brillante rubí "8".
- Marca de agua de procedencia dinámica: Usa
libvipspara agregar dinámicamente una insignia visual en la esquina (sello de nube colorido en GCS). - Intercambio en caliente de Turbo Stream: Cuando se adjunta la imagen,
Turbo::StreamsChannel.broadcast_refresh_to(post)actualiza el navegador del lector en vivo sin necesidad de volver a cargar la página de forma manual.

📐 Ver la fuente del diagrama de secuencia de 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!
🧪 ¡Pruébalo en vivo!
Puedes activarlo de dos maneras:
- Opción A (IU web): Haz clic en New post, ingresa un título como "Arquitectura sin servidores con Ruby on Rails 8", escribe contenido y deja vacío el campo de entrada del archivo de imagen de portada. Haz clic en Crear publicación.
- Opción B (CLI Runner): Ejecuta desde la terminal:
bin/rails runner 'Post.create!(title: "Serverless Architecture with Ruby on Rails 8", body: "Exploring asynchronous AI pipelines with Solid Queue and NanoBanana!")'
Espera entre 15 y 30 segundos y observa los registros del trabajador de Solid Queue en Cloud Run. El póster vintage aparecerá directamente en tu publicación.
📸 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. La misión de Podcastifier bilingüe (ejercicio práctico de programación antigravedad)
⚠️ Aviso importante: A diferencia del generador de portadas anterior, esta función AÚN NO se implementó.
Si abres blog/app/jobs/podcastifier_job.rb, encontrarás un código auxiliar de estructura vacío:
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
En este ejercicio práctico, tu misión es programar en parejas con Google Antigravity para completar esta función:
- Instrucción de antigravedad:"Ayúdame a implementar PodcastifierJob en
app/jobs/podcastifier_job.rb. Debería recuperar la publicación, traducir o sintetizar una descripción general de audio en italiano con Google Cloud Text-to-Speech (voz "it-IT-Wavenet-A" con el código de idioma "it-IT" a través de las credenciales de ADC) y adjuntar el archivo de audio apost.podcast_audio_itcon ActiveStorage". - Add Player to View: En
app/views/posts/show.html.erb, agrega un reproductorHTML5 cuando@post.podcast_audio_it.attached?. - Activa la síntesis: Cuando se guarda, Solid Queue ejecutará el trabajo de texto a voz de forma asíncrona en segundo plano.

🎧 Ejemplos de resultados de Podcastifier:
💡 ¿Necesitas ayuda o te atascaste? En la rama solutions/podcastifier, se encuentra disponible una solución de referencia completa y probada en batalla. Puedes inspeccionarlo en cualquier momento con git diff solutions/podcastifier.
3. [OPCIONAL] 🏴☠️ La búsqueda del tesoro de GCS (recuperación de blobs de la consola)
ℹ️ Verificación de requisitos previos: Local
cloud-sql-proxy
Se requiere la CLI En este ejercicio opcional, se conecta tu laptop local directamente a Google Cloud SQL a través de un túnel de mTLS encriptado. Comprueba si tienes instalado el proxy de la CLI:
which cloud-sql-proxy
- Si ya está instalado: Continúa con los pasos que se indican a continuación.
- Si NO está instalado y quieres completar esta misión, instálalo con el siguiente comando:
# 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 - De lo contrario: Puedes omitir la validación automatizada del paso 7.
¿Recuerdas la foto que subiste en el paso 4 antes de que el reinicio del contenedor borrara la base de datos efímera de SQLite? Ese archivo de imagen sigue almacenado de forma segura en tu bucket privado de GCS como un "blob huérfano".
Usemos la consola de Rails para rescatarlo y adjuntarlo a una publicación de 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
Dentro de la consola de 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): add screenshot of the interactive Rails console executing the GCS Treasure Hunt snippet and rescuing orphaned ActiveStorage blobs
Actualiza tu blog: La foto que subiste durante el Shock sin estado del paso 4 se recupera y se adjunta de forma permanente a tu publicación de Cloud SQL.
4. Validación automatizada del paso 7
Verifica los trabajos y recursos de IA generativa:
just workshop-eval 7
10. Paso 8: Elige tu propia aventura / Misiones avanzadas 🏆
Ahora que dominas la arquitectura de referencia canónica, elige tu misión de graduación.
🛡️ Quest 1: Zero-Trust Google IAP (Identity-Aware Proxy)
- Dificultad: Media (seguridad empresarial)
- El objetivo: Bloquear tu aplicación de Cloud Run para que solo tus cuentas verificadas de Google o Gmail puedan acceder a ella, y eliminar por completo el acceso con contraseña.
- Cómo funciona:
- Un balanceador de cargas de aplicaciones HTTPS externo finaliza la autenticación de Google OAuth y verifica la identidad antes de que el tráfico llegue a Cloud Run.
- Google reenvía encabezados de identidad verificada (
X-Goog-Authenticated-User-Email). - Rails accede automáticamente a la identidad verificada de Google a través de
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 - Habilita el módulo de Terraform en
iac/iap.tfconenable_iap = truey especifica las Cuentas de Google permitidas:iap_allowed_users = [var.google_cloud_account]

📊 Quest 2: Telemetría de SRE de producción y Cloud Logging
- Dificultad: Media (Observabilidad)
- El objetivo: Transmitir registros de aplicaciones JSON estructurados con IDs de correlación de seguimiento directamente a Google Cloud Logging y detectar excepciones de producción en tiempo real con Cloud Error Reporting.
- Cómo funciona:
- Configura
blog/config/environments/production.rbpara que emita registros JSON estructurados con etiquetas de seguimiento de GCP. - Activa una excepción de prueba intencional y observa cómo Error Reporting de Google Cloud te agrupa y notifica de inmediato.
- Configura
📸 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 Búsqueda semántica y RAG de Gemini
- Dificultad: Difícil (proyecto final de IA generativa)
- Objetivo: Buscar artículos de forma conceptual con embeddings de vectores almacenados en PostgreSQL en Cloud SQL.
- Cómo funciona:
- Habilita la extensión
vectoren tu instancia de Cloud SQL para PostgreSQL:CREATE EXTENSION IF NOT EXISTS vector; - Agrega la gem de
neighborablog/Gemfiley genera embeddings de texto con Gemini (text-embedding-004) enPost#after_save. - Realiza consultas de distancia coseno (
<=>) para potenciar una barra de búsqueda semántica con Turbo Streams.
- Habilita la extensión
📸 TODO(riccardo): add screenshot of the live blog semantic search bar returning conceptual matches with cosine similarity scores
⚡ Quest 4: Asistente de auditoría de SEO y rendimiento
- Dificultad: Fácil / Divertida (experiencia del desarrollador)
- El objetivo: Auditar las Métricas web esenciales, el Largest Contentful Paint (LCP) y los índices de legibilidad de Flesch-Kincaid y Fog con las herramientas Antigravity y Speedgrapher
📸 TODO(riccardo): add screenshot of Speedgrapher / Lighthouse SEO and Core Web Vitals audit summary
🤔 Curiosity Box: Cómo obtener una consola de Rails activa en Cloud Run
💡 La pregunta: En las VMs tradicionales o las plataformas de PaaS de estilo Heroku, los desarrolladores suelen conectarse a una instancia en ejecución a través de SSH o usar heroku run rails console para inspeccionar o corregir datos de producción en vivo. ¿Cuál es el equivalente nativo de Cloud Run y cómo lo puedes proteger?
Por qué
gcloud run exec
no existe para los servicios de Cloud Run
Cloud Run es una plataforma sin servidores: tu contenedor puede ejecutarse como cero, una o cientos de instancias idénticas, sin una identidad estable ni un shell direccionable. Esto es fundamentalmente diferente de Kubernetes (kubectl exec), en el que un Pod específico tiene un socket con nombre al que puedes adjuntar.
# ❌ 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
Los cuatro patrones nativos de GCP
Patrón | Comando | ¿Es interactivo? | Seguridad |
1. Proxy local + BD remota |
| ✅ Completa | 🔒 mTLS del proxy de Cloud SQL Auth |
2. Trabajo de Cloud Run (único) |
| ❌ Solo secuencia de comandos | 🔒 Se requiere un rol de IAM |
3. Gema de la consola web y IAP | Navegador → | ✅ Completa | 🛡️ IAP de confianza cero |
4. Cloud Shell + Proxy | Cloud Shell → | ✅ Completa | 🔒 Identidad de Google |
Patrón 1 (recomendado para la inspección de datos): Ejecuta una consola de Rails local que se comunique directamente con tu instancia de Cloud SQL PostgreSQL a través del proxy de Cloud SQL Auth:
# 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
Obtienes interactividad completa con IRB sin exposición a la red: el proxy de Cloud SQL usa mTLS a través de un socket de dominio de Unix o de bucle invertido.
Patrón 3 (consola web + confianza cero de IAP, la ruta del navegador reforzado):
La gema web-console proporciona una sesión de IRB basada en el navegador. Sin protección, es una vulnerabilidad de seguridad crítica, por lo que nunca debes exponerla públicamente. Sin embargo, cuando se combina con IAP de Google (Quest 1), la superficie de ataque se reduce: solo las identidades verificadas de Google en tu lista de entidades permitidas pueden acceder al extremo /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
⚠️ El patrón 3 requiere que primero se habilite la misión 1 (IAP). Sin ella, /rails/console en una URL pública equivale a dejar abierta la sala de servidores. Con IAP, se convierte en una potente herramienta para operadores reforzada por la confianza cero de nivel de Google.
Pregúntale a Antigravity: "Muéstrame cómo ejecutar
rails console --sandbox
de forma segura en mi base de datos de producción de Cloud SQL con el proxy de autenticación de Cloud SQL".
🏆 Graduación en The Hive: Prueba de trabajo validada por un supervisor
Una vez que completes la misión que elegiste, reclama tu Trofeo de graduación del paso 8 🏆 en el Ranking de The Hive:
- Envía tu misión en GitHub:
- Abre un problema nuevo en el repositorio oficial: github.com/palladius/rails8-app-on-gcp/issues/new
- Formato del título:
🎓 [Step 8 Completed]: - En el cuerpo del correo electrónico, incluye una breve descripción de lo que lograste y un vínculo a tu implementación activa de Cloud Run.
- Conecta tu app de Cloud Run a tu problema:
- Anota el número de problema (p.ej.,
88o la URL completahttps://github.com/palladius/rails8-app-on-gcp/issues/88). - Inserta la variable de entorno
STEP_8_GHIen tu servicio de Cloud Run:gcloud run services update blog \ --region europe-west1 \ --update-env-vars STEP_8_GHI=<YOUR_ISSUE_NUMBER> - Verifica que tu extremo
/status.jsonahora informe tu misión: Deberías ver lo siguiente: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" }
- Anota el número de problema (p.ej.,
- Obtén la revisión de tu supervisor y desbloquea el trofeo dorado:
- Consulta la tabla de clasificación de The Hive: En tu fila, ahora se mostrará una insignia ámbar:
⏳ GHI #XX review pending. - Pídele a los supervisores del taller (@palladius, @emilianodellacasa o @ricc) que inspeccionen tu implementación.
- En cuanto un supervisor comente
LGTMsobre tu problema, The Hive automáticamente aumentará tu progreso a 8/8 🏆 con una insignia dorada o púrpura brillante y un trofeo permanente que se vinculará directamente a tu problema de proyecto final.
- Consulta la tabla de clasificación de The Hive: En tu fila, ahora se mostrará una insignia ámbar:
11. Conclusión y limpieza 🎓
¡Felicitaciones! 🎉
Compilaste e implementaste una aplicación de Rails 8 lista para producción y para empresas en Google Cloud Platform:
- Progresión sin ramas: Dominar los flujos de trabajo modernos con superposiciones de Time Machine en
main - Cloud Storage: Almacenamiento de objetos privado y escalable con firma de BLOB de IAM (
iam: true). - Cloud SQL: PostgreSQL administrado y protegido con túneles mTLS del proxy de autenticación de Cloud SQL.
- Secret Manager: No hay filtraciones de credenciales de texto sin formato ni de archivos
.env. - Cloud Run Multi-Container: Archivos adicionales aislados de Puma web, Solid Queue worker y proxy.
- IA generativa: Generación asíncrona de pósteres vintage y síntesis de podcasts de texto a voz con Vertex AI.
- Misiones: IAP de confianza cero, observabilidad de SRE y embeddings de pgvector
🧹 Limpieza de recursos
Para evitar que se apliquen cargos después de completar el taller, asegúrate de limpiar tus recursos de Google Cloud. Elige el enfoque que mejor se adapte a tu entorno:
Opción 1: Desmantelamiento de Terraform (recomendado / predeterminado 🟢)
Usa esta opción si utilizas un proyecto de Google Cloud existente o compartido en el que residen otros recursos o datos. Destruye quirúrgicamente solo los recursos aprovisionados durante este taller (Cloud Run, Cloud SQL, Secret Manager y buckets de GCS), y deja el resto de tu proyecto intacto.
cd iac
terraform destroy -auto-approve
Opción 2: Eliminación total del proyecto (sin dejar rastros 🌪️)
Usa esta opción si creaste un proyecto de taller dedicado (p.ej., con créditos de taller o un entorno de pruebas) y deseas garantizar que no quede ningún rastro, incluidos los registros, los metadatos, las cuentas de servicio y los vínculos de facturación.
gcloud projects delete $GOOGLE_CLOUD_PROJECT --quiet