En 02-07 se escribió una decisión y se dejó pendiente. DA-001: el catálogo web de AlpinaShop debe pasar a Cloud Run. Se justificó con una tabla, se firmó, y se guardó en el README de alpinashop-catalogo. Desde entonces el curso ha vuelto a ella una docena de veces —en el balanceador de 03-02, en los secretos de 03-06, en Pub/Sub push de 04-04, en Cloud Functions de 06-03, en el agente de operaciones de 06-04— siempre con la misma coletilla: "en 07-02".

Esta es 07-02. Aquí se cumple.

No es un capricho de coherencia narrativa: es que hasta ahora no estaban las condiciones. Mover el catálogo de producción a otra plataforma sin un pipeline que construya imágenes reproducibles, sin observabilidad para saber si ha ido bien, sin infraestructura como código para revertir y sin un balanceador delante que absorba el cambio, es una temeridad. Con las cuatro cosas hechas —módulos 3 y 6— el movimiento es una tarde de trabajo con red de seguridad.

Al terminar la lección, el catálogo de AlpinaShop servirá desde Cloud Run, detrás de la misma IP, el mismo dominio, el mismo certificado, la misma CDN y el mismo WAF que tenía; habrá pasado por un despliegue canario al 10 %; el MIG alpinashop-web-mig estará apagado; y la factura de cómputo del catálogo habrá bajado de unos 50 € al mes a menos de 10. Y sabrás por qué la concurrencia es el parámetro que más gente configura mal.

Contenido

  1. Qué es Cloud Run y por qué importa el contrato del contenedor
  2. Servicios y trabajos (jobs): el criterio para elegir
  3. Desplegar el catálogo: gcloud run deploy bandera a bandera
  4. El mismo servicio en Terraform
  5. Revisiones y reparto de tráfico: canario, etiquetas y reversión
  6. Concurrencia: el concepto que más se malinterpreta
  7. Escalado: cero, mínimo, máximo y el modelo de facturación de CPU
  8. Conectar Cloud Run con el resto de la plataforma
  9. Identidad y seguridad del servicio
  10. Detrás del balanceador global: el NEG sin servidor
  11. La migración desde el MIG, paso a paso y sin corte
  12. Coste: el modelo y el cálculo comparado
  13. Límites y cuándo Cloud Run no encaja
  14. La tabla madura: Cloud Run vs Functions vs GKE vs Compute Engine
  15. Apagar alpinashop-web-mig y lo que eso simplifica

  1. Qué es Cloud Run y por qué importa el contrato del contenedor

Cloud Run ejecuta contenedores sin servidor. Le das una imagen, te da una URL HTTPS, y escala de cero a muchas instancias según el tráfico, cobrando por lo que uses.

Dicho así suena a App Engine con otro nombre, y no lo es. La diferencia está en la unidad de despliegue:

Plataforma Le entregas Consecuencia
App Engine estándar Código fuente + app.yaml Runtimes fijos, formato propietario, poco portable
Cloud Functions Una función y su firma Un punto de entrada; el runtime lo pone Google
Cloud Run Una imagen de contenedor Cualquier lenguaje, cualquier binario, cualquier dependencia del sistema
GKE Manifiestos de Kubernetes Control total y complejidad total

Que la unidad sea un contenedor estándar es lo que hace a Cloud Run distinto y lo que resolvió el punto más delicado de DA-001: la portabilidad. La misma imagen que corre hoy en alpinashop-cluster correrá mañana en Cloud Run sin tocar una línea. Y si algún día hiciera falta salir, esa imagen corre en cualquier sitio donde haya un runtime de contenedores.

El contrato del contenedor

Cloud Run no ejecuta cualquier imagen de cualquier manera: exige cumplir un contrato, y es deliberadamente corto.

Regla Detalle Por qué
Escuchar en $PORT La variable de entorno PORT (por defecto 8080), en 0.0.0.0 La plataforma elige el puerto y enruta hacia él
Responder a HTTP/gRPC/WebSocket El tráfico entra por la red, no por eventos locales Es una plataforma de peticiones
Arrancar rápido Escuchar en el puerto en segundos, no en minutos Cada arranque en frío es latencia para un usuario
Sin estado en disco El sistema de ficheros es efímero y vive en memoria Las instancias nacen y mueren sin avisar
Sin procesos en segundo plano fuera de la petición Salvo con CPU siempre activa Por defecto la CPU se congela entre peticiones
Contenedor Linux x86-64 o arm64 Sin privilegios, sin acceso al núcleo Aislamiento multiinquilino

Esas seis líneas son todo. No hay que aprender ningún formato de despliegue nuevo, y esa es la razón por la que Cloud Run se ha convertido en la opción por defecto para aplicaciones web en Google Cloud.

Dos errores frecuentes contra el contrato, dichos ya:

# MAL: puerto fijo y solo escucha en localhost
app.run(host="127.0.0.1", port=5000)

# BIEN: el puerto lo dice la plataforma y se escucha en todas las interfaces
import os
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

El primero produce el error más común de los principiantes en Cloud Run: "The user-provided container failed to start and listen on the port defined provided by the PORT environment variable". Cuando lo veas, ya sabes qué mirar.

Cómo funciona por dentro

flowchart TB
    U["Usuarios"] --> LB["Frontend HTTPS gestionado<br/>o balanceador global"]
    LB --> CR["Cloud Run - servicio alpinashop-web"]
    subgraph CR
      direction LR
      R1["Revision 0042<br/>90% del trafico"]
      R2["Revision 0043<br/>10% del trafico"]
    end
    R1 --> I1["Instancia 1<br/>concurrencia 80"]
    R1 --> I2["Instancia 2"]
    R2 --> I3["Instancia 3"]
    I1 -.-> SQL["Cloud SQL<br/>alpinashop-pedidos"]
    I1 -.-> SM["Secret Manager"]
    I1 -.-> GCS["Bucket<br/>alpinashop-catalogo"]

Tres conceptos que hay que fijar desde ya:

  • Servicio: la entidad estable, con nombre y URL. alpinashop-web.
  • Revisión: una versión inmutable del servicio (imagen + configuración). alpinashop-web-00042-abc. No se modifica nunca: cada cambio crea una revisión nueva.
  • Instancia: un contenedor en ejecución de una revisión. Nacen y mueren solas.

La inmutabilidad de las revisiones es lo que hace posible el reparto de tráfico y la reversión instantánea del apartado 5.

  1. Servicios y trabajos (jobs): el criterio para elegir

Cloud Run tiene dos formas de ejecutar un contenedor, y elegir mal es una fuente clásica de arquitecturas raras.

Servicio (service) Trabajo (job)
Se dispara por Una petición HTTP entrante Una ejecución explícita (jobs execute, Scheduler, Workflows)
Debe escuchar en $PORT Sí No
Termina Nunca (vive mientras haya tráfico) Cuando el proceso acaba con código 0
Escala por Peticiones concurrentes --tasks: N tareas en paralelo
Duración máxima Hasta 60 minutos por petición Hasta 24 horas por tarea
Reintentos Los hace el cliente Integrados: --max-retries
Facturación Por tiempo de instancia Por tiempo de tarea

La regla de decisión, en una frase:

Si algo responde a alguien, es un servicio. Si algo hace una cosa y termina, es un trabajo.

Aplicado a AlpinaShop:

Carga Tipo Por qué
Catálogo web Servicio Responde peticiones de clientes
API interna de stock Servicio Responde al proceso de sincronización
Informe nocturno de ventas Trabajo Se ejecuta, produce un fichero, termina
Reindexado del buscador Trabajo Proceso por lotes, 20 minutos, sin cliente esperando
Migración de esquema de BD Trabajo Una ejecución, con reintentos y código de salida
Procesar imagen subida Cloud Function (06-03) Evento único, ya resuelto y funcionando

El antipatrón clásico —y se ve mucho— es montar el informe nocturno como un servicio con un endpoint /ejecutar-informe que llama Cloud Scheduler. Funciona, pero: tienes un endpoint HTTP que hay que proteger, la ejecución está limitada por el tiempo máximo de petición, no hay reintentos integrados, y si tarda de más el proceso se corta a mitad. Un trabajo elimina los cuatro problemas.

Un trabajo real de AlpinaShop, el informe nocturno de 04-06:

gcloud run jobs create informes-nocturnos \
  --image=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/informes:v3 \
  --region=europe-west1 \
  --service-account=sa-informes-nocturnos@alpinashop-prod.iam.gserviceaccount.com \
  --tasks=1 \
  --max-retries=2 \
  --task-timeout=30m \
  --memory=1Gi \
  --set-env-vars=DATASET=alpinashop_analitica

Y su ejecución desde Cloud Scheduler, sin exponer ningún HTTP propio:

gcloud scheduler jobs create http disparar-informes-nocturnos \
  --location=europe-west1 \
  --schedule="30 3 * * *" \
  --time-zone="Europe/Madrid" \
  --uri="https://run.googleapis.com/v2/projects/alpinashop-prod/locations/europe-west1/jobs/informes-nocturnos:run" \
  --http-method=POST \
  --oauth-service-account-email=sa-informes-nocturnos@alpinashop-prod.iam.gserviceaccount.com

Nótese --time-zone="Europe/Madrid": sin ella, el 30 3 * * * es UTC y el informe se ejecutará a las 5:30 en verano. Es un error que se descubre en marzo o en octubre y desconcierta durante horas.

  1. Desplegar el catálogo: gcloud run deploy bandera a bandera

La imagen del catálogo ya está en Artifact Registry desde 06-01, construida por Cloud Build y etiquetada con $COMMIT_SHA. Solo falta desplegarla. Este es el comando completo de AlpinaShop, y a continuación cada bandera explicada.

gcloud run deploy alpinashop-web \
  --image=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:a3f91c2 \
  --region=europe-west1 \
  --platform=managed \
  --service-account=sa-catalogo-web@alpinashop-prod.iam.gserviceaccount.com \
  --no-allow-unauthenticated \
  --port=8080 \
  --cpu=1 \
  --memory=512Mi \
  --concurrency=80 \
  --min-instances=1 \
  --max-instances=30 \
  --timeout=60s \
  --cpu-throttling \
  --execution-environment=gen2 \
  --set-env-vars=ENTORNO=prod,REGION=europe-west1 \
  --set-secrets=DB_PASSWORD=db-password-catalogo:latest \
  --add-cloudsql-instances=alpinashop-prod:europe-west1:alpinashop-pedidos \
  --labels=entorno=prod,equipo=desarrollo,centro-coste=tienda,aplicacion=catalogo \
  --no-traffic \
  --tag=candidata
Bandera Qué hace Por qué este valor en AlpinaShop
--image Imagen a desplegar Etiquetada con el hash del commit, nunca latest (06-01). Con latest no sabes qué hay corriendo
--region Región del servicio europe-west1, donde está todo lo demás. Cloud Run es regional
--platform=managed Cloud Run gestionado La alternativa era gke, en desuso (07-01)
--service-account Identidad del servicio Su propia cuenta con mínimo privilegio (03-04). Sin esto usa la cuenta por defecto de Compute, que es demasiado permisiva
--no-allow-unauthenticated Exige IAM para invocar El servicio no se expone a internet directamente: entrará por el balanceador (apartado 10)
--port Puerto del contenedor 8080, el mismo del contrato y de la sonda hc-catalogo
--cpu vCPU por instancia 1 vCPU. Se puede fraccionar (0.5, 0.25) o llegar a 8
--memory Memoria por instancia 512 MiB. Medida, no adivinada: ver apartado 6
--concurrency Peticiones simultáneas por instancia 80, el valor por defecto, validado midiendo (apartado 6)
--min-instances Instancias siempre encendidas 1, para que ningún cliente sufra arranque en frío
--max-instances Techo de instancias 30, como cortafuegos de coste y de conexiones a Cloud SQL
--timeout Tiempo máximo por petición 60 s. El máximo es 60 min, pero un catálogo que tarde 60 s ya está roto
--cpu-throttling CPU solo durante la petición Modo por defecto y el más barato
--execution-environment=gen2 Entorno de ejecución Segunda generación: compatibilidad completa con Linux, arranque algo más lento, necesario para montar volúmenes
--set-env-vars Variables de entorno Configuración no sensible
--set-secrets Secretos montados La contraseña nunca como variable en claro (03-06)
--add-cloudsql-instances Conector a Cloud SQL Habilita el socket Unix hacia alpinashop-pedidos
--labels Etiquetas Las cuatro de AlpinaShop desde 01-04, imprescindibles para 07-05
--no-traffic Despliega sin enviarle tráfico Crea la revisión pero no la activa. Clave para el canario
--tag=candidata Etiqueta de revisión Le da una URL propia para probarla antes de darle tráfico

Las dos últimas son las que convierten un despliegue en un despliegue seguro, y merecen su propio apartado.

  1. El mismo servicio en Terraform

AlpinaShop gestiona su infraestructura con Terraform desde 06-07, así que el comando anterior es para experimentar: lo que va a main es HCL.

# entornos/prod/cloud_run_catalogo.tf

resource "google_cloud_run_v2_service" "catalogo" {
  name     = "alpinashop-web"
  location = var.region                       # europe-west1
  project  = var.proyecto_prod

  # Solo trafico interno y del balanceador: nadie llega por la URL run.app
  ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"

  template {
    service_account = google_service_account.sa_catalogo_web.email

    scaling {
      min_instance_count = 1
      max_instance_count = 30
    }

    max_instance_request_concurrency = 80
    timeout                          = "60s"

    containers {
      image = "europe-west1-docker.pkg.dev/${var.proyecto_prod}/alpinashop/catalogo:${var.version_catalogo}"

      ports {
        container_port = 8080
      }

      resources {
        limits = {
          cpu    = "1"
          memory = "512Mi"
        }
        cpu_idle          = true    # CPU solo durante la peticion
        startup_cpu_boost = true    # CPU extra durante el arranque
      }

      env {
        name  = "ENTORNO"
        value = "prod"
      }

      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = "db-password-catalogo"
            version = "latest"
          }
        }
      }

      volume_mounts {
        name       = "cloudsql"
        mount_path = "/cloudsql"
      }
    }

    volumes {
      name = "cloudsql"
      cloud_sql_instance {
        instances = [google_sql_database_instance.pedidos.connection_name]
      }
    }
  }

  # Reparto de trafico explicito: 100% a la ultima revision estable
  traffic {
    type    = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
    percent = 100
  }

  labels = {
    entorno      = "prod"
    equipo       = "desarrollo"
    centro-coste = "tienda"
    aplicacion   = "catalogo"
  }
}

Cuatro observaciones importantes sobre este HCL:

  1. Usa google_cloud_run_v2_service, no google_cloud_run_service. El recurso v1 sigue existiendo por compatibilidad y su sintaxis (basada en anotaciones de Knative) es mucho peor. Todo lo nuevo va en v2.
  2. cpu_idle = true es el equivalente a --cpu-throttling: la CPU solo se factura durante las peticiones. false significa CPU siempre activa, y es hasta cuatro veces más caro.
  3. startup_cpu_boost = true da CPU adicional durante el arranque. Reduce el arranque en frío entre un 20 % y un 50 % en aplicaciones con inicialización pesada (JVM, frameworks grandes), y solo se paga ese medio segundo.
  4. ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" es la pieza de seguridad: nadie puede llegar al servicio por su URL *.run.app. Solo entra tráfico desde el balanceador global.

Y hay una tensión que conviene resolver explícitamente, porque es la pregunta que todo el mundo se hace al llegar aquí: si el despliegue lo hace el pipeline, ¿qué gestiona Terraform?

Aspecto Quién lo gestiona Motivo
Existencia del servicio, cuenta de servicio, ingress, secretos, escalado, VPC Terraform Es la forma; cambia por decisión humana
Versión de la imagen desplegada Cloud Build Cambia varias veces al día; es contenido
Reparto de tráfico entre revisiones Cloud Build durante el canario, Terraform en reposo Es una operación, no una configuración

Es exactamente la regla de 06-07: Terraform gestiona la forma, no el contenido. En la práctica se resuelve con lifecycle { ignore_changes = [template[0].containers[0].image, traffic] } en el recurso, para que Terraform no intente devolver la imagen a la versión de su fichero cada vez que corre.

  1. Revisiones y reparto de tráfico: canario, etiquetas y reversión

Aquí está una de las mejores capacidades de Cloud Run, y es prácticamente gratis.

Etiquetas de revisión: probar sin arriesgar

Al desplegar con --no-traffic --tag=candidata, Cloud Run crea la revisión y le asigna una URL propia:

https://candidata---alpinashop-web-x7fq2abc-ew.a.run.app

Esa URL apunta solo a esa revisión, con cero tráfico de producción. Sirve para:

  • Probar la versión nueva contra la base de datos real, sin que ningún cliente la vea.
  • Pasar las pruebas de humo automáticas del pipeline.
  • Enseñársela a alguien antes de publicarla.
# Prueba de humo contra la revision candidata, con token porque el servicio es privado
URL=$(gcloud run services describe alpinashop-web --region=europe-west1 \
        --format='value(status.traffic[?tag==`candidata`].url)')

curl -sf -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
     "$URL/salud" || { echo "FALLA LA SONDA"; exit 1; }

El canario al 10 %

Si la prueba pasa, se le da un porcentaje pequeño de tráfico real:

gcloud run services update-traffic alpinashop-web \
  --region=europe-west1 \
  --to-tags=candidata=10

A partir de ese momento, una de cada diez peticiones va a la revisión nueva. Se observa durante veinte o treinta minutos en el panel de 06-04 —tasa de error, latencia p95, errores en Error Reporting— y se decide.

Progresión completa recomendada:

Paso Tráfico Qué se observa Duración
1 0 % (solo etiqueta) Pruebas de humo 2 min
2 10 % Tasa de error 5xx, latencia p95 20-30 min
3 50 % Lo mismo, con volumen 30-60 min
4 100 % Estabilización —
gcloud run services update-traffic alpinashop-web --region=europe-west1 --to-tags=candidata=50
gcloud run services update-traffic alpinashop-web --region=europe-west1 --to-latest

La reversión instantánea

Y esta es la parte que cambia cómo se duerme por las noches. Si la revisión nueva falla:

# Ver las revisiones y su reparto actual
gcloud run revisions list --service=alpinashop-web --region=europe-west1

# Volver TODO el trafico a la revision anterior conocida como buena
gcloud run services update-traffic alpinashop-web \
  --region=europe-west1 \
  --to-revisions=alpinashop-web-00041-xyz=100

Tarda segundos y no reconstruye ni redespliega nada, porque la revisión anterior sigue existiendo íntegra. Compárese con el MIG: había que crear una plantilla de instancia nueva, lanzar una actualización progresiva y esperar a que las VM se sustituyeran una a una — entre cinco y quince minutos, con el sitio degradado mientras tanto.

Consejo operativo: ten el comando de reversión escrito en el runbook (07-06) con el número de revisión rellenado como variable. A las tres de la madrugada nadie recuerda la sintaxis de --to-revisions.

Dos advertencias sobre el canario

  1. El canario no protege contra cambios de esquema de base de datos. Si la revisión nueva hace ALTER TABLE, el 90 % que sigue en la vieja también ve la tabla cambiada. Las migraciones de esquema deben ser compatibles hacia atrás (añadir columnas, nunca renombrarlas en el mismo despliegue), y ejecutarse como un trabajo aparte.
  2. Con poco tráfico, el 10 % no significa nada estadísticamente. Si AlpinaShop recibe 30 peticiones por minuto fuera de campaña, el 10 % son 3 peticiones. Un canario que no ve errores durante 20 minutos con 60 peticiones no ha demostrado gran cosa. En volúmenes bajos conviene subir el porcentaje y alargar el tiempo, o hacer el canario en horas de más tráfico.

  1. Concurrencia: el concepto que más se malinterpreta

Si te llevas un solo apartado de esta lección, que sea este.

Concurrencia es el número máximo de peticiones que Cloud Run envía simultáneamente a una misma instancia antes de arrancar otra.

El valor por defecto es 80. Y aquí está la fuente de todos los malentendidos: mucha gente llega a Cloud Run desde Cloud Functions o desde AWS Lambda, donde el modelo es una petición por instancia, y asume que Cloud Run funciona igual. No.

Comparación honesta de los dos modelos

Una petición por instancia (--concurrency=1) Concurrencia alta (--concurrency=80)
Aislamiento Total: cada petición tiene su contenedor Compartido: 80 peticiones comparten memoria y CPU
Razonamiento Trivial: no hay concurrencia que gestionar Hay que pensar en variables globales y estado compartido
Coste Muy alto: una instancia por petición Bajo: 80 peticiones pagan una instancia
Arranques en frío Muchísimos: cada pico crea instancias Pocos
Rendimiento en E/S Malísimo: la CPU está ociosa esperando la BD Excelente: mientras una petición espera, otra usa la CPU
Aplica bien a Cargas de CPU intensiva, código no seguro para hilos, ML Aplicaciones web y APIs normales

La clave está en la penúltima fila. Una petición típica del catálogo de AlpinaShop pasa así sus 200 ms:

|-- 5 ms --|------------ 170 ms ------------|-- 25 ms --|
  parsear      esperando a Cloud SQL           render
   (CPU)          (CPU OCIOSA)                  (CPU)

El 85 % del tiempo la CPU no hace nada: espera a la base de datos. Con --concurrency=1, pagas una vCPU entera para que esté ociosa 170 ms de cada 200. Con --concurrency=80, esa misma vCPU atiende a otras peticiones durante esa espera.

Cómo elegirla midiendo

La concurrencia correcta no se adivina, se mide. El procedimiento:

Paso 1 — Averiguar el modelo de concurrencia de tu aplicación. Es lo primero y casi nadie lo comprueba:

Servidor Modelo Concurrencia útil
Flask con app.run() (desarrollo) Un hilo 1. No pongas esto en producción
Gunicorn con workers síncronos workers × 1 Igual al número de workers
Gunicorn con gthread workers × threads Ese producto
Node.js / Express Bucle de eventos Alta: 80-250
Go / Java con hilos Alta 80-1000
Python con asyncio (FastAPI + Uvicorn) Bucle de eventos Alta: 80-250

El error más caro de todos: poner --concurrency=80 en un servidor que solo procesa una petición a la vez. Cloud Run le manda 80, el servidor las encola, y las peticiones 2 a 80 esperan su turno. La latencia se dispara y parece un problema de la plataforma. Es un problema de configuración del servidor de aplicaciones.

Para el catálogo de AlpinaShop, que es Flask, el Dockerfile termina así:

# 4 procesos x 20 hilos = 80 peticiones simultaneas reales
CMD exec gunicorn --bind :$PORT \
                  --workers 4 \
                  --threads 20 \
                  --worker-class gthread \
                  --timeout 0 \
                  main:app

--timeout 0 desactiva el temporizador de Gunicorn porque el tiempo de espera lo gobierna Cloud Run con --timeout=60s. Tener dos temporizadores compitiendo produce cortes inexplicables.

Paso 2 — Prueba de carga con distintos valores. Se despliegan revisiones con etiqueta y se mide cada una:

for C in 1 10 40 80 160; do
  gcloud run deploy alpinashop-web \
    --image=$IMAGEN --region=europe-west1 \
    --concurrency=$C --no-traffic --tag=c$C
done

Y con una herramienta de carga (hey, k6, vegeta) contra cada URL etiquetada. Resultados reales del catálogo, con 50 peticiones por segundo sostenidas durante 5 minutos:

Concurrencia Instancias usadas Latencia p50 Latencia p95 Coste relativo
1 12 190 ms 240 ms 12×
10 5 195 ms 260 ms 5×
40 2 200 ms 290 ms 2×
80 1 210 ms 340 ms 1×
160 1 380 ms 1200 ms 1×

Lectura de la tabla, que es donde está el aprendizaje:

  • De 1 a 80, la latencia p50 apenas sube (190 → 210 ms) y el coste se divide por doce. Regalo evidente.
  • De 80 a 160 la latencia se dispara: se han superado los 80 hilos reales de Gunicorn y las peticiones se encolan. El techo no lo pone Cloud Run, lo pone tu aplicación.
  • El p95 se degrada antes que el p50. Por eso se mide el p95: es donde se ve el sufrimiento antes de que sea evidente.

Paso 3 — Ajustar CPU y memoria a la concurrencia elegida. Son parámetros acoplados:

  • Con concurrencia 80, la instancia necesita memoria para 80 peticiones simultáneas. Si cada una consume 4 MiB de estructuras, son 320 MiB solo de peticiones, más el runtime.
  • Si la aplicación es intensiva en CPU, la concurrencia alta hace que las peticiones compitan por la misma vCPU. Ahí se sube CPU o se baja concurrencia.

La señal de que la memoria se queda corta es inconfundible en los logs:

Memory limit of 512 MiB exceeded with 518 MiB used. Consider increasing the memory limit.

Cuando eso ocurre, la instancia muere y las peticiones en curso se pierden. Es de los pocos fallos de Cloud Run que se manifiestan como errores 5xx sin traza de la aplicación.

  1. Escalado: cero, mínimo, máximo y el modelo de facturación de CPU

Escalar a cero

Sin tráfico, Cloud Run apaga todas las instancias y no cobra nada. Es la propiedad que hace que un entorno de desarrollo cueste céntimos.

El precio de esa propiedad es el arranque en frío (cold start): la primera petición tras un periodo de inactividad tiene que esperar a que se cree una instancia, se descargue la imagen y arranque la aplicación.

Fase Tiempo típico Cómo se reduce
Aprovisionar el sandbox 100-300 ms No está en tu mano
Descargar la imagen 200 ms - 5 s Imagen pequeña. Es la palanca principal
Arrancar la aplicación 100 ms - 10 s Menos dependencias, carga perezosa, startup_cpu_boost

Números orientativos de arranque en frío completo: un binario de Go en una imagen distroless de 15 MB arranca en unos 300 ms; una aplicación Python con muchas dependencias, en 1-3 s; una JVM con Spring Boot sin ajustar, entre 8 y 20 s. La diferencia la marca la imagen y el runtime, no Cloud Run.

--min-instances: pagar por no tener frío

gcloud run services update alpinashop-web --region=europe-west1 --min-instances=1

Mantiene N instancias siempre vivas. La instancia inactiva se factura a una tarifa reducida —del orden de la décima parte de la activa—, así que una instancia mínima es barata: unos pocos euros al mes.

Criterio para AlpinaShop:

Servicio --min-instances Razón
alpinashop-web (prod) 1 Un cliente que llega a las 7:00 no debe esperar 2 s
alpinashop-web (dev) 0 Nadie sufre un arranque en frío en desarrollo
Servicios internos 0 Los llama software, que puede esperar
Durante la campaña de otoño 3 Amortiguar el arranque de la mañana

Advertencia importante: --min-instances reduce mucho los arranques en frío, pero no los elimina. Si el tráfico sube de golpe, hay que crear instancias nuevas y esas arrancan en frío igualmente. Y hay un detalle contraintuitivo: con CPU limitada a las peticiones (cpu_idle=true), las instancias mínimas tienen la CPU congelada entre peticiones, así que si tu aplicación hace algo periódico en segundo plano, no se ejecutará.

--max-instances: el cortafuegos de coste

gcloud run services update alpinashop-web --region=europe-west1 --max-instances=30

Esto es protección, y hay que ponerlo siempre. Dos motivos:

  1. Coste. Un bucle infinito en el código, un rastreador agresivo o un ataque pueden lanzar cientos de instancias. Sin techo, la factura del día es un incidente.
  2. Dependencias aguas abajo. Cloud SQL tiene un límite de conexiones. Si cada instancia abre 5 conexiones y arrancan 200 instancias, son 1000 conexiones contra una base de datos que admite 100: Cloud Run escala perfectamente y tumba la base de datos.

La fórmula para elegirlo:

max-instances = min(
    capacidad_de_la_dependencia_mas_debil / conexiones_por_instancia,
    pico_de_trafico_esperado / concurrencia * margen
)

Para AlpinaShop: Cloud SQL admite ~100 conexiones útiles, el pool por instancia es de 3 → 33 instancias. El pico esperado en campaña son 20 req/s, con concurrencia 80 → menos de 1 instancia, con margen ×5 para picos bruscos → 5. Se elige 30: cómodo para el tráfico, por debajo del límite de la base de datos.

Cuando se alcanza el máximo, Cloud Run no rechaza: encola las peticiones y las va sirviendo, y solo devuelve 429 si la cola se satura. Es un comportamiento amable, pero conviene tener una alerta:

gcloud monitoring channels list   # el canal de Marta, de 06-04
# Alerta cuando se supera el 80% del maximo durante 5 minutos

CPU siempre activa frente a CPU durante la petición

--cpu-throttling (por defecto) --no-cpu-throttling
CPU entre peticiones Congelada Disponible
Precio de la vCPU Base Del orden de 4 veces menor por segundo, pero se paga todo el tiempo de vida de la instancia
Procesos en segundo plano No funcionan Funcionan
Casos de uso Aplicaciones web normales Consumidores de streaming, servicios con caché que refresca sola, WebSockets con trabajo entre mensajes

Para el catálogo, --cpu-throttling es lo correcto: entre peticiones no hay nada que hacer. La trampa clásica es lanzar un hilo en segundo plano para "enviar métricas cada 30 segundos" y descubrir que no se ejecuta nunca. Con CPU congelada, ese hilo se para.

  1. Conectar Cloud Run con el resto de la plataforma

Un servicio aislado no vale nada. Estas son las cinco conexiones de AlpinaShop.

Cloud SQL: por conector, no por IP

--add-cloudsql-instances=alpinashop-prod:europe-west1:alpinashop-pedidos

Cloud Run monta un socket Unix en /cloudsql/<nombre-de-conexión> que habla con la instancia por un canal cifrado, sin IP pública y sin abrir firewall. La cadena de conexión de la aplicación:

import os, sqlalchemy

CONEXION = "alpinashop-prod:europe-west1:alpinashop-pedidos"

engine = sqlalchemy.create_engine(
    sqlalchemy.engine.url.URL.create(
        drivername="postgresql+pg8000",
        username="catalogo",
        password=os.environ["DB_PASSWORD"],       # inyectada desde Secret Manager
        database="tienda",
        query={"unix_sock": f"/cloudsql/{CONEXION}/.s.PGSQL.5432"},
    ),
    pool_size=3,           # bajo: multiplica por el numero de instancias
    max_overflow=0,        # sin conexiones extra sorpresa
    pool_pre_ping=True,    # detecta conexiones muertas tras una pausa
    pool_recycle=1800,
)

Las cuatro opciones del pool no son decorativas:

  • pool_size=3: recuérdese la fórmula de --max-instances. Un pool de 20 por instancia es la receta para agotar la base de datos.
  • max_overflow=0: impide que el pool abra conexiones adicionales bajo carga, que es justo cuando no te lo puedes permitir.
  • pool_pre_ping=True: imprescindible en Cloud Run. Una instancia con la CPU congelada durante minutos vuelve con conexiones muertas; sin pre-ping, la primera petición tras la pausa falla.
  • pool_recycle=1800: recicla conexiones cada media hora antes de que las corte el servidor.

La alternativa moderna es conectar por IP privada a través de la VPC (siguiente punto), que evita el conector y da menos latencia. Es lo que AlpinaShop hará cuando la red esté ordenada en 07-03.

Secret Manager: montado, no copiado

--set-secrets=DB_PASSWORD=db-password-catalogo:latest

La aplicación lee os.environ["DB_PASSWORD"] y no sabe que viene de Secret Manager. Alternativa, montarlo como fichero:

--set-secrets=/secretos/api-pago=api-key-pasarela-pago:latest

Diferencia práctica que conviene conocer: una variable de entorno se lee una vez al arrancar la instancia; un fichero montado se puede releer. Si rotas un secreto, las instancias vivas con variable de entorno siguen usando el valor viejo hasta que se reemplacen. Con :latest y ficheros, la rotación se propaga sola.

Y la contrapartida: usar :latest significa que una rotación mal hecha rompe el servicio sin que nadie despliegue nada. Para secretos críticos, fijar la versión (:7) y actualizarla en un despliegue controlado es más seguro.

VPC directa: salir por la red privada

Por defecto, el tráfico saliente de Cloud Run va a internet. Para llegar a recursos con IP privada —una réplica de Cloud SQL, Memorystore, o el ERP de Sabadell al otro lado del túnel de 07-03— hay que conectar el servicio a la VPC:

gcloud run services update alpinashop-web \
  --region=europe-west1 \
  --network=alpinashop-vpc \
  --subnet=sn-web-euw1 \
  --vpc-egress=private-ranges-only

Esto es VPC directa (Direct VPC egress), que sustituye al antiguo conector de acceso a VPC sin servidor (Serverless VPC Access connector):

Conector (antiguo) VPC directa (actual)
Infraestructura VM gestionadas que hay que dimensionar y pagar Ninguna
Coste Del orden de decenas de € al mes Sin coste adicional
Latencia Un salto extra Directa
Escalado Manual (número de instancias del conector) Automático

Si encuentras documentación que habla de conectores, es anterior. VPC directa es lo que hay que usar hoy.

El valor de --vpc-egress decide qué sale por la VPC:

Valor Efecto
private-ranges-only Solo los rangos RFC 1918 van por la VPC; internet sale directo. El habitual
all-traffic Todo sale por la VPC, incluido internet. Necesario si quieres que salga por el Cloud NAT alpinashop-nat-euw1 con IP fija (03-01), por ejemplo porque la pasarela de pago tiene lista blanca de IP

Pub/Sub push con OIDC

En 04-04 se dijo que el modelo push encaja con Cloud Run. Así se conecta, sin abrir el servicio a nadie:

gcloud pubsub subscriptions create pedidos-nuevos-a-catalogo \
  --topic=pedidos-nuevos \
  --push-endpoint=https://alpinashop-web-x7fq2abc-ew.a.run.app/eventos/pedido \
  --push-auth-service-account=sa-pubsub-invoker@alpinashop-prod.iam.gserviceaccount.com \
  --ack-deadline=60 \
  --dead-letter-topic=pedidos-nuevos-dlq \
  --max-delivery-attempts=5

Lo esencial es --push-auth-service-account: Pub/Sub firma cada entrega con un token OIDC de esa cuenta. El servicio, que está en --no-allow-unauthenticated, solo acepta la petición si esa cuenta tiene roles/run.invoker:

gcloud run services add-iam-policy-binding alpinashop-web \
  --region=europe-west1 \
  --member=serviceAccount:[email protected] \
  --role=roles/run.invoker

Ningún secreto compartido, ninguna cabecera mágica, ninguna comprobación en el código. La plataforma valida el token antes de que la petición llegue a tu contenedor.

Eventarc: reaccionar a lo que pasa en GCP

Eventarc entrega a Cloud Run eventos de más de un centenar de servicios de Google Cloud, con el formato estándar CloudEvents:

gcloud eventarc triggers create catalogo-imagen-nueva \
  --location=europe-west1 \
  --destination-run-service=alpinashop-web \
  --destination-run-path=/eventos/imagen \
  --event-filters="type=google.cloud.storage.object.v1.finalized" \
  --event-filters="bucket=alpinashop-catalogo" \
  --service-account=sa-eventarc@alpinashop-prod.iam.gserviceaccount.com

La relación con lo ya visto: Cloud Functions 2ª generación (06-03) está construida sobre Cloud Run y Eventarc. La función procesar-imagen-producto es, por debajo, un servicio de Cloud Run con un disparador de Eventarc. Saberlo explica por qué comparten límites, opciones de escalado y modelo de facturación.

  1. Identidad y seguridad del servicio

Cuatro decisiones, todas coherentes con el módulo 3.

1. Cuenta de servicio propia y mínima. sa-catalogo-web tiene exactamente lo que necesita y nada más:

# Leer el secreto de la contrasena
gcloud secrets add-iam-policy-binding db-password-catalogo \
  --member=serviceAccount:[email protected] \
  --role=roles/secretmanager.secretAccessor

# Conectarse a Cloud SQL
gcloud projects add-iam-policy-binding alpinashop-prod \
  --member=serviceAccount:[email protected] \
  --role=roles/cloudsql.client

# Leer imagenes del bucket del catalogo
gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
  --member=serviceAccount:[email protected] \
  --role=roles/storage.objectViewer

Si no se especifica --service-account, Cloud Run usa la cuenta de servicio por defecto de Compute Engine, que históricamente tiene el rol Editor sobre todo el proyecto. Desplegar así es dar a tu aplicación web permiso para borrar la base de datos. Es el fallo de seguridad más común de todo Cloud Run.

2. --no-allow-unauthenticated. El servicio exige un token de identidad válido de alguien con roles/run.invoker. Quién puede invocarlo:

Invocador Cómo
El balanceador global El NEG sin servidor invoca sin token; se controla con ingress
Pub/Sub Token OIDC de sa-pubsub-invoker
Otro servicio de Cloud Run Token OIDC de su propia cuenta
Una persona (para depurar) curl -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Un usuario final autenticado con Google IAP delante del balanceador

3. Control de entrada (ingress). Es distinto de la autenticación y complementario:

Valor Quién puede llegar
all Todo internet (la URL run.app es pública)
internal Solo desde la VPC, VPC Service Controls y otros servicios del proyecto
internal-and-cloud-load-balancing Lo anterior más el balanceador global. El de AlpinaShop

Con internal-and-cloud-load-balancing, la URL *.run.app deja de responder desde fuera. Todo el tráfico de clientes tiene que pasar por Cloud Armor y la CDN, sin excepción. Esto es lo que impide el atajo de "voy a probar directamente contra run.app", que es el agujero por el que se salta el WAF.

4. IAP para servicios internos. El panel de administración interno se publica con el mismo patrón de 03-04: balanceador + IAP + Cloud Run privado. Los empleados entran con su cuenta de Google, sin VPN y sin usuarios propios.

  1. Detrás del balanceador global: el NEG sin servidor

En 03-02 se montó el balanceador global de AlpinaShop y se dejó dicho que el día que el catálogo fuera a Cloud Run bastaría con cambiar el backend. Ese día ha llegado.

Un NEG sin servidor (serverless network endpoint group) es un backend que apunta a un servicio de Cloud Run, App Engine o Cloud Functions en lugar de a un grupo de instancias.

flowchart LR
    C["Clientes"] --> IP["alpinashop-lb-ip<br/>IP anycast global"]
    IP --> CERT["alpinashop-cert<br/>TLS gestionado"]
    CERT --> ARM["Cloud Armor<br/>pol-catalogo-web"]
    ARM --> UM["alpinashop-url-map"]
    UM -->|"/*"| BS["bs-catalogo-web<br/>+ Cloud CDN"]
    UM -->|"/img/*"| BB["bb-catalogo-imagenes<br/>bucket"]
    BS --> NEG["NEG sin servidor<br/>neg-catalogo-run"]
    NEG --> CR["Cloud Run<br/>alpinashop-web"]

    style NEG fill:#e6f4ea,stroke:#34a853
    style CR fill:#e6f4ea,stroke:#34a853

Todo lo azul del diagrama —IP, certificado, WAF, mapa de URL, CDN— no se toca. Solo cambia lo verde.

Creación del NEG:

gcloud compute network-endpoint-groups create neg-catalogo-run \
  --region=europe-west1 \
  --network-endpoint-type=serverless \
  --cloud-run-service=alpinashop-web

Y su incorporación al servicio de backend existente:

gcloud compute backend-services add-backend bs-catalogo-web \
  --global \
  --network-endpoint-group=neg-catalogo-run \
  --network-endpoint-group-region=europe-west1

Cuatro particularidades del NEG sin servidor que hay que conocer, porque son distintas de todo lo visto en 03-02:

  1. No hay comprobaciones de estado. El servicio de backend con NEG sin servidor no usa hc-catalogo. La salud la gestiona Cloud Run. Al principio desconcierta; es correcto.
  2. No hay modo de balanceo ni capacidad. No existen max-rate-per-instance ni capacity-scaler: Cloud Run escala solo.
  3. El NEG es regional, aunque el balanceador sea global. Para servir desde varias regiones se crean varios NEG, uno por región, y se añaden al mismo servicio de backend — que es exactamente cómo se hace la alta disponibilidad multirregión de 07-06.
  4. La CDN sigue funcionando igual. Las cabeceras Cache-Control que emite Cloud Run gobiernan la caché exactamente como lo hacían las del MIG (03-03).

  1. La migración desde el MIG, paso a paso y sin corte

Este es el procedimiento real que ejecutó Marta. Está escrito para poder seguirlo, y su virtud es que en ningún momento hay un punto sin retorno.

Fase 0 — Preparación (días antes)

# 1. Verificar que la imagen cumple el contrato, en local
docker run -e PORT=8080 -p 8080:8080 \
  europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:a3f91c2
curl -sf localhost:8080/salud && echo "CONTRATO OK"

# 2. Desplegar en desarrollo primero. SIEMPRE.
gcloud run deploy alpinashop-web --project=alpinashop-dev \
  --image=... --region=europe-west1 --min-instances=0

Y una lista de comprobación previa que evita el 90 % de los sustos:

  • [ ] La aplicación escucha en $PORT y en 0.0.0.0.
  • [ ] No escribe nada importante en disco local.
  • [ ] No guarda sesiones en memoria (las de AlpinaShop están en Firestore desde 02-06).
  • [ ] Los logs van a stdout/stderr en JSON estructurado (06-06).
  • [ ] Gunicorn está configurado con hilos suficientes para la concurrencia elegida.
  • [ ] El pool de conexiones es pequeño y tiene pool_pre_ping.
  • [ ] No hay hilos de fondo que dependan de CPU entre peticiones.

Fase 1 — Desplegar en producción sin tráfico

gcloud run deploy alpinashop-web \
  --project=alpinashop-prod \
  --image=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:a3f91c2 \
  --region=europe-west1 \
  --no-traffic --tag=candidata \
  [... el resto de banderas del apartado 3 ...]

El servicio existe, funciona contra la base de datos real y ningún cliente lo ve. Aquí se hacen las pruebas de humo y se compara la latencia con la del MIG.

Fase 2 — Añadir el NEG al balanceador con peso cero

Se crea el NEG y se añade al servicio de backend bs-catalogo-web, que ahora tiene dos backends: el MIG y Cloud Run. El balanceador reparte entre ellos según capacidad.

Para controlar el reparto con precisión, AlpinaShop usa la técnica del capacity-scaler sobre el backend del MIG:

# Estado inicial: todo al MIG
gcloud compute backend-services update-backend bs-catalogo-web --global \
  --instance-group=alpinashop-web-mig \
  --instance-group-zone=europe-west1-b \
  --capacity-scaler=1.0

Fase 3 — El trasvase progresivo

Se va reduciendo la capacidad del MIG, lo que empuja tráfico hacia Cloud Run:

Momento capacity-scaler del MIG Tráfico aproximado a Cloud Run Qué se vigila
T+0 1.0 ~0 % —
T+30 min 0.5 ~30 % 5xx, p95, conexiones a Cloud SQL
T+2 h 0.2 ~70 % Lo mismo, más coste
T+1 día 0.0 100 % Todo, durante 24 h

En cada escalón se miran las mismas cuatro cosas en el panel de 06-04, y se comparan por backend:

# En el explorador de metricas, filtrando por backend_target_name
loadbalancing.googleapis.com/https/request_count      -> reparto real
loadbalancing.googleapis.com/https/backend_latencies  -> p50 y p95 por backend
run.googleapis.com/container/instance_count           -> instancias creadas
cloudsql.googleapis.com/database/postgresql/num_backends -> conexiones abiertas

La métrica de conexiones a Cloud SQL es la que más sorpresas da, y es el motivo por el que el trasvase se hace en horas y no en minutos.

Fase 4 — Convivencia vigilada

Durante 24-72 horas el MIG sigue existiendo con capacity-scaler=0.0: no recibe tráfico pero está encendido. La reversión es un solo comando:

gcloud compute backend-services update-backend bs-catalogo-web --global \
  --instance-group=alpinashop-web-mig \
  --instance-group-zone=europe-west1-b \
  --capacity-scaler=1.0

Y en segundos vuelve todo al MIG. Pagar dos días de VM apagada de tráfico es el precio de dormir tranquilo, y es un precio ridículo.

Fase 5 — Retirada

Solo cuando han pasado los tres días y se ha atravesado al menos un pico de tráfico:

# 1. Quitar el MIG del balanceador
gcloud compute backend-services remove-backend bs-catalogo-web --global \
  --instance-group=alpinashop-web-mig \
  --instance-group-zone=europe-west1-b

# 2. Reducir el MIG a cero antes de borrarlo (permite volver rapido)
gcloud compute instance-groups managed resize alpinashop-web-mig \
  --size=0 --zone=europe-west1-b

# 3. Una semana despues, borrar de verdad, desde Terraform
#    (eliminar el recurso del .tf y hacer plan/apply, nunca a mano)

El paso 2 es importante: reducir a cero antes de borrar. Un MIG de tamaño cero no cuesta cómputo, conserva su plantilla, y volver a levantarlo es un resize. El borrado definitivo se hace desde Terraform, porque si se hace a mano el siguiente plan intentará recrearlo.

  1. Coste: el modelo y el cálculo comparado

El modelo de facturación

Cloud Run cobra tres cosas, y solo mientras hay instancias vivas:

Concepto Unidad Orden de magnitud en europe-west1
CPU vCPU-segundo ~0,000024 $ por vCPU-s activa; la instancia mínima ociosa, alrededor de una décima parte
Memoria GiB-segundo ~0,0000025 $ por GiB-s
Peticiones Por millón ~0,40 $ por millón
Salida a internet GB Tarifa estándar de egress (07-05)

Y hay un nivel gratuito mensual generoso —del orden de 180.000 vCPU-s, 360.000 GiB-s y 2 millones de peticiones— que hace que muchos proyectos pequeños cuesten literalmente cero.

Los precios cambian y varían por región. Consulta siempre la página de precios oficial y la calculadora. Lo que sigue son órdenes de magnitud para razonar, no un presupuesto.

El cálculo de AlpinaShop

Escenario: 1,5 millones de peticiones al mes, latencia media 200 ms, 1 vCPU, 512 MiB, concurrencia 80, min-instances=1, CPU limitada a la petición.

Tiempo de instancia activa. Con concurrencia 80, las peticiones se solapan. Una estimación conservadora del tiempo con al menos una petición en vuelo, a partir de la distribución horaria real, da unas 28 horas al mes de actividad ≈ 100.000 vCPU-s.

Concepto Cálculo Coste
CPU activa 100.000 vCPU-s × 0,000024 $ ~2,40 $
CPU ociosa (min-instances=1) 2.592.000 s × ~0,0000025 $ ~6,50 $
Memoria activa 50.000 GiB-s × 0,0000025 $ ~0,13 $
Memoria ociosa 1.296.000 GiB-s × ~0,00000025 $ ~0,32 $
Peticiones 1,5 M × 0,40 $/M ~0,60 $
Menos nivel gratuito −2 a −3 $
Total ≈ 7-8 $/mes ≈ 7 €/mes

Comparación con lo que se venía pagando, usando las cifras de 02-07:

Concepto MIG (antes) Cloud Run (después)
Cómputo 2 × e2-medium 24×7 ≈ 50 €/mes ≈ 7 €/mes
Balanceador global + IP ~20 €/mes ~20 €/mes (igual)
Discos persistentes ~4 €/mes 0 €
Agente de operaciones Incluido, pero hay que instalarlo No hace falta
Total infraestructura ≈ 74 €/mes ≈ 27 €/mes
Parcheo del SO, plantillas, actualizaciones progresivas ~2-4 h/mes de Marta 0 h

Ahorro: unos 47 €/mes, un 63 % del coste de la tienda. Y esa es la parte pequeña. Las 2-4 horas mensuales de Marta valen más que los 47 €, y el tiempo de reversión pasa de 10 minutos a 5 segundos.

Pero hay que decir la verdad completa, porque este curso no vende:

  • Si el tráfico fuera constante y alto las 24 horas, el MIG con descuento por uso comprometido sería más barato. Cloud Run gana con tráfico irregular, que es el caso de AlpinaShop, no todos los casos.
  • En campaña de otoño la factura de Cloud Run sube, porque se paga el uso. Es lo correcto —se paga cuando se vende— pero significa que la factura ya no es plana. Se planifica en 07-05.
  • El balanceador global sigue costando lo mismo y ahora representa el 74 % de la factura del catálogo. Es lo que pasa cuando se optimiza bien: el mayor coste se desplaza a otra parte.

  1. Límites y cuándo Cloud Run no encaja

Los límites que hay que conocer antes de decidir (verifícalos en la documentación, cambian a mejor con el tiempo):

Límite Valor orientativo
Tiempo máximo por petición 60 minutos
Memoria por instancia Hasta 32 GiB
CPU por instancia Hasta 8 vCPU
Concurrencia máxima 1.000
Instancias máximas 1.000 por defecto (ampliable)
Tamaño de petición y respuesta 32 MiB (sin streaming)
GPU Disponible en regiones seleccionadas
Almacenamiento local En memoria (cuenta contra el límite) o volúmenes montados

Y los escenarios donde Cloud Run no es la respuesta:

Escenario Por qué no Alternativa
Procesos que deben correr permanentemente sin peticiones La CPU se congela; con min-instances y CPU siempre activa se puede, pero pagas por una VM cara Compute Engine o GKE
Bases de datos, colas, Elasticsearch Necesitan estado persistente y disco Servicios gestionados
Aplicaciones que necesitan disco local rápido y grande El sistema de ficheros vive en memoria Compute Engine con SSD local
Tareas de más de 60 minutos Límite duro Cloud Run jobs (24 h) o Batch
Software con licencia atada al hardware No hay hardware que licenciar Compute Engine con licencia propia
Protocolos que no sean HTTP/gRPC/WebSocket (por ejemplo, un servidor de juego UDP) Solo entra tráfico HTTP Compute Engine con balanceador de red
Necesidad de acceso al núcleo, módulos o privilegios Sandbox sin privilegios GKE con nodos propios

  1. La tabla madura: Cloud Run vs Functions vs GKE vs Compute Engine

En 02-07 se hizo esta comparación con la información de entonces. Ahora, con seis módulos más de recorrido, se puede hacer con criterio de verdad.

Criterio Cloud Run Cloud Functions 2ª gen GKE Autopilot Compute Engine
Unidad de despliegue Contenedor Función Contenedor + manifiestos VM
Escala a cero Sí Sí No (plano de control fijo) No
Facturación Por uso, al segundo Por uso Por recursos de los pods Por VM encendida
Arranque en frío Segundos, mitigable Segundos No aplica No aplica
Trabajo operativo Mínimo Mínimo Medio Alto
Portabilidad Alta (contenedor estándar) Media Muy alta Media
Curva de aprendizaje Baja Muy baja Alta Media
Servicios que se llaman entre sí Bien, hasta cierta escala Regular Excelente Manual
Cargas por lotes largas Jobs, hasta 24 h No (límite corto) Sí Sí
Estado en disco No No Sí (volúmenes) Sí
Control del sistema operativo No No Parcial Total
Coste con tráfico irregular El mejor El mejor para eventos Malo (coste base) Malo
Coste con tráfico constante alto Bueno Malo Bueno El mejor con compromiso
Elección de AlpinaShop Catálogo web procesar-imagen-producto Se retira Se retira

Las cuatro preguntas que resuelven casi todas las decisiones, en este orden:

  1. ¿Es un contenedor HTTP sin estado con tráfico irregular? → Cloud Run. Es el caso mayoritario.
  2. ¿Es una reacción a un evento único, de código breve? → Cloud Functions (que por debajo es Cloud Run).
  3. ¿Hay muchos servicios que se llaman entre sí, con estado, DaemonSets u operadores? → GKE.
  4. ¿Necesitas el sistema operativo, disco local, licencias atadas o protocolos no HTTP? → Compute Engine.

Y la observación que cierra el continuo de abstracción de 02-07: la respuesta correcta para AlpinaShop resultó ser el escalón que no se había probado. Se pasó por VM, por App Engine y por Kubernetes antes de llegar a la opción que encajaba. Eso no fue tiempo perdido: fue cómo se adquirió el criterio para decidir.

  1. Apagar alpinashop-web-mig y lo que eso simplifica

La lista de cosas que dejan de existir al retirar el MIG es más larga de lo que parece, y es el argumento más fuerte a favor del movimiento:

Desaparece Qué implicaba
La plantilla de instancia Un recurso más en Terraform, con su imagen base y su versión
El startup script Cien líneas de bash que instalaban Docker y arrancaban el contenedor
El parcheo del sistema operativo Actualizaciones de seguridad mensuales de Marta
El agente de operaciones Instalación, configuración y actualización para tener métricas de memoria
La comprobación de estado hc-catalogo Ya no hace falta para el backend (sigue útil para las uptime checks)
La política de autoescalado del MIG CPU objetivo, periodo de estabilización, retardo de inicialización
Las actualizaciones progresivas max-surge, max-unavailable y esperar diez minutos
Las claves SSH y el acceso por IAP a las VM Una superficie de ataque entera
Los discos persistentes y sus instantáneas Recursos que se olvidan y se pagan (07-05)
La reserva de capacidad para la campaña Cloud Run escala solo

Diez elementos menos que mantener, entender, documentar y explicar a quien venga después. Para una empresa con una persona de infraestructura, esa reducción vale más que el ahorro económico.

Lo que sí hay que seguir haciendo, para que nadie se confunda:

  • Actualizar la imagen base del Dockerfile y reconstruir. Cloud Run parchea el sandbox, no tu contenedor.
  • Vigilar las vulnerabilidades de la imagen en Artifact Registry (07-04).
  • Gestionar migraciones de esquema de la base de datos.
  • Todo lo relacionado con la aplicación en sí.

La arquitectura de cómputo de AlpinaShop queda, por fin, así:

flowchart TB
    subgraph BORDE["Borde global"]
      IP["alpinashop-lb-ip"] --> ARM["Cloud Armor pol-catalogo-web"] --> CDN["Cloud CDN"]
    end
    CDN --> NEG["NEG sin servidor"]
    NEG --> RUN["Cloud Run alpinashop-web<br/>0 a 30 instancias"]
    RUN --> SQL["Cloud SQL alpinashop-pedidos<br/>HA regional"]
    RUN --> FS["Firestore - sesiones"]
    RUN --> SEC["Secret Manager"]
    GCS["Bucket alpinashop-catalogo"] --> CDN
    GCS -->|"evento"| FN["Cloud Function<br/>procesar-imagen-producto"]
    RUN -->|"pedido"| PS["Pub/Sub pedidos-nuevos"]
    PS --> DF["Dataflow"] --> BQ["BigQuery alpinashop_analitica"]
    JOB["Cloud Run job<br/>informes-nocturnos"] --> BQ

    style RUN fill:#e6f4ea,stroke:#34a853
    style JOB fill:#e6f4ea,stroke:#34a853

No queda ni una máquina virtual en la ruta de servicio de la tienda. Y alpinashop-cluster queda sin cargas de producción: se conserva como entorno de pruebas de contenedores y para lo que pueda venir, con una nota en DA-004 para revisar si merece la pena mantenerlo.

Errores Comunes y Consejos

  • Escuchar en un puerto fijo o en 127.0.0.1. Es el error número uno. Lee $PORT y escucha en 0.0.0.0.
  • Desplegar sin --service-account. Heredas la cuenta por defecto de Compute Engine, que suele tener Editor sobre el proyecto. Tu web tendría permiso para borrar la base de datos.
  • Poner --concurrency=80 con un servidor de un solo hilo. Las peticiones se encolan dentro del contenedor y la latencia se dispara. Configura Gunicorn, Uvicorn o lo que uses antes de tocar la concurrencia.
  • No poner --max-instances. Un bucle infinito o un rastreador te crea cientos de instancias, y además tumba Cloud SQL por agotamiento de conexiones.
  • Pool de conexiones grande. pool_size=20 × 30 instancias = 600 conexiones contra una base de datos que aguanta 100. Pool pequeño, max_overflow=0 y pool_pre_ping=True.
  • Guardar algo en el sistema de ficheros y esperar encontrarlo. El disco es efímero y vive en memoria: escribir un fichero de 300 MiB consume 300 MiB de tu límite de memoria.
  • Lanzar hilos en segundo plano con CPU limitada a la petición. No se ejecutan. O usas CPU siempre activa, o mueves ese trabajo a un job o a Cloud Scheduler.
  • Usar la URL *.run.app como URL pública del producto. Cambia si recreas el servicio, se salta Cloud Armor y la CDN, y no es tu dominio. Balanceador siempre.
  • Confundir autenticación con ingress. --no-allow-unauthenticated dice quién puede invocar; ingress dice por dónde puede llegar. Hacen falta los dos.
  • Desplegar con latest. Pierdes la trazabilidad de qué versión corre y la reversión deja de ser fiable. Etiqueta con $COMMIT_SHA.
  • Creer que el canario protege de todo. No protege de cambios de esquema de base de datos ni sirve de nada con tráfico bajo.
  • Olvidar --timeout 0 en Gunicorn. Dos temporizadores compitiendo producen cortes que parecen aleatorios.
  • Consejo: mide la concurrencia con una prueba de carga real antes de fijarla. Es el parámetro con mayor impacto conjunto en coste y latencia.
  • Consejo: usa --tag en cada despliegue, aunque no hagas canario. Tener una URL por revisión es útil para depurar y no cuesta nada.
  • Consejo: activa startup_cpu_boost. Es gratis en la práctica y reduce el arranque en frío de forma apreciable en runtimes pesados.
  • Consejo: la imagen pequeña es la mejor optimización de arranque en frío. Una imagen distroless o alpine bien hecha arranca mucho antes que una ubuntu con medio sistema dentro.

Ejercicios

Ejercicio 1 — Elegir concurrencia, CPU, memoria e instancias

AlpinaShop va a desplegar en Cloud Run un servicio nuevo: alpinashop-buscador, una API de búsqueda del catálogo.

Datos medidos en el entorno de desarrollo:

  • Escrito en Python con FastAPI + Uvicorn (asíncrono, un solo proceso).
  • Cada petición: 15 ms de CPU (tokenizar y puntuar) + 120 ms esperando a Elasticsearch.
  • Consumo de memoria: 180 MiB en reposo + ~2 MiB por petición en vuelo.
  • Tráfico: 5 peticiones/segundo de media, 40 peticiones/segundo en campaña.
  • Elasticsearch admite 200 conexiones concurrentes en total y lo comparte con otros dos servicios.
  • Nadie tolera más de 800 ms de latencia p95.

Determina --concurrency, --cpu, --memory, --min-instances y --max-instances, justificando cada valor con los datos. Indica además qué cambiarías en el arranque de la aplicación.

Ejercicio 2 — Diseñar el despliegue seguro en el pipeline

Escribe los pasos de Cloud Build (cloudbuild.yaml) que implementan el despliegue canario del catálogo con estas reglas:

  1. Construir la imagen etiquetada con $COMMIT_SHA.
  2. Desplegar sin tráfico, con la etiqueta candidata.
  3. Ejecutar una prueba de humo contra la URL de la etiqueta; si falla, abortar.
  4. Dar el 10 % del tráfico.
  5. Esperar 10 minutos y comprobar automáticamente que la tasa de error 5xx de la revisión nueva está por debajo del 1 %; si no, revertir a 0 % y fallar la compilación.
  6. Solo entonces, esperar la aprobación manual de Marta para pasar al 100 %.

Indica también qué permisos necesita la cuenta de servicio del pipeline y por qué el paso 5 es el que más gente omite.

Ejercicio 3 — Diagnosticar una migración que va mal

Tres horas después de mover el 70 % del tráfico a Cloud Run, Marta observa esto:

  • La latencia p50 del backend de Cloud Run es de 210 ms; la del MIG, 195 ms. Aceptable.
  • La latencia p99 de Cloud Run es de 4,2 segundos; la del MIG, 380 ms.
  • En Cloud Logging aparecen unos 40 errores por hora: sqlalchemy.exc.OperationalError: server closed the connection unexpectedly.
  • El número de instancias oscila entre 1 y 9 constantemente, con subidas y bajadas cada pocos minutos.
  • cloudsql.googleapis.com/database/postgresql/num_backends ha pasado de 12 a 34.
  • Error Reporting muestra 6 casos de Memory limit of 512 MiB exceeded.

Diagnostica cada síntoma, indica la causa raíz probable de cada uno, y escribe el plan de corrección ordenado por prioridad. Decide además si hay que revertir al MIG o se puede corregir en caliente.

Soluciones

Solución 1 — Elegir concurrencia, CPU, memoria e instancias

Punto de partida: el perfil de la petición. 15 ms de CPU frente a 120 ms de espera. La CPU trabaja el 11 % del tiempo. Es un servicio dominado por la E/S, exactamente el caso donde la concurrencia alta es rentable.

--concurrency. Tres restricciones a la vez, y hay que respetar la más estricta:

  1. Por CPU: con 15 ms de CPU cada 135 ms totales, una vCPU podría sostener teóricamente unas 9 peticiones simultáneas antes de saturarse. Este es el límite duro.
  2. Por Elasticsearch: 200 conexiones compartidas entre tres servicios, digamos 60 para el buscador. Si cada petición abre una conexión, concurrencia × instancias ≤ 60.
  3. Por latencia: el presupuesto p95 es de 800 ms, y la petición base son 135 ms. Hay margen para algo de cola, pero no mucho.

Con 1 vCPU, poner concurrencia por encima de 9-10 hace que las peticiones esperen CPU. Se elige --concurrency=10. Y si se sube a --cpu=2, se podría ir a 20; con el tráfico de este servicio no compensa.

--cpu=1. Suficiente para la concurrencia elegida. Subir a 2 vCPU solo tiene sentido si se sube también la concurrencia, y el tráfico no lo justifica.

--memory=512Mi. Cálculo: 180 MiB de base + 10 peticiones × 2 MiB = 200 MiB, más el runtime de Python y margen. 512 MiB deja holgura de más del doble. Poner 256 MiB sería jugar con fuego, porque quedarse sin memoria mata la instancia, no ralentiza.

--max-instances. El pico son 40 req/s. Con 135 ms por petición y concurrencia 10, cada instancia sostiene unas 74 req/s en teoría; en la práctica, con margen, digamos 40-50. Bastarían 1-2 instancias. Pero el límite real lo pone Elasticsearch: 60 conexiones / 10 por instancia = 6 instancias. Se elige --max-instances=6, que cubre el pico con margen amplio y protege el backend compartido, que es el recurso escaso.

--min-instances=1. Un buscador es de cara al cliente y un arranque en frío de 1-2 s en la primera búsqueda de la mañana es visible y molesto. Una instancia mínima cuesta unos pocos euros al mes. En alpinashop-dev, 0.

Qué cambiar en el arranque. FastAPI con Uvicorn en un solo proceso es asíncrono, así que puede con varias peticiones simultáneas —pero solo si el código es realmente asíncrono. La comprobación crítica:

# MAL: cliente sincrono dentro de una funcion async. Bloquea el bucle de eventos
# y la concurrencia efectiva pasa a ser 1, por muy async que sea el endpoint.
@app.get("/buscar")
async def buscar(q: str):
    r = requests.get(f"{ES}/catalogo/_search", json=consulta(q))   # BLOQUEA
    return r.json()

# BIEN: cliente asincrono
@app.get("/buscar")
async def buscar(q: str):
    async with httpx.AsyncClient() as cli:
        r = await cli.post(f"{ES}/catalogo/_search", json=consulta(q))
    return r.json()

Este es exactamente el error del apartado 6 con otra cara: un async def que llama a una biblioteca síncrona no es concurrente, y la concurrencia 10 configurada en Cloud Run no sirve de nada.

Además: crear el cliente HTTP una vez al arrancar y reutilizarlo (no en cada petición, como en el ejemplo simplificado), y arrancar con uvicorn main:app --host 0.0.0.0 --port $PORT --workers 1 — un solo worker, porque el paralelismo lo da el bucle de eventos y Cloud Run añade instancias.

Solución 2 — Diseñar el despliegue seguro en el pipeline

# cloudbuild.yaml — despliegue canario de alpinashop-web
substitutions:
  _REGION: europe-west1
  _SERVICIO: alpinashop-web
  _IMAGEN: europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo

steps:

# 1. Construir con el hash del commit
- id: construir
  name: gcr.io/cloud-builders/docker
  args: ['build', '-t', '${_IMAGEN}:$COMMIT_SHA', '.']

- id: publicar
  name: gcr.io/cloud-builders/docker
  args: ['push', '${_IMAGEN}:$COMMIT_SHA']

# 2. Desplegar SIN trafico, con etiqueta
- id: desplegar-candidata
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: gcloud
  args:
    - run
    - deploy
    - ${_SERVICIO}
    - --image=${_IMAGEN}:$COMMIT_SHA
    - --region=${_REGION}
    - --no-traffic
    - --tag=candidata
    - --revision-suffix=$SHORT_SHA

# 3. Prueba de humo contra la URL de la etiqueta
- id: humo
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: bash
  args:
    - -c
    - |
      set -e
      URL=$(gcloud run services describe ${_SERVICIO} --region=${_REGION} \
            --format='value(status.traffic[?tag==`candidata`].url)')
      TOKEN=$(gcloud auth print-identity-token)
      for RUTA in /salud /producto/1 /api/categorias; do
        CODIGO=$(curl -s -o /dev/null -w '%{http_code}' \
                 -H "Authorization: Bearer $$TOKEN" "$$URL$$RUTA")
        echo "$$RUTA -> $$CODIGO"
        [ "$$CODIGO" = "200" ] || { echo "FALLO EN $$RUTA"; exit 1; }
      done

# 4. Canario al 10%
- id: canario-10
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: gcloud
  args:
    - run
    - services
    - update-traffic
    - ${_SERVICIO}
    - --region=${_REGION}
    - --to-tags=candidata=10

# 5. Esperar y verificar la tasa de error de la revision nueva
- id: verificar-canario
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: bash
  args:
    - -c
    - |
      set -e
      REV=${_SERVICIO}-$SHORT_SHA
      sleep 600     # 10 minutos de observacion

      FILTRO="metric.type=\"run.googleapis.com/request_count\" \
              AND resource.labels.service_name=\"${_SERVICIO}\" \
              AND resource.labels.revision_name=\"$$REV\""

      TOTAL=$(gcloud monitoring time-series list \
              --filter="$$FILTRO" --interval-end-time=now --interval-start-time=-10m \
              --format='value(points.value.int64Value)' | paste -sd+ | bc)

      ERR=$(gcloud monitoring time-series list \
            --filter="$$FILTRO AND metric.labels.response_code_class=\"5xx\"" \
            --interval-end-time=now --interval-start-time=-10m \
            --format='value(points.value.int64Value)' | paste -sd+ | bc)

      TOTAL=$${TOTAL:-0}; ERR=$${ERR:-0}
      echo "Peticiones: $$TOTAL, errores 5xx: $$ERR"

      if [ "$$TOTAL" -lt 100 ]; then
        echo "AVISO: muestra insuficiente ($$TOTAL peticiones). No se puede concluir."
        exit 1
      fi

      RATIO=$(echo "scale=4; $$ERR / $$TOTAL * 100" | bc)
      echo "Tasa de error: $$RATIO %"

      if [ $(echo "$$RATIO > 1" | bc) -eq 1 ]; then
        echo "REVIRTIENDO: tasa de error por encima del 1%"
        gcloud run services update-traffic ${_SERVICIO} \
          --region=${_REGION} --to-tags=candidata=0
        exit 1
      fi

options:
  logging: CLOUD_LOGGING_ONLY

El paso 6, la aprobación manual, no es un paso del cloudbuild.yaml: se configura en el disparador de Cloud Build con --require-approval, tal como se montó en 06-01 para producción. Cuando la compilación llega al final, queda a la espera de que Marta apruebe, y el paso al 100 % se hace en un disparador separado o manualmente con --to-latest.

Permisos de la cuenta de servicio del pipeline (sa-deploy-prod):

Rol Para qué
roles/run.developer Desplegar servicios y cambiar el reparto de tráfico
roles/iam.serviceAccountUser sobre sa-catalogo-web Poder asignar esa cuenta al servicio. Este es el que todo el mundo olvida y produce un PERMISSION_DENIED desconcertante
roles/artifactregistry.writer Publicar la imagen
roles/monitoring.viewer Leer las métricas del paso 5
roles/logging.logWriter Escribir los logs de la compilación

Nótese que no tiene roles/editor ni permisos sobre Cloud SQL o Secret Manager: despliega, no administra.

Por qué el paso 5 es el que casi todo el mundo omite. Porque es el único que requiere pensar. Los pasos 1-4 son mecánicos y salen en cualquier tutorial; el 5 exige decidir qué métrica, qué umbral, cuánto tiempo y qué hacer si no hay datos suficientes. Sin él, el canario es teatro: se despliega al 10 %, nadie mira, y a los diez minutos alguien pulsa "aprobar" porque no ha saltado ninguna alarma — que es distinto de que todo esté bien.

La comprobación de muestra mínima (TOTAL -lt 100) es la sutileza que separa un canario real de uno decorativo: con pocas peticiones, la ausencia de errores no demuestra nada, y fallar la compilación obliga a mirarlo en lugar de aprobar a ciegas.

Solución 3 — Diagnosticar una migración que va mal

Diagnóstico síntoma a síntoma.

(1) p50 correcto pero p99 de 4,2 s. El p50 sano descarta un problema general de rendimiento: la mayoría de peticiones van bien. Un p99 veinte veces peor con un p50 normal es la firma inconfundible del arranque en frío. El servicio se desplegó con --min-instances=0, o el tráfico es lo bastante irregular como para crear instancias constantemente. La confirmación está en el síntoma (4).

(2) server closed the connection unexpectedly, ~40/hora. Conexiones del pool que llevaban tiempo inactivas y que Cloud SQL, o la propia congelación de CPU de la instancia, ha dado por muertas. Es el caso exacto de pool_pre_ping: sin él, la primera consulta tras un periodo de inactividad usa una conexión zombi y falla. La causa raíz es falta de pool_pre_ping=True y de pool_recycle en la configuración de SQLAlchemy, agravada por la congelación de CPU entre peticiones que Cloud Run aplica y el MIG no aplicaba.

(3) Instancias oscilando entre 1 y 9 cada pocos minutos. Es la causa de (1), y a su vez tiene su propia causa. Con concurrencia 80 y el tráfico de AlpinaShop, una sola instancia debería bastar de sobra. Que suba a 9 significa que las instancias mueren y se recrean: eso lo explica el síntoma (6). Es un ciclo vicioso —muere por memoria, arranca en frío, atiende, vuelve a morir— que además genera parte de (2), porque cada muerte se lleva por delante las conexiones del pool.

(4) Conexiones a Cloud SQL de 12 a 34. Coherente con 9 instancias × pool de 3-4. No es un problema por sí mismo —34 está lejos del límite— pero confirma la aritmética y avisa: si el máximo de instancias estuviera mal puesto, aquí es donde se vería el desastre. Es un síntoma para vigilar, no para corregir.

(5) Seis casos de Memory limit exceeded. Esta es la causa raíz principal. 512 MiB no bastan para la concurrencia configurada. Las hipótesis, por probabilidad: la concurrencia es 80 y cada petición del catálogo consume más memoria de lo estimado (renderiza plantillas, carga imágenes en memoria); o hay una caché en proceso que crece sin límite; o la aplicación escribe ficheros temporales, que en Cloud Run cuentan como memoria.

Cadena causal completa:

Memoria insuficiente (512 MiB con concurrencia 80)
      |
      v
La instancia muere al superar el limite
      |
      +--> Se recrean instancias -> oscilacion 1 a 9 (sintoma 3)
      |                                   |
      |                                   v
      |                          arranques en frio -> p99 de 4,2 s (sintoma 1)
      |                                   |
      |                                   v
      |                          mas instancias -> 34 conexiones (sintoma 4)
      |
      +--> Se pierden conexiones del pool
                    |
                    v
        (agravado por falta de pool_pre_ping)
                    |
                    v
        OperationalError, 40/hora (sintoma 2)

¿Revertir o corregir en caliente?

No hay que revertir. El razonamiento: el 30 % del tráfico sigue en el MIG, el p50 es correcto, los errores son 40 a la hora sobre miles de peticiones (~0,5 %) y todas las causas son de configuración, no de código. Revertir costaría el resto del día y no aportaría información nueva. Ahora bien, sí hay que dejar de subir tráfico hasta que esté corregido, y tener el comando de reversión preparado por si algo empeora.

Si en lugar de esto los errores fueran del 10 %, o hubiera pérdida de pedidos, o el p50 estuviera degradado, la decisión sería la contraria: revertir primero, diagnosticar después. En un incidente, primero se para el daño.

Plan de corrección por prioridad:

Prio Acción Comando / cambio Efecto esperado
1 Subir memoria a 1 GiB gcloud run services update alpinashop-web --region=europe-west1 --memory=1Gi Corta la causa raíz; desaparecen (3), (1) y buena parte de (2)
2 Bajar concurrencia a 40 mientras se investiga --concurrency=40 Reduce la memoria simultánea a la mitad. Coste algo mayor, aceptable durante el diagnóstico
3 Añadir pool_pre_ping=True y pool_recycle=1800 Cambio de código, despliegue canario Elimina el residuo de (2)
4 Poner --min-instances=1 --min-instances=1 Elimina el arranque en frío del primer usuario
5 Alerta de memoria Política sobre run.googleapis.com/container/memory/utilizations > 80 % Que la próxima vez avise antes de morir
6 Perfilar la memoria de verdad Cloud Profiler o análisis local con carga Saber si 1 GiB es la solución o solo un parche sobre una fuga

El paso 6 es el que no hay que saltarse. Subir la memoria es la acción correcta ahora, porque para el sangrado. Pero si hay una fuga de memoria real —una caché que crece sin límite, por ejemplo— con 1 GiB simplemente tardará el doble en morir, y el problema reaparecerá en campaña con el peor momento posible. La diferencia entre resolver un incidente y aplazarlo es exactamente ese último paso, y es el que se cancela cuando todo vuelve a estar verde.

Conclusión

DA-001 está cumplida. El catálogo de AlpinaShop corre en Cloud Run, detrás de la misma IP, el mismo dominio, el mismo certificado, la misma CDN y el mismo WAF; el MIG está apagado; y no queda una sola máquina virtual en la ruta de servicio de la tienda.

Sabes qué es Cloud Run —contenedores sin servidor— y por qué la unidad de despliegue lo cambia todo: el contrato son seis reglas (escuchar en $PORT y en 0.0.0.0, hablar HTTP, arrancar rápido, no guardar estado, no hacer nada entre peticiones, ser un contenedor Linux sin privilegios) y cumplirlas hace que la misma imagen corra en Cloud Run, en Kubernetes o en tu portátil. Distingues servicio, revisión e instancia, y sabes que las revisiones son inmutables — que es lo que hace posible todo lo demás.

Tienes el criterio entre servicios y trabajos: si responde a alguien, servicio; si hace algo y termina, trabajo. Con el antipatrón identificado: el informe nocturno como endpoint HTTP disparado por Scheduler, que hereda cuatro problemas que un job no tiene.

Has desplegado el catálogo con gcloud run deploy bandera a bandera, sabiendo qué hace cada una y por qué tiene ese valor, y lo has escrito en Terraform con google_cloud_run_v2_service, con cpu_idle, startup_cpu_boost e ingress restringido al balanceador, y con la frontera clara de qué gestiona Terraform (la forma) y qué gestiona el pipeline (la imagen y el tráfico).

Dominas las revisiones y el reparto de tráfico: la etiqueta que da una URL propia para probar sin arriesgar, la progresión 0 → 10 → 50 → 100 con lo que se observa en cada escalón, y la reversión instantánea que tarda segundos porque la revisión anterior nunca dejó de existir. Con las dos advertencias honestas: el canario no protege de cambios de esquema, y con tráfico bajo el 10 % no demuestra nada.

Entiendes la concurrencia mejor que la mayoría: qué es, por qué el modelo de una petición por instancia es doce veces más caro para una carga dominada por E/S, cómo se mide con una prueba de carga real, y —lo más importante— que el techo no lo pone Cloud Run sino tu servidor de aplicaciones. Un --concurrency=80 sobre un Gunicorn de un worker síncrono, o un async def que llama a una biblioteca bloqueante, convierten la concurrencia en una cola invisible.

Sabes gobernar el escalado: el escalado a cero y su precio en arranques en frío, --min-instances que los mitiga pero no los elimina, --max-instances como cortafuegos de coste y de conexiones a la base de datos con su fórmula, y la elección entre CPU durante la petición y CPU siempre activa, con la trampa de los hilos en segundo plano que no se ejecutan.

Has conectado el servicio con toda la plataforma: Cloud SQL por conector con un pool pequeño y pool_pre_ping, Secret Manager montado como variable o como fichero con la diferencia práctica en la rotación, VPC directa —que sustituye al viejo conector— con sus dos modos de egress, Pub/Sub push con OIDC sin un solo secreto compartido, y Eventarc, con la revelación de que Cloud Functions 2ª generación es Cloud Run por debajo.

Has asegurado el servicio con las cuatro decisiones: cuenta de servicio propia y mínima —evitando el fallo más común, heredar la cuenta por defecto con Editor—, --no-allow-unauthenticated, ingress restringido al balanceador para que nadie pueda saltarse el WAF por la URL run.app, e IAP para lo interno.

Has hecho la migración sin corte: preparación con lista de comprobación, despliegue sin tráfico, NEG sin servidor añadido al balanceador existente, trasvase progresivo con capacity-scaler vigilando cuatro métricas, tres días de convivencia con reversión de un comando, y retirada en dos pasos con resize 0 antes del borrado desde Terraform. Y conoces las cuatro particularidades del NEG sin servidor: sin comprobaciones de estado, sin capacidad, regional aunque el balanceador sea global, y con la CDN funcionando igual.

Tienes el coste desmenuzado: vCPU-segundo, GiB-segundo y peticiones, con nivel gratuito, y el cálculo comparado que baja el cómputo del catálogo de unos 50 € a unos 7 € al mes — con la honestidad de señalar que con tráfico constante alto el MIG con compromiso ganaría, que en campaña la factura sube porque se paga el uso, y que ahora el balanceador es el 74 % de lo que cuesta la tienda. Conoces los límites y los siete escenarios donde Cloud Run no encaja. Y te llevas la tabla madura con las cuatro preguntas que resuelven casi cualquier decisión de cómputo.

Por último, has visto lo que desaparece al apagar el MIG: diez elementos —plantilla, script de arranque, parcheo, agente, autoescalado, actualizaciones progresivas, claves SSH, discos, instantáneas, reserva de campaña— que ya no hay que mantener, entender ni explicar. Para un equipo de una persona, eso vale más que el ahorro.

Y sin embargo, dos apartados de esta lección han dejado un cabo suelto que apunta al mismo sitio. La conexión a Cloud SQL sería mejor por IP privada a través de la VPC. Salir hacia la pasarela de pago con IP fija exige pasar por el Cloud NAT. Y el ERP de Sabadell, que DA-004 dejó al otro lado de un túnel que todavía no existe, necesita que Cloud Run pueda alcanzarlo por la red privada.

Los tres son el mismo problema: la red de AlpinaShop se quedó en lo básico en el módulo 3 y ya no basta. Una sola VPC en un solo proyecto, tres proyectos que se ignoran entre sí, ninguna conectividad con la tienda física y ningún perímetro que impida que los datos de alpinashop-datos salgan por la puerta. La siguiente lección lo resuelve: VPC compartida, emparejamiento, Private Service Connect, Cloud VPN HA hacia Sabadell y controles de servicio de VPC.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados