A 02-07 es va escriure una decisió i es va deixar pendent. DA-001: el catàleg web d'AlpinaShop ha de passar a Cloud Run. Es va justificar amb una taula, es va signar, i es va desar al README d'alpinashop-catalogo. Des de llavors el curs hi ha tornat una dotzena de vegades —al balancejador de 03-02, als secrets de 03-06, a Pub/Sub push de 04-04, a Cloud Functions de 06-03, a l'agent d'operacions de 06-04— sempre amb la mateixa cantarella: «a 07-02».

Aquesta és 07-02. Aquí es compleix.

No és un caprici de coherència narrativa: és que fins ara no hi havia les condicions. Moure el catàleg de producció a una altra plataforma sense un pipeline que construeixi imatges reproduïbles, sense observabilitat per saber si ha anat bé, sense infraestructura com a codi per revertir i sense un balancejador al davant que absorbeixi el canvi, és una temeritat. Amb les quatre coses fetes —mòduls 3 i 6— el moviment és una tarda de feina amb xarxa de seguretat.

En acabar la lliçó, el catàleg d'AlpinaShop servirà des de Cloud Run, darrere de la mateixa IP, el mateix domini, el mateix certificat, la mateixa CDN i el mateix WAF que tenia; haurà passat per un desplegament canari al 10 %; el MIG alpinashop-web-mig estarà apagat; i la factura de còmput del catàleg haurà baixat d'uns 50 € al mes a menys de 10. I sabràs per què la concurrència és el paràmetre que més gent configura malament.

Contingut

  1. Què és Cloud Run i per què importa el contracte del contenidor
  2. Serveis i treballs (jobs): el criteri per triar
  3. Desplegar el catàleg: gcloud run deploy bandera a bandera
  4. El mateix servei en Terraform
  5. Revisions i repartiment de trànsit: canari, etiquetes i reversió
  6. Concurrència: el concepte que més es malinterpreta
  7. Escalat: zero, mínim, màxim i el model de facturació de CPU
  8. Connectar Cloud Run amb la resta de la plataforma
  9. Identitat i seguretat del servei
  10. Darrere del balancejador global: el NEG sense servidor
  11. La migració des del MIG, pas a pas i sense tall
  12. Cost: el model i el càlcul comparat
  13. Límits i quan Cloud Run no encaixa
  14. La taula madura: Cloud Run vs Functions vs GKE vs Compute Engine
  15. Apagar alpinashop-web-mig i el que això simplifica

  1. Què és Cloud Run i per què importa el contracte del contenidor

Cloud Run executa contenidors sense servidor. Li dones una imatge, et dona una URL HTTPS, i escala de zero a moltes instàncies segons el trànsit, cobrant pel que facis servir.

Dit així sona a App Engine amb un altre nom, i no ho és. La diferència és a la unitat de desplegament:

Plataforma Li lliures Conseqüència
App Engine estàndard Codi font + app.yaml Runtimes fixos, format propietari, poc portable
Cloud Functions Una funció i la seva signatura Un punt d'entrada; el runtime el posa Google
Cloud Run Una imatge de contenidor Qualsevol llenguatge, qualsevol binari, qualsevol dependència del sistema
GKE Manifestos de Kubernetes Control total i complexitat total

Que la unitat sigui un contenidor estàndard és el que fa Cloud Run diferent i el que va resoldre el punt més delicat de DA-001: la portabilitat. La mateixa imatge que corre avui a alpinashop-cluster correrà demà a Cloud Run sense tocar-ne una línia. I si algun dia calgués sortir, aquesta imatge corre a qualsevol lloc on hi hagi un runtime de contenidors.

El contracte del contenidor

Cloud Run no executa qualsevol imatge de qualsevol manera: exigeix complir un contracte, i és deliberadament curt.

Regla Detall Per què
Escoltar a $PORT La variable d'entorn PORT (per defecte 8080), a 0.0.0.0 La plataforma tria el port i hi encamina
Respondre a HTTP/gRPC/WebSocket El trànsit entra per la xarxa, no per esdeveniments locals És una plataforma de peticions
Arrencar ràpid Escoltar al port en segons, no en minuts Cada arrencada en fred és latència per a un usuari
Sense estat al disc El sistema de fitxers és efímer i viu a la memòria Les instàncies neixen i moren sense avisar
Sense processos en segon pla fora de la petició Tret que hi hagi CPU sempre activa Per defecte la CPU es congela entre peticions
Contenidor Linux x86-64 o arm64 Sense privilegis, sense accés al nucli Aïllament multiinquilí

Aquestes sis línies són tot. No cal aprendre cap format de desplegament nou, i aquesta és la raó per la qual Cloud Run s'ha convertit en l'opció per defecte per a aplicacions web a Google Cloud.

Dos errors freqüents contra el contracte, dits ja:

# MALAMENT: port fix i nomes escolta a localhost
app.run(host="127.0.0.1", port=5000)

# BE: el port el diu la plataforma i s escolta a totes les interficies
import os
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

El primer produeix l'error més comú dels principiants a Cloud Run: «The user-provided container failed to start and listen on the port defined provided by the PORT environment variable». Quan el vegis, ja saps què has de mirar.

Com funciona per dins

flowchart TB
    U["Usuaris"] --> LB["Frontal HTTPS gestionat<br/>o balancejador global"]
    LB --> CR["Cloud Run - servei alpinashop-web"]
    subgraph CR
      direction LR
      R1["Revisió 0042<br/>90% del trànsit"]
      R2["Revisió 0043<br/>10% del trànsit"]
    end
    R1 --> I1["Instància 1<br/>concurrència 80"]
    R1 --> I2["Instància 2"]
    R2 --> I3["Instància 3"]
    I1 -.-> SQL["Cloud SQL<br/>alpinashop-pedidos"]
    I1 -.-> SM["Secret Manager"]
    I1 -.-> GCS["Bucket<br/>alpinashop-catalogo"]

Tres conceptes que cal fixar des d'ara:

  • Servei: l'entitat estable, amb nom i URL. alpinashop-web.
  • Revisió: una versió immutable del servei (imatge + configuració). alpinashop-web-00042-abc. No es modifica mai: cada canvi crea una revisió nova.
  • Instància: un contenidor en execució d'una revisió. Neixen i moren soles.

La immutabilitat de les revisions és el que fa possible el repartiment de trànsit i la reversió instantània de l'apartat 5.

  1. Serveis i treballs (jobs): el criteri per triar

Cloud Run té dues maneres d'executar un contenidor, i triar malament és una font clàssica d'arquitectures rares.

Servei (service) Treball (job)
Es dispara per Una petició HTTP entrant Una execució explícita (jobs execute, Scheduler, Workflows)
Ha d'escoltar a $PORT No
Acaba Mai (viu mentre hi hagi trànsit) Quan el procés acaba amb codi 0
Escala per Peticions concurrents --tasks: N tasques en paral·lel
Durada màxima Fins a 60 minuts per petició Fins a 24 hores per tasca
Reintents Els fa el client Integrats: --max-retries
Facturació Per temps d'instància Per temps de tasca

La regla de decisió, en una frase:

Si alguna cosa respon a algú, és un servei. Si alguna cosa fa una cosa i acaba, és un treball.

Aplicat a AlpinaShop:

Càrrega Tipus Per què
Catàleg web Servei Respon peticions de clients
API interna d'estoc Servei Respon al procés de sincronització
Informe nocturn de vendes Treball S'executa, produeix un fitxer, acaba
Reindexació del cercador Treball Procés per lots, 20 minuts, sense client esperant
Migració d'esquema de BD Treball Una execució, amb reintents i codi de sortida
Processar imatge pujada Cloud Function (06-03) Esdeveniment únic, ja resolt i funcionant

L'antipatró clàssic —i es veu molt— és muntar l'informe nocturn com un servei amb un endpoint /ejecutar-informe que crida Cloud Scheduler. Funciona, però: tens un endpoint HTTP que cal protegir, l'execució està limitada pel temps màxim de petició, no hi ha reintents integrats, i si triga massa el procés es talla a mitges. Un treball elimina els quatre problemes.

Un treball real d'AlpinaShop, l'informe nocturn 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

I la seva execució des de Cloud Scheduler, sense exposar cap HTTP propi:

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

Fixa't en --time-zone="Europe/Madrid": sense això, el 30 3 * * * és UTC i l'informe s'executarà a les 5:30 a l'estiu. És un error que es descobreix al març o a l'octubre i desconcerta durant hores.

  1. Desplegar el catàleg: gcloud run deploy bandera a bandera

La imatge del catàleg ja és a Artifact Registry des de 06-01, construïda per Cloud Build i etiquetada amb $COMMIT_SHA. Només falta desplegar-la. Aquesta és la comanda completa d'AlpinaShop, i tot seguit 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è fa Per què aquest valor a AlpinaShop
--image Imatge a desplegar Etiquetada amb el hash del commit, mai latest (06-01). Amb latest no saps què hi ha corrent
--region Regió del servei europe-west1, on és tota la resta. Cloud Run és regional
--platform=managed Cloud Run gestionat L'alternativa era gke, en desús (07-01)
--service-account Identitat del servei El seu propi compte amb mínim privilegi (03-04). Sense això fa servir el compte per defecte de Compute, que és massa permissiu
--no-allow-unauthenticated Exigeix IAM per invocar El servei no s'exposa a internet directament: entrarà pel balancejador (apartat 10)
--port Port del contenidor 8080, el mateix del contracte i de la sonda hc-catalogo
--cpu vCPU per instància 1 vCPU. Es pot fraccionar (0.5, 0.25) o arribar a 8
--memory Memòria per instància 512 MiB. Mesurada, no endevinada: veure apartat 6
--concurrency Peticions simultànies per instància 80, el valor per defecte, validat mesurant (apartat 6)
--min-instances Instàncies sempre enceses 1, perquè cap client pateixi arrencada en fred
--max-instances Sostre d'instàncies 30, com a tallafoc de cost i de connexions a Cloud SQL
--timeout Temps màxim per petició 60 s. El màxim és 60 min, però un catàleg que trigui 60 s ja està trencat
--cpu-throttling CPU només durant la petició Mode per defecte i el més barat
--execution-environment=gen2 Entorn d'execució Segona generació: compatibilitat completa amb Linux, arrencada una mica més lenta, necessari per muntar volums
--set-env-vars Variables d'entorn Configuració no sensible
--set-secrets Secrets muntats La contrasenya mai com a variable en clar (03-06)
--add-cloudsql-instances Connector a Cloud SQL Habilita el sòcol Unix cap a alpinashop-pedidos
--labels Etiquetes Les quatre d'AlpinaShop des de 01-04, imprescindibles per a 07-05
--no-traffic Desplega sense enviar-li trànsit Crea la revisió però no l'activa. Clau per al canari
--tag=candidata Etiqueta de revisió Li dona una URL pròpia per provar-la abans de donar-li trànsit

Les dues últimes són les que converteixen un desplegament en un desplegament segur, i mereixen el seu propi apartat.

  1. El mateix servei en Terraform

AlpinaShop gestiona la seva infraestructura amb Terraform des de 06-07, així que la comanda anterior és per experimentar: el que va a main és 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

  # Nomes transit intern i del balancejador: ningu no arriba per 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 nomes durant la peticio
        startup_cpu_boost = true    # CPU extra durant l arrencada
      }

      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]
      }
    }
  }

  # Repartiment de transit explicit: 100% a la ultima revisio estable
  traffic {
    type    = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
    percent = 100
  }

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

Quatre observacions importants sobre aquest HCL:

  1. Fes servir google_cloud_run_v2_service, no google_cloud_run_service. El recurs v1 continua existint per compatibilitat i la seva sintaxi (basada en anotacions de Knative) és molt pitjor. Tot el que és nou va en v2.
  2. cpu_idle = true és l'equivalent a --cpu-throttling: la CPU només es factura durant les peticions. false significa CPU sempre activa, i és fins a quatre vegades més car.
  3. startup_cpu_boost = true dona CPU addicional durant l'arrencada. Redueix l'arrencada en fred entre un 20 % i un 50 % en aplicacions amb inicialització pesada (JVM, frameworks grans), i només es paga aquell mig segon.
  4. ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" és la peça de seguretat: ningú no pot arribar al servei per la seva URL *.run.app. Només hi entra trànsit des del balancejador global.

I hi ha una tensió que convé resoldre explícitament, perquè és la pregunta que tothom es fa en arribar aquí: si el desplegament el fa el pipeline, què gestiona Terraform?

Aspecte Qui ho gestiona Motiu
Existència del servei, compte de servei, ingress, secrets, escalat, VPC Terraform És la forma; canvia per decisió humana
Versió de la imatge desplegada Cloud Build Canvia diverses vegades al dia; és contingut
Repartiment de trànsit entre revisions Cloud Build durant el canari, Terraform en repòs És una operació, no una configuració

És exactament la regla de 06-07: Terraform gestiona la forma, no el contingut. A la pràctica es resol amb lifecycle { ignore_changes = [template[0].containers[0].image, traffic] } al recurs, perquè Terraform no intenti tornar la imatge a la versió del seu fitxer cada vegada que corre.

  1. Revisions i repartiment de trànsit: canari, etiquetes i reversió

Aquí hi ha una de les millors capacitats de Cloud Run, i és pràcticament gratis.

Etiquetes de revisió: provar sense arriscar

En desplegar amb --no-traffic --tag=candidata, Cloud Run crea la revisió i li assigna una URL pròpia:

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

Aquesta URL apunta només a aquella revisió, amb zero trànsit de producció. Serveix per a:

  • Provar la versió nova contra la base de dades real, sense que cap client la vegi.
  • Passar les proves de fum automàtiques del pipeline.
  • Ensenyar-la a algú abans de publicar-la.
# Prova de fum contra la revisio candidata, amb testimoni perque el servei es privat
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 canari al 10 %

Si la prova passa, se li dona un percentatge petit de trànsit real:

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

A partir d'aquell moment, una de cada deu peticions va a la revisió nova. S'observa durant vint o trenta minuts al tauler de 06-04 —taxa d'error, latència p95, errors a Error Reporting— i es decideix.

Progressió completa recomanada:

Pas Trànsit Què s'observa Durada
1 0 % (només etiqueta) Proves de fum 2 min
2 10 % Taxa d'error 5xx, latència p95 20-30 min
3 50 % El mateix, amb volum 30-60 min
4 100 % Estabilització
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ó instantània

I aquesta és la part que canvia com es dorm a les nits. Si la revisió nova falla:

# Veure les revisions i el seu repartiment actual
gcloud run revisions list --service=alpinashop-web --region=europe-west1

# Tornar TOT el transit a la revisio anterior coneguda com a bona
gcloud run services update-traffic alpinashop-web \
  --region=europe-west1 \
  --to-revisions=alpinashop-web-00041-xyz=100

Triga segons i no reconstrueix ni redesplega res, perquè la revisió anterior continua existint íntegra. Compara-ho amb el MIG: calia crear una plantilla d'instància nova, llançar una actualització progressiva i esperar que les VM se substituïssin una a una — entre cinc i quinze minuts, amb el lloc degradat mentrestant.

Consell operatiu: tingues la comanda de reversió escrita al runbook (07-06) amb el número de revisió emplenat com a variable. A les tres de la matinada ningú no recorda la sintaxi de --to-revisions.

Dues advertències sobre el canari

  1. El canari no protegeix contra canvis d'esquema de base de dades. Si la revisió nova fa ALTER TABLE, el 90 % que continua a la vella també veu la taula canviada. Les migracions d'esquema han de ser compatibles cap enrere (afegir columnes, mai reanomenar-les al mateix desplegament), i executar-se com un treball a part.
  2. Amb poc trànsit, el 10 % no significa res estadísticament. Si AlpinaShop rep 30 peticions per minut fora de campanya, el 10 % són 3 peticions. Un canari que no veu errors durant 20 minuts amb 60 peticions no ha demostrat gran cosa. En volums baixos convé pujar el percentatge i allargar el temps, o fer el canari en hores de més trànsit.

  1. Concurrència: el concepte que més es malinterpreta

Si t'emportes un sol apartat d'aquesta lliçó, que sigui aquest.

Concurrència és el nombre màxim de peticions que Cloud Run envia simultàniament a una mateixa instància abans d'arrencar-ne una altra.

El valor per defecte és 80. I aquí hi ha la font de tots els malentesos: molta gent arriba a Cloud Run des de Cloud Functions o des d'AWS Lambda, on el model és una petició per instància, i assumeix que Cloud Run funciona igual. No.

Comparació honesta dels dos models

Una petició per instància (--concurrency=1) Concurrència alta (--concurrency=80)
Aïllament Total: cada petició té el seu contenidor Compartit: 80 peticions comparteixen memòria i CPU
Raonament Trivial: no hi ha concurrència per gestionar Cal pensar en variables globals i estat compartit
Cost Molt alt: una instància per petició Baix: 80 peticions paguen una instància
Arrencades en fred Moltíssimes: cada pic crea instàncies Poques
Rendiment en E/S Dolentíssim: la CPU està ociosa esperant la BD Excel·lent: mentre una petició espera, una altra fa servir la CPU
Aplica bé a Càrregues de CPU intensiva, codi no segur per a fils, ML Aplicacions web i API normals

La clau és a la penúltima fila. Una petició típica del catàleg d'AlpinaShop passa així els seus 200 ms:

|-- 5 ms --|------------ 170 ms ------------|-- 25 ms --|
  analitzar    esperant a Cloud SQL           renderitzar
   (CPU)          (CPU OCIOSA)                  (CPU)

El 85 % del temps la CPU no fa res: espera la base de dades. Amb --concurrency=1, pagues una vCPU sencera perquè estigui ociosa 170 ms de cada 200. Amb --concurrency=80, aquesta mateixa vCPU atén altres peticions durant aquesta espera.

Com triar-la mesurant

La concurrència correcta no s'endevina, es mesura. El procediment:

Pas 1 — Esbrinar el model de concurrència de la teva aplicació. És el primer i gairebé ningú no ho comprova:

Servidor Model Concurrència útil
Flask amb app.run() (desenvolupament) Un fil 1. No posis això en producció
Gunicorn amb workers síncrons workers × 1 Igual al nombre de workers
Gunicorn amb gthread workers × threads Aquest producte
Node.js / Express Bucle d'esdeveniments Alta: 80-250
Go / Java amb fils Alta 80-1000
Python amb asyncio (FastAPI + Uvicorn) Bucle d'esdeveniments Alta: 80-250

L'error més car de tots: posar --concurrency=80 en un servidor que només processa una petició alhora. Cloud Run n'hi envia 80, el servidor les encua, i les peticions 2 a 80 esperen el seu torn. La latència es dispara i sembla un problema de la plataforma. És un problema de configuració del servidor d'aplicacions.

Per al catàleg d'AlpinaShop, que és Flask, el Dockerfile acaba així:

# 4 processos x 20 fils = 80 peticions simultanies reals
CMD exec gunicorn --bind :$PORT \
                  --workers 4 \
                  --threads 20 \
                  --worker-class gthread \
                  --timeout 0 \
                  main:app

--timeout 0 desactiva el temporitzador de Gunicorn perquè el temps d'espera el governa Cloud Run amb --timeout=60s. Tenir dos temporitzadors competint produeix talls inexplicables.

Pas 2 — Prova de càrrega amb valors diferents. Es despleguen revisions amb etiqueta i es mesura cadascuna:

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

I amb una eina de càrrega (hey, k6, vegeta) contra cada URL etiquetada. Resultats reals del catàleg, amb 50 peticions per segon sostingudes durant 5 minuts:

Concurrència Instàncies usades Latència p50 Latència p95 Cost relatiu
1 12 190 ms 240 ms 12×
10 5 195 ms 260 ms
40 2 200 ms 290 ms
80 1 210 ms 340 ms
160 1 380 ms 1200 ms

Lectura de la taula, que és on hi ha l'aprenentatge:

  • D'1 a 80, la latència p50 amb prou feines puja (190 → 210 ms) i el cost es divideix per dotze. Regal evident.
  • De 80 a 160 la latència es dispara: s'han superat els 80 fils reals de Gunicorn i les peticions s'encuen. El sostre no el posa Cloud Run, el posa la teva aplicació.
  • El p95 es degrada abans que el p50. Per això es mesura el p95: és on es veu el patiment abans que sigui evident.

Pas 3 — Ajustar CPU i memòria a la concurrència triada. Són paràmetres acoblats:

  • Amb concurrència 80, la instància necessita memòria per a 80 peticions simultànies. Si cadascuna consumeix 4 MiB d'estructures, són 320 MiB només de peticions, més el runtime.
  • Si l'aplicació és intensiva en CPU, la concurrència alta fa que les peticions competeixin per la mateixa vCPU. Aquí es puja CPU o es baixa concurrència.

El senyal que la memòria es queda curta és inconfusible als registres:

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

Quan això passa, la instància mor i les peticions en curs es perden. És de les poques fallades de Cloud Run que es manifesten com a errors 5xx sense traça de l'aplicació.

  1. Escalat: zero, mínim, màxim i el model de facturació de CPU

Escalar a zero

Sense trànsit, Cloud Run apaga totes les instàncies i no cobra res. És la propietat que fa que un entorn de desenvolupament costi cèntims.

El preu d'aquesta propietat és l'arrencada en fred (cold start): la primera petició després d'un període d'inactivitat ha d'esperar que es creï una instància, es descarregui la imatge i arrenqui l'aplicació.

Fase Temps típic Com es redueix
Aprovisionar el sandbox 100-300 ms No està a la teva mà
Descarregar la imatge 200 ms - 5 s Imatge petita. És la palanca principal
Arrencar l'aplicació 100 ms - 10 s Menys dependències, càrrega mandrosa, startup_cpu_boost

Números orientatius d'arrencada en fred completa: un binari de Go en una imatge distroless de 15 MB arrenca en uns 300 ms; una aplicació Python amb moltes dependències, en 1-3 s; una JVM amb Spring Boot sense ajustar, entre 8 i 20 s. La diferència la marca la imatge i el runtime, no Cloud Run.

--min-instances: pagar per no tenir fred

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

Manté N instàncies sempre vives. La instància inactiva es factura a una tarifa reduïda —de l'ordre de la desena part de l'activa—, així que una instància mínima és barata: uns pocs euros al mes.

Criteri per a AlpinaShop:

Servei --min-instances Raó
alpinashop-web (prod) 1 Un client que arriba a les 7:00 no ha d'esperar 2 s
alpinashop-web (dev) 0 Ningú no pateix una arrencada en fred en desenvolupament
Serveis interns 0 Els crida programari, que pot esperar
Durant la campanya de tardor 3 Esmorteir l'arrencada del matí

Advertiment important: --min-instances redueix molt les arrencades en fred, però no les elimina. Si el trànsit puja de cop, cal crear instàncies noves i aquestes arrenquen en fred igualment. I hi ha un detall contraintuïtiu: amb CPU limitada a les peticions (cpu_idle=true), les instàncies mínimes tenen la CPU congelada entre peticions, així que si la teva aplicació fa alguna cosa periòdica en segon pla, no s'executarà.

--max-instances: el tallafoc de cost

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

Això és protecció, i cal posar-ho sempre. Dos motius:

  1. Cost. Un bucle infinit al codi, un rastrejador agressiu o un atac poden llançar centenars d'instàncies. Sense sostre, la factura del dia és un incident.
  2. Dependències aigües avall. Cloud SQL té un límit de connexions. Si cada instància obre 5 connexions i arrenquen 200 instàncies, són 1000 connexions contra una base de dades que admet 100: Cloud Run escala perfectament i tomba la base de dades.

La fórmula per triar-lo:

max-instances = min(
    capacitat_de_la_dependencia_mes_feble / connexions_per_instancia,
    pic_de_transit_esperat / concurrencia * marge
)

Per a AlpinaShop: Cloud SQL admet ~100 connexions útils, el pool per instància és de 3 → 33 instàncies. El pic esperat en campanya són 20 req/s, amb concurrència 80 → menys d'1 instància, amb marge ×5 per a pics bruscos → 5. Es tria 30: còmode per al trànsit, per sota del límit de la base de dades.

Quan s'assoleix el màxim, Cloud Run no rebutja: encua les peticions i les va servint, i només retorna 429 si la cua se satura. És un comportament amable, però convé tenir una alerta:

gcloud monitoring channels list   # el canal de la Marta, de 06-04
# Alerta quan se supera el 80% del maxim durant 5 minuts

CPU sempre activa davant de CPU durant la petició

--cpu-throttling (per defecte) --no-cpu-throttling
CPU entre peticions Congelada Disponible
Preu de la vCPU Base De l'ordre de 4 vegades menor per segon, però es paga tot el temps de vida de la instància
Processos en segon pla No funcionen Funcionen
Casos d'ús Aplicacions web normals Consumidors de streaming, serveis amb memòria cau que es refresca sola, WebSockets amb feina entre missatges

Per al catàleg, --cpu-throttling és el correcte: entre peticions no hi ha res a fer. El parany clàssic és llançar un fil en segon pla per «enviar mètriques cada 30 segons» i descobrir que no s'executa mai. Amb CPU congelada, aquest fil s'atura.

  1. Connectar Cloud Run amb la resta de la plataforma

Un servei aïllat no val res. Aquestes són les cinc connexions d'AlpinaShop.

Cloud SQL: per connector, no per IP

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

Cloud Run munta un sòcol Unix a /cloudsql/<nom-de-connexió> que parla amb la instància per un canal xifrat, sense IP pública i sense obrir tallafoc. La cadena de connexió de l'aplicació:

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"],       # injectada des de Secret Manager
        database="tienda",
        query={"unix_sock": f"/cloudsql/{CONEXION}/.s.PGSQL.5432"},
    ),
    pool_size=3,           # baix: multiplica pel nombre d instancies
    max_overflow=0,        # sense connexions extra sorpresa
    pool_pre_ping=True,    # detecta connexions mortes despres d una pausa
    pool_recycle=1800,
)

Les quatre opcions del pool no són decoratives:

  • pool_size=3: recorda la fórmula de --max-instances. Un pool de 20 per instància és la recepta per esgotar la base de dades.
  • max_overflow=0: impedeix que el pool obri connexions addicionals sota càrrega, que és justament quan no t'ho pots permetre.
  • pool_pre_ping=True: imprescindible a Cloud Run. Una instància amb la CPU congelada durant minuts torna amb connexions mortes; sense pre-ping, la primera petició després de la pausa falla.
  • pool_recycle=1800: recicla connexions cada mitja hora abans que les talli el servidor.

L'alternativa moderna és connectar per IP privada a través de la VPC (punt següent), que evita el connector i dona menys latència. És el que AlpinaShop farà quan la xarxa estigui ordenada a 07-03.

Secret Manager: muntat, no copiat

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

L'aplicació llegeix os.environ["DB_PASSWORD"] i no sap que ve de Secret Manager. Alternativa, muntar-lo com a fitxer:

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

Diferència pràctica que convé conèixer: una variable d'entorn es llegeix una vegada en arrencar la instància; un fitxer muntat es pot rellegir. Si rotes un secret, les instàncies vives amb variable d'entorn continuen fent servir el valor vell fins que se substitueixin. Amb :latest i fitxers, la rotació es propaga sola.

I la contrapartida: fer servir :latest significa que una rotació mal feta trenca el servei sense que ningú desplegui res. Per a secrets crítics, fixar la versió (:7) i actualitzar-la en un desplegament controlat és més segur.

VPC directa: sortir per la xarxa privada

Per defecte, el trànsit sortint de Cloud Run va a internet. Per arribar a recursos amb IP privada —una rèplica de Cloud SQL, Memorystore, o l'ERP de Sabadell a l'altra banda del túnel de 07-03— cal connectar el servei a la VPC:

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

Això és VPC directa (Direct VPC egress), que substitueix l'antic connector d'accés a VPC sense servidor (Serverless VPC Access connector):

Connector (antic) VPC directa (actual)
Infraestructura VM gestionades que cal dimensionar i pagar Cap
Cost De l'ordre de desenes de € al mes Sense cost addicional
Latència Un salt extra Directa
Escalat Manual (nombre d'instàncies del connector) Automàtic

Si trobes documentació que parla de connectors, és anterior. VPC directa és el que cal fer servir avui.

El valor de --vpc-egress decideix què surt per la VPC:

Valor Efecte
private-ranges-only Només els rangs RFC 1918 van per la VPC; internet surt directe. L'habitual
all-traffic Tot surt per la VPC, inclòs internet. Necessari si vols que surti pel Cloud NAT alpinashop-nat-euw1 amb IP fixa (03-01), per exemple perquè la passarel·la de pagament té llista blanca d'IP

Pub/Sub push amb OIDC

A 04-04 es va dir que el model push encaixa amb Cloud Run. Així es connecta, sense obrir el servei a ningú:

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

L'essencial és --push-auth-service-account: Pub/Sub signa cada lliurament amb un testimoni OIDC d'aquell compte. El servei, que està en --no-allow-unauthenticated, només accepta la petició si aquell compte té roles/run.invoker:

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

Cap secret compartit, cap capçalera màgica, cap comprovació al codi. La plataforma valida el testimoni abans que la petició arribi al teu contenidor.

Eventarc: reaccionar al que passa a GCP

Eventarc lliura a Cloud Run esdeveniments de més d'un centenar de serveis de Google Cloud, amb el format estàndard 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ó amb el que ja s'ha vist: Cloud Functions 2a generació (06-03) està construïda sobre Cloud Run i Eventarc. La funció procesar-imagen-producto és, per sota, un servei de Cloud Run amb un disparador d'Eventarc. Saber-ho explica per què comparteixen límits, opcions d'escalat i model de facturació.

  1. Identitat i seguretat del servei

Quatre decisions, totes coherents amb el mòdul 3.

1. Compte de servei propi i mínim. sa-catalogo-web té exactament el que necessita i res més:

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

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

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

Si no s'especifica --service-account, Cloud Run fa servir el compte de servei per defecte de Compute Engine, que històricament té el rol Editor sobre tot el projecte. Desplegar així és donar a la teva aplicació web permís per esborrar la base de dades. És la fallada de seguretat més comuna de tot Cloud Run.

2. --no-allow-unauthenticated. El servei exigeix un testimoni d'identitat vàlid d'algú amb roles/run.invoker. Qui pot invocar-lo:

Invocador Com
El balancejador global El NEG sense servidor invoca sense testimoni; es controla amb ingress
Pub/Sub Testimoni OIDC de sa-pubsub-invoker
Un altre servei de Cloud Run Testimoni OIDC del seu propi compte
Una persona (per depurar) curl -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Un usuari final autenticat amb Google IAP davant del balancejador

3. Control d'entrada (ingress). És diferent de l'autenticació i complementari:

Valor Qui pot arribar
all Tot internet (la URL run.app és pública)
internal Només des de la VPC, VPC Service Controls i altres serveis del projecte
internal-and-cloud-load-balancing L'anterior més el balancejador global. El d'AlpinaShop

Amb internal-and-cloud-load-balancing, la URL *.run.app deixa de respondre des de fora. Tot el trànsit de clients ha de passar per Cloud Armor i la CDN, sense excepció. Això és el que impedeix la drecera de «provaré directament contra run.app», que és el forat pel qual se salta el WAF.

4. IAP per a serveis interns. El tauler d'administració intern es publica amb el mateix patró de 03-04: balancejador + IAP + Cloud Run privat. Els empleats entren amb el seu compte de Google, sense VPN i sense usuaris propis.

  1. Darrere del balancejador global: el NEG sense servidor

A 03-02 es va muntar el balancejador global d'AlpinaShop i es va deixar dit que el dia que el catàleg anés a Cloud Run n'hi hauria prou amb canviar el backend. Aquell dia ha arribat.

Un NEG sense servidor (serverless network endpoint group) és un backend que apunta a un servei de Cloud Run, App Engine o Cloud Functions en lloc de a un grup d'instàncies.

flowchart LR
    C["Clients"] --> IP["alpinashop-lb-ip<br/>IP anycast global"]
    IP --> CERT["alpinashop-cert<br/>TLS gestionat"]
    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 sense servidor<br/>neg-catalogo-run"]
    NEG --> CR["Cloud Run<br/>alpinashop-web"]

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

Tot el blau del diagrama —IP, certificat, WAF, mapa d'URL, CDN— no es toca. Només canvia el verd.

Creació del NEG:

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

I la seva incorporació al servei de backend existent:

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

Quatre particularitats del NEG sense servidor que cal conèixer, perquè són diferents de tot el que s'ha vist a 03-02:

  1. No hi ha comprovacions d'estat. El servei de backend amb NEG sense servidor no fa servir hc-catalogo. La salut la gestiona Cloud Run. Al principi desconcerta; és correcte.
  2. No hi ha mode de balanceig ni capacitat. No existeixen max-rate-per-instance ni capacity-scaler: Cloud Run escala sol.
  3. El NEG és regional, encara que el balancejador sigui global. Per servir des de diverses regions es creen diversos NEG, un per regió, i s'afegeixen al mateix servei de backend — que és exactament com es fa l'alta disponibilitat multiregió de 07-06.
  4. La CDN continua funcionant igual. Les capçaleres Cache-Control que emet Cloud Run governen la memòria cau exactament com ho feien les del MIG (03-03).

  1. La migració des del MIG, pas a pas i sense tall

Aquest és el procediment real que va executar la Marta. Està escrit per poder seguir-lo, i la seva virtut és que en cap moment hi ha un punt sense retorn.

Fase 0 — Preparació (dies abans)

# 1. Verificar que la imatge compleix el contracte, 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 "CONTRACTE OK"

# 2. Desplegar en desenvolupament primer. SEMPRE.
gcloud run deploy alpinashop-web --project=alpinashop-dev \
  --image=... --region=europe-west1 --min-instances=0

I una llista de comprovació prèvia que evita el 90 % dels ensurts:

  • [ ] L'aplicació escolta a $PORT i a 0.0.0.0.
  • [ ] No escriu res important al disc local.
  • [ ] No desa sessions a la memòria (les d'AlpinaShop són a Firestore des de 02-06).
  • [ ] Els registres van a stdout/stderr en JSON estructurat (06-06).
  • [ ] Gunicorn està configurat amb fils suficients per a la concurrència triada.
  • [ ] El pool de connexions és petit i té pool_pre_ping.
  • [ ] No hi ha fils de fons que depenguin de CPU entre peticions.

Fase 1 — Desplegar en producció sense trànsit

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 \
  [... la resta de banderes de l apartat 3 ...]

El servei existeix, funciona contra la base de dades real i cap client no el veu. Aquí es fan les proves de fum i es compara la latència amb la del MIG.

Fase 2 — Afegir el NEG al balancejador amb pes zero

Es crea el NEG i s'afegeix al servei de backend bs-catalogo-web, que ara té dos backends: el MIG i Cloud Run. El balancejador reparteix entre ells segons capacitat.

Per controlar el repartiment amb precisió, AlpinaShop fa servir la tècnica del capacity-scaler sobre el backend del MIG:

# Estat inicial: tot 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 transvasament progressiu

Es va reduint la capacitat del MIG, cosa que empeny trànsit cap a Cloud Run:

Moment capacity-scaler del MIG Trànsit aproximat a Cloud Run Què es vigila
T+0 1.0 ~0 %
T+30 min 0.5 ~30 % 5xx, p95, connexions a Cloud SQL
T+2 h 0.2 ~70 % El mateix, més cost
T+1 dia 0.0 100 % Tot, durant 24 h

A cada esglaó es miren les mateixes quatre coses al tauler de 06-04, i es comparen per backend:

# A l explorador de metriques, filtrant per backend_target_name
loadbalancing.googleapis.com/https/request_count      -> repartiment real
loadbalancing.googleapis.com/https/backend_latencies  -> p50 i p95 per backend
run.googleapis.com/container/instance_count           -> instancies creades
cloudsql.googleapis.com/database/postgresql/num_backends -> connexions obertes

La mètrica de connexions a Cloud SQL és la que més sorpreses dona, i és el motiu pel qual el transvasament es fa en hores i no en minuts.

Fase 4 — Convivència vigilada

Durant 24-72 hores el MIG continua existint amb capacity-scaler=0.0: no rep trànsit però està encès. La reversió és una sola comanda:

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

I en segons torna tot al MIG. Pagar dos dies de VM apagada de trànsit és el preu de dormir tranquil, i és un preu ridícul.

Fase 5 — Retirada

Només quan han passat els tres dies i s'ha travessat almenys un pic de trànsit:

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

# 2. Reduir el MIG a zero abans d esborrar-lo (permet tornar rapid)
gcloud compute instance-groups managed resize alpinashop-web-mig \
  --size=0 --zone=europe-west1-b

# 3. Una setmana despres, esborrar de debo, des de Terraform
#    (eliminar el recurs del .tf i fer plan/apply, mai a ma)

El pas 2 és important: reduir a zero abans d'esborrar. Un MIG de mida zero no costa còmput, conserva la seva plantilla, i tornar-lo a aixecar és un resize. L'esborrat definitiu es fa des de Terraform, perquè si es fa a mà el plan següent intentarà recrear-lo.

  1. Cost: el model i el càlcul comparat

El model de facturació

Cloud Run cobra tres coses, i només mentre hi ha instàncies vives:

Concepte Unitat Ordre de magnitud a europe-west1
CPU vCPU-segon ~0,000024 $ per vCPU-s activa; la instància mínima ociosa, al voltant d'una desena part
Memòria GiB-segon ~0,0000025 $ per GiB-s
Peticions Per milió ~0,40 $ per milió
Sortida a internet GB Tarifa estàndard d'egress (07-05)

I hi ha un nivell gratuït mensual generós —de l'ordre de 180.000 vCPU-s, 360.000 GiB-s i 2 milions de peticions— que fa que molts projectes petits costin literalment zero.

Els preus canvien i varien per regió. Consulta sempre la pàgina de preus oficial i la calculadora. El que segueix són ordres de magnitud per raonar, no un pressupost.

El càlcul d'AlpinaShop

Escenari: 1,5 milions de peticions al mes, latència mitjana 200 ms, 1 vCPU, 512 MiB, concurrència 80, min-instances=1, CPU limitada a la petició.

Temps d'instància activa. Amb concurrència 80, les peticions s'encavalquen. Una estimació conservadora del temps amb almenys una petició en vol, a partir de la distribució horària real, dona unes 28 hores al mes d'activitat ≈ 100.000 vCPU-s.

Concepte Càlcul Cost
CPU activa 100.000 vCPU-s × 0,000024 $ ~2,40 $
CPU ociosa (min-instances=1) 2.592.000 s × ~0,0000025 $ ~6,50 $
Memòria activa 50.000 GiB-s × 0,0000025 $ ~0,13 $
Memòria ociosa 1.296.000 GiB-s × ~0,00000025 $ ~0,32 $
Peticions 1,5 M × 0,40 $/M ~0,60 $
Menys nivell gratuït −2 a −3 $
Total ≈ 7-8 $/mes ≈ 7 €/mes

Comparació amb el que es venia pagant, fent servir les xifres de 02-07:

Concepte MIG (abans) Cloud Run (després)
Còmput 2 × e2-medium 24×7 ≈ 50 €/mes ≈ 7 €/mes
Balancejador global + IP ~20 €/mes ~20 €/mes (igual)
Discos persistents ~4 €/mes 0 €
Agent d'operacions Inclòs, però cal instal·lar-lo No cal
Total infraestructura ≈ 74 €/mes ≈ 27 €/mes
Apedaçament del SO, plantilles, actualitzacions progressives ~2-4 h/mes de la Marta 0 h

Estalvi: uns 47 €/mes, un 63 % del cost de la botiga. I aquesta és la part petita. Les 2-4 hores mensuals de la Marta valen més que els 47 €, i el temps de reversió passa de 10 minuts a 5 segons.

Però cal dir la veritat completa, perquè aquest curs no ven:

  • Si el trànsit fos constant i alt les 24 hores, el MIG amb descompte per ús compromès seria més barat. Cloud Run guanya amb trànsit irregular, que és el cas d'AlpinaShop, no tots els casos.
  • En campanya de tardor la factura de Cloud Run puja, perquè es paga l'ús. És el correcte —es paga quan es ven— però significa que la factura ja no és plana. Es planifica a 07-05.
  • El balancejador global continua costant el mateix i ara representa el 74 % de la factura del catàleg. És el que passa quan s'optimitza bé: el major cost es desplaça a una altra banda.

  1. Límits i quan Cloud Run no encaixa

Els límits que cal conèixer abans de decidir (verifica'ls a la documentació, canvien a millor amb el temps):

Límit Valor orientatiu
Temps màxim per petició 60 minuts
Memòria per instància Fins a 32 GiB
CPU per instància Fins a 8 vCPU
Concurrència màxima 1.000
Instàncies màximes 1.000 per defecte (ampliable)
Mida de petició i resposta 32 MiB (sense streaming)
GPU Disponible en regions seleccionades
Emmagatzematge local A la memòria (compta contra el límit) o volums muntats

I els escenaris on Cloud Run no és la resposta:

Escenari Per què no Alternativa
Processos que han de córrer permanentment sense peticions La CPU es congela; amb min-instances i CPU sempre activa es pot, però pagues per una VM cara Compute Engine o GKE
Bases de dades, cues, Elasticsearch Necessiten estat persistent i disc Serveis gestionats
Aplicacions que necessiten disc local ràpid i gran El sistema de fitxers viu a la memòria Compute Engine amb SSD local
Tasques de més de 60 minuts Límit dur Cloud Run jobs (24 h) o Batch
Programari amb llicència lligada al maquinari No hi ha maquinari per llicenciar Compute Engine amb llicència pròpia
Protocols que no siguin HTTP/gRPC/WebSocket (per exemple, un servidor de joc UDP) Només hi entra trànsit HTTP Compute Engine amb balancejador de xarxa
Necessitat d'accés al nucli, mòduls o privilegis Sandbox sense privilegis GKE amb nodes propis

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

A 02-07 es va fer aquesta comparació amb la informació d'aleshores. Ara, amb sis mòduls més de recorregut, es pot fer amb criteri de debò.

Criteri Cloud Run Cloud Functions 2a gen GKE Autopilot Compute Engine
Unitat de desplegament Contenidor Funció Contenidor + manifestos VM
Escala a zero No (pla de control fix) No
Facturació Per ús, al segon Per ús Per recursos dels pods Per VM encesa
Arrencada en fred Segons, mitigable Segons No aplica No aplica
Feina operativa Mínima Mínima Mitjana Alta
Portabilitat Alta (contenidor estàndard) Mitjana Molt alta Mitjana
Corba d'aprenentatge Baixa Molt baixa Alta Mitjana
Serveis que es criden entre si Bé, fins a certa escala Regular Excel·lent Manual
Càrregues per lots llargues Jobs, fins a 24 h No (límit curt)
Estat al disc No No Sí (volums)
Control del sistema operatiu No No Parcial Total
Cost amb trànsit irregular El millor El millor per a esdeveniments Dolent (cost base) Dolent
Cost amb trànsit constant alt Bo Dolent Bo El millor amb compromís
Elecció d'AlpinaShop Catàleg web procesar-imagen-producto Es retira Es retira

Les quatre preguntes que resolen gairebé totes les decisions, en aquest ordre:

  1. És un contenidor HTTP sense estat amb trànsit irregular? → Cloud Run. És el cas majoritari.
  2. És una reacció a un esdeveniment únic, de codi breu? → Cloud Functions (que per sota és Cloud Run).
  3. Hi ha molts serveis que es criden entre si, amb estat, DaemonSets o operadors? → GKE.
  4. Necessites el sistema operatiu, disc local, llicències lligades o protocols no HTTP? → Compute Engine.

I l'observació que tanca el continu d'abstracció de 02-07: la resposta correcta per a AlpinaShop va resultar ser l'esglaó que no s'havia provat. Es va passar per VM, per App Engine i per Kubernetes abans d'arribar a l'opció que encaixava. Això no va ser temps perdut: va ser com es va adquirir el criteri per decidir.

  1. Apagar alpinashop-web-mig i el que això simplifica

La llista de coses que deixen d'existir en retirar el MIG és més llarga del que sembla, i és l'argument més fort a favor del moviment:

Desapareix Què implicava
La plantilla d'instància Un recurs més a Terraform, amb la seva imatge base i la seva versió
L'startup script Cent línies de bash que instal·laven Docker i arrencaven el contenidor
L'apedaçament del sistema operatiu Actualitzacions de seguretat mensuals de la Marta
L'agent d'operacions Instal·lació, configuració i actualització per tenir mètriques de memòria
La comprovació d'estat hc-catalogo Ja no cal per al backend (continua sent útil per a les uptime checks)
La política d'autoescalat del MIG CPU objectiu, període d'estabilització, retard d'inicialització
Les actualitzacions progressives max-surge, max-unavailable i esperar deu minuts
Les claus SSH i l'accés per IAP a les VM Una superfície d'atac sencera
Els discos persistents i les seves instantànies Recursos que s'obliden i es paguen (07-05)
La reserva de capacitat per a la campanya Cloud Run escala sol

Deu elements menys per mantenir, entendre, documentar i explicar a qui vingui després. Per a una empresa amb una persona d'infraestructura, aquesta reducció val més que l'estalvi econòmic.

El que que cal continuar fent, perquè ningú no es confongui:

  • Actualitzar la imatge base del Dockerfile i reconstruir. Cloud Run apedaça el sandbox, no el teu contenidor.
  • Vigilar les vulnerabilitats de la imatge a Artifact Registry (07-04).
  • Gestionar migracions d'esquema de la base de dades.
  • Tot el relacionat amb l'aplicació en si.

L'arquitectura de còmput d'AlpinaShop queda, per fi, així:

flowchart TB
    subgraph BORDE["Vora global"]
      IP["alpinashop-lb-ip"] --> ARM["Cloud Armor pol-catalogo-web"] --> CDN["Cloud CDN"]
    end
    CDN --> NEG["NEG sense servidor"]
    NEG --> RUN["Cloud Run alpinashop-web<br/>0 a 30 instàncies"]
    RUN --> SQL["Cloud SQL alpinashop-pedidos<br/>HA regional"]
    RUN --> FS["Firestore - sessions"]
    RUN --> SEC["Secret Manager"]
    GCS["Bucket alpinashop-catalogo"] --> CDN
    GCS -->|"esdeveniment"| FN["Cloud Function<br/>procesar-imagen-producto"]
    RUN -->|"comanda"| 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 a la ruta de servei de la botiga. I alpinashop-cluster queda sense càrregues de producció: es conserva com a entorn de proves de contenidors i per al que pugui venir, amb una nota a DA-004 per revisar si val la pena mantenir-lo.

Errors Habituals i Consells

  • Escoltar en un port fix o a 127.0.0.1. És l'error número u. Llegeix $PORT i escolta a 0.0.0.0.
  • Desplegar sense --service-account. Heretes el compte per defecte de Compute Engine, que sol tenir Editor sobre el projecte. La teva web tindria permís per esborrar la base de dades.
  • Posar --concurrency=80 amb un servidor d'un sol fil. Les peticions s'encuen dins del contenidor i la latència es dispara. Configura Gunicorn, Uvicorn o el que facis servir abans de tocar la concurrència.
  • No posar --max-instances. Un bucle infinit o un rastrejador et crea centenars d'instàncies, i a més tomba Cloud SQL per esgotament de connexions.
  • Pool de connexions gran. pool_size=20 × 30 instàncies = 600 connexions contra una base de dades que aguanta 100. Pool petit, max_overflow=0 i pool_pre_ping=True.
  • Desar alguna cosa al sistema de fitxers i esperar trobar-la. El disc és efímer i viu a la memòria: escriure un fitxer de 300 MiB consumeix 300 MiB del teu límit de memòria.
  • Llançar fils en segon pla amb CPU limitada a la petició. No s'executen. O fas servir CPU sempre activa, o mous aquesta feina a un job o a Cloud Scheduler.
  • Fer servir la URL *.run.app com a URL pública del producte. Canvia si recrees el servei, se salta Cloud Armor i la CDN, i no és el teu domini. Balancejador sempre.
  • Confondre autenticació amb ingress. --no-allow-unauthenticated diu qui pot invocar; ingress diu per on pot arribar. Calen tots dos.
  • Desplegar amb latest. Perds la traçabilitat de quina versió corre i la reversió deixa de ser fiable. Etiqueta amb $COMMIT_SHA.
  • Creure que el canari protegeix de tot. No protegeix de canvis d'esquema de base de dades ni serveix de res amb trànsit baix.
  • Oblidar --timeout 0 a Gunicorn. Dos temporitzadors competint produeixen talls que semblen aleatoris.
  • Consell: mesura la concurrència amb una prova de càrrega real abans de fixar-la. És el paràmetre amb major impacte conjunt en cost i latència.
  • Consell: fes servir --tag a cada desplegament, encara que no facis canari. Tenir una URL per revisió és útil per depurar i no costa res.
  • Consell: activa startup_cpu_boost. És gratis a la pràctica i redueix l'arrencada en fred de manera apreciable en runtimes pesats.
  • Consell: la imatge petita és la millor optimització d'arrencada en fred. Una imatge distroless o alpine ben feta arrenca molt abans que una ubuntu amb mig sistema a dins.

Exercicis

Exercici 1 — Triar concurrència, CPU, memòria i instàncies

AlpinaShop desplegarà a Cloud Run un servei nou: alpinashop-buscador, una API de cerca del catàleg.

Dades mesurades a l'entorn de desenvolupament:

  • Escrit en Python amb FastAPI + Uvicorn (asíncron, un sol procés).
  • Cada petició: 15 ms de CPU (tokenitzar i puntuar) + 120 ms esperant Elasticsearch.
  • Consum de memòria: 180 MiB en repòs + ~2 MiB per petició en vol.
  • Trànsit: 5 peticions/segon de mitjana, 40 peticions/segon en campanya.
  • Elasticsearch admet 200 connexions concurrents en total i el comparteix amb dos serveis més.
  • Ningú no tolera més de 800 ms de latència p95.

Determina --concurrency, --cpu, --memory, --min-instances i --max-instances, justificant cada valor amb les dades. Indica a més què canviaries a l'arrencada de l'aplicació.

Exercici 2 — Dissenyar el desplegament segur al pipeline

Escriu els passos de Cloud Build (cloudbuild.yaml) que implementen el desplegament canari del catàleg amb aquestes regles:

  1. Construir la imatge etiquetada amb $COMMIT_SHA.
  2. Desplegar sense trànsit, amb l'etiqueta candidata.
  3. Executar una prova de fum contra la URL de l'etiqueta; si falla, avortar.
  4. Donar el 10 % del trànsit.
  5. Esperar 10 minuts i comprovar automàticament que la taxa d'error 5xx de la revisió nova està per sota de l'1 %; si no, revertir a 0 % i fer fallar la compilació.
  6. Només llavors, esperar l'aprovació manual de la Marta per passar al 100 %.

Indica també quins permisos necessita el compte de servei del pipeline i per què el pas 5 és el que més gent omet.

Exercici 3 — Diagnosticar una migració que va malament

Tres hores després de moure el 70 % del trànsit a Cloud Run, la Marta observa això:

  • La latència p50 del backend de Cloud Run és de 210 ms; la del MIG, 195 ms. Acceptable.
  • La latència p99 de Cloud Run és de 4,2 segons; la del MIG, 380 ms.
  • A Cloud Logging apareixen uns 40 errors per hora: sqlalchemy.exc.OperationalError: server closed the connection unexpectedly.
  • El nombre d'instàncies oscil·la entre 1 i 9 constantment, amb pujades i baixades cada pocs minuts.
  • cloudsql.googleapis.com/database/postgresql/num_backends ha passat de 12 a 34.
  • Error Reporting mostra 6 casos de Memory limit of 512 MiB exceeded.

Diagnostica cada símptoma, indica la causa arrel probable de cadascun, i escriu el pla de correcció ordenat per prioritat. Decideix a més si cal revertir al MIG o es pot corregir en calent.

Solucions

Solució 1 — Triar concurrència, CPU, memòria i instàncies

Punt de partida: el perfil de la petició. 15 ms de CPU davant de 120 ms d'espera. La CPU treballa l'11 % del temps. És un servei dominat per l'E/S, exactament el cas on la concurrència alta és rendible.

--concurrency. Tres restriccions alhora, i cal respectar la més estricta:

  1. Per CPU: amb 15 ms de CPU cada 135 ms totals, una vCPU podria sostenir teòricament unes 9 peticions simultànies abans de saturar-se. Aquest és el límit dur.
  2. Per Elasticsearch: 200 connexions compartides entre tres serveis, diguem-ne 60 per al cercador. Si cada petició obre una connexió, concurrència × instàncies ≤ 60.
  3. Per latència: el pressupost p95 és de 800 ms, i la petició base són 135 ms. Hi ha marge per a una mica de cua, però no gaire.

Amb 1 vCPU, posar concurrència per damunt de 9-10 fa que les peticions esperin CPU. Es tria --concurrency=10. I si es puja a --cpu=2, es podria anar a 20; amb el trànsit d'aquest servei no compensa.

--cpu=1. Suficient per a la concurrència triada. Pujar a 2 vCPU només té sentit si es puja també la concurrència, i el trànsit no ho justifica.

--memory=512Mi. Càlcul: 180 MiB de base + 10 peticions × 2 MiB = 200 MiB, més el runtime de Python i marge. 512 MiB deixa folgança de més del doble. Posar 256 MiB seria jugar amb foc, perquè quedar-se sense memòria mata la instància, no l'alenteix.

--max-instances. El pic són 40 req/s. Amb 135 ms per petició i concurrència 10, cada instància sosté unes 74 req/s en teoria; a la pràctica, amb marge, diguem-ne 40-50. N'hi hauria prou amb 1-2 instàncies. Però el límit real el posa Elasticsearch: 60 connexions / 10 per instància = 6 instàncies. Es tria --max-instances=6, que cobreix el pic amb marge ampli i protegeix el backend compartit, que és el recurs escàs.

--min-instances=1. Un cercador és de cara al client i una arrencada en fred d'1-2 s a la primera cerca del matí és visible i molesta. Una instància mínima costa uns pocs euros al mes. A alpinashop-dev, 0.

Què canviar a l'arrencada. FastAPI amb Uvicorn en un sol procés és asíncron, així que pot amb diverses peticions simultànies —però només si el codi és realment asíncron. La comprovació crítica:

# MALAMENT: client sincron dins d una funcio async. Bloqueja el bucle d esdeveniments
# i la concurrencia efectiva passa a ser 1, per molt async que sigui l endpoint.
@app.get("/buscar")
async def buscar(q: str):
    r = requests.get(f"{ES}/catalogo/_search", json=consulta(q))   # BLOQUEJA
    return r.json()

# BE: client asincron
@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()

Aquest és exactament l'error de l'apartat 6 amb una altra cara: un async def que crida una biblioteca síncrona no és concurrent, i la concurrència 10 configurada a Cloud Run no serveix de res.

A més: crear el client HTTP una vegada en arrencar i reutilitzar-lo (no a cada petició, com a l'exemple simplificat), i arrencar amb uvicorn main:app --host 0.0.0.0 --port $PORT --workers 1 — un sol worker, perquè el paral·lelisme el dona el bucle d'esdeveniments i Cloud Run afegeix instàncies.

Solució 2 — Dissenyar el desplegament segur al pipeline

# cloudbuild.yaml — desplegament canari d alpinashop-web
substitutions:
  _REGION: europe-west1
  _SERVICIO: alpinashop-web
  _IMAGEN: europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo

steps:

# 1. Construir amb 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 SENSE transit, amb 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. Prova de fum contra la URL de l 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 "FALLADA A $$RUTA"; exit 1; }
      done

# 4. Canari 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 i verificar la taxa d error de la revisio nova
- id: verificar-canario
  name: gcr.io/google.com/cloudsdktool/cloud-sdk
  entrypoint: bash
  args:
    - -c
    - |
      set -e
      REV=${_SERVICIO}-$SHORT_SHA
      sleep 600     # 10 minuts d observacio

      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 "Peticions: $$TOTAL, errors 5xx: $$ERR"

      if [ "$$TOTAL" -lt 100 ]; then
        echo "AVIS: mostra insuficient ($$TOTAL peticions). No es pot concloure."
        exit 1
      fi

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

      if [ $(echo "$$RATIO > 1" | bc) -eq 1 ]; then
        echo "REVERTINT: taxa d error per damunt de l 1%"
        gcloud run services update-traffic ${_SERVICIO} \
          --region=${_REGION} --to-tags=candidata=0
        exit 1
      fi

options:
  logging: CLOUD_LOGGING_ONLY

El pas 6, l'aprovació manual, no és un pas del cloudbuild.yaml: es configura al disparador de Cloud Build amb --require-approval, tal com es va muntar a 06-01 per a producció. Quan la compilació arriba al final, queda a l'espera que la Marta aprovi, i el pas al 100 % es fa en un disparador separat o manualment amb --to-latest.

Permisos del compte de servei del pipeline (sa-deploy-prod):

Rol Per a què
roles/run.developer Desplegar serveis i canviar el repartiment de trànsit
roles/iam.serviceAccountUser sobre sa-catalogo-web Poder assignar aquell compte al servei. Aquest és el que tothom oblida i produeix un PERMISSION_DENIED desconcertant
roles/artifactregistry.writer Publicar la imatge
roles/monitoring.viewer Llegir les mètriques del pas 5
roles/logging.logWriter Escriure els registres de la compilació

Fixa't que noroles/editor ni permisos sobre Cloud SQL o Secret Manager: desplega, no administra.

Per què el pas 5 és el que gairebé tothom omet. Perquè és l'únic que requereix pensar. Els passos 1-4 són mecànics i surten a qualsevol tutorial; el 5 exigeix decidir quina mètrica, quin llindar, quant de temps i què fer si no hi ha dades suficients. Sense ell, el canari és teatre: es desplega al 10 %, ningú no ho mira, i al cap de deu minuts algú prem «aprovar» perquè no ha saltat cap alarma — que és diferent de que tot estigui bé.

La comprovació de mostra mínima (TOTAL -lt 100) és la subtilesa que separa un canari real d'un de decoratiu: amb poques peticions, l'absència d'errors no demostra res, i fer fallar la compilació obliga a mirar-ho en lloc d'aprovar a cegues.

Solució 3 — Diagnosticar una migració que va malament

Diagnòstic símptoma a símptoma.

(1) p50 correcte però p99 de 4,2 s. El p50 sa descarta un problema general de rendiment: la majoria de peticions van bé. Un p99 vint vegades pitjor amb un p50 normal és la signatura inconfusible de l'arrencada en fred. El servei es va desplegar amb --min-instances=0, o el trànsit és prou irregular com per crear instàncies constantment. La confirmació és al símptoma (4).

(2) server closed the connection unexpectedly, ~40/hora. Connexions del pool que feia temps que estaven inactives i que Cloud SQL, o la mateixa congelació de CPU de la instància, ha donat per mortes. És el cas exacte de pool_pre_ping: sense ell, la primera consulta després d'un període d'inactivitat fa servir una connexió zombi i falla. La causa arrel és manca de pool_pre_ping=True i de pool_recycle a la configuració de SQLAlchemy, agreujada per la congelació de CPU entre peticions que Cloud Run aplica i el MIG no aplicava.

(3) Instàncies oscil·lant entre 1 i 9 cada pocs minuts. És la causa d'(1), i al seu torn té la seva pròpia causa. Amb concurrència 80 i el trànsit d'AlpinaShop, una sola instància hauria de bastar de sobres. Que pugi a 9 significa que les instàncies moren i es recreen: això ho explica el símptoma (6). És un cicle viciós —mor per memòria, arrenca en fred, atén, torna a morir— que a més genera part d'(2), perquè cada mort s'emporta per davant les connexions del pool.

(4) Connexions a Cloud SQL de 12 a 34. Coherent amb 9 instàncies × pool de 3-4. No és un problema per si mateix —34 està lluny del límit— però confirma l'aritmètica i avisa: si el màxim d'instàncies estigués mal posat, aquí és on es veuria el desastre. És un símptoma per vigilar, no per corregir.

(5) Sis casos de Memory limit exceeded. Aquesta és la causa arrel principal. 512 MiB no basten per a la concurrència configurada. Les hipòtesis, per probabilitat: la concurrència és 80 i cada petició del catàleg consumeix més memòria de l'estimada (renderitza plantilles, carrega imatges a la memòria); o hi ha una memòria cau en procés que creix sense límit; o l'aplicació escriu fitxers temporals, que a Cloud Run compten com a memòria.

Cadena causal completa:

Memoria insuficient (512 MiB amb concurrencia 80)
      |
      v
La instancia mor en superar el limit
      |
      +--> Es recreen instancies -> oscil.lacio 1 a 9 (simptoma 3)
      |                                   |
      |                                   v
      |                          arrencades en fred -> p99 de 4,2 s (simptoma 1)
      |                                   |
      |                                   v
      |                          mes instancies -> 34 connexions (simptoma 4)
      |
      +--> Es perden connexions del pool
                    |
                    v
        (agreujat per manca de pool_pre_ping)
                    |
                    v
        OperationalError, 40/hora (simptoma 2)

Revertir o corregir en calent?

No cal revertir. El raonament: el 30 % del trànsit continua al MIG, el p50 és correcte, els errors són 40 a l'hora sobre milers de peticions (~0,5 %) i totes les causes són de configuració, no de codi. Revertir costaria la resta del dia i no aportaria informació nova. Ara bé, sí que cal deixar de pujar trànsit fins que estigui corregit, i tenir la comanda de reversió preparada per si alguna cosa empitjora.

Si en lloc d'això els errors fossin del 10 %, o hi hagués pèrdua de comandes, o el p50 estigués degradat, la decisió seria la contrària: revertir primer, diagnosticar després. En un incident, primer s'atura el dany.

Pla de correcció per prioritat:

Prio Acció Comanda / canvi Efecte esperat
1 Pujar memòria a 1 GiB gcloud run services update alpinashop-web --region=europe-west1 --memory=1Gi Talla la causa arrel; desapareixen (3), (1) i bona part de (2)
2 Baixar concurrència a 40 mentre s'investiga --concurrency=40 Redueix la memòria simultània a la meitat. Cost una mica major, acceptable durant el diagnòstic
3 Afegir pool_pre_ping=True i pool_recycle=1800 Canvi de codi, desplegament canari Elimina el residu de (2)
4 Posar --min-instances=1 --min-instances=1 Elimina l'arrencada en fred del primer usuari
5 Alerta de memòria Política sobre run.googleapis.com/container/memory/utilizations > 80 % Que la propera vegada avisi abans de morir
6 Perfilar la memòria de debò Cloud Profiler o anàlisi local amb càrrega Saber si 1 GiB és la solució o només un pedaç sobre una fuita

El pas 6 és el que no cal saltar-se. Pujar la memòria és l'acció correcta ara, perquè atura l'hemorràgia. Però si hi ha una fuita de memòria real —una memòria cau que creix sense límit, per exemple— amb 1 GiB simplement trigarà el doble a morir, i el problema reapareixerà en campanya amb el pitjor moment possible. La diferència entre resoldre un incident i ajornar-lo és exactament aquest últim pas, i és el que es cancel·la quan tot torna a estar verd.

Conclusió

DA-001 està complerta. El catàleg d'AlpinaShop corre a Cloud Run, darrere de la mateixa IP, el mateix domini, el mateix certificat, la mateixa CDN i el mateix WAF; el MIG està apagat; i no queda una sola màquina virtual a la ruta de servei de la botiga.

Saps què és Cloud Run —contenidors sense servidor— i per què la unitat de desplegament ho canvia tot: el contracte són sis regles (escoltar a $PORT i a 0.0.0.0, parlar HTTP, arrencar ràpid, no desar estat, no fer res entre peticions, ser un contenidor Linux sense privilegis) i complir-les fa que la mateixa imatge corri a Cloud Run, a Kubernetes o al teu portàtil. Distingeixes servei, revisió i instància, i saps que les revisions són immutables — que és el que fa possible tota la resta.

Tens el criteri entre serveis i treballs: si respon a algú, servei; si fa alguna cosa i acaba, treball. Amb l'antipatró identificat: l'informe nocturn com a endpoint HTTP disparat per Scheduler, que hereta quatre problemes que un job no té.

Has desplegat el catàleg amb gcloud run deploy bandera a bandera, sabent què fa cadascuna i per què té aquell valor, i ho has escrit en Terraform amb google_cloud_run_v2_service, amb cpu_idle, startup_cpu_boost i ingress restringit al balancejador, i amb la frontera clara de què gestiona Terraform (la forma) i què gestiona el pipeline (la imatge i el trànsit).

Domines les revisions i el repartiment de trànsit: l'etiqueta que dona una URL pròpia per provar sense arriscar, la progressió 0 → 10 → 50 → 100 amb el que s'observa a cada esglaó, i la reversió instantània que triga segons perquè la revisió anterior mai no va deixar d'existir. Amb les dues advertències honestes: el canari no protegeix de canvis d'esquema, i amb trànsit baix el 10 % no demostra res.

Entens la concurrència millor que la majoria: què és, per què el model d'una petició per instància és dotze vegades més car per a una càrrega dominada per E/S, com es mesura amb una prova de càrrega real, i —el més important— que el sostre no el posa Cloud Run sinó el teu servidor d'aplicacions. Un --concurrency=80 sobre un Gunicorn d'un worker síncron, o un async def que crida una biblioteca bloquejant, converteixen la concurrència en una cua invisible.

Saps governar l'escalat: l'escalat a zero i el seu preu en arrencades en fred, --min-instances que les mitiga però no les elimina, --max-instances com a tallafoc de cost i de connexions a la base de dades amb la seva fórmula, i l'elecció entre CPU durant la petició i CPU sempre activa, amb el parany dels fils en segon pla que no s'executen.

Has connectat el servei amb tota la plataforma: Cloud SQL per connector amb un pool petit i pool_pre_ping, Secret Manager muntat com a variable o com a fitxer amb la diferència pràctica en la rotació, VPC directa —que substitueix el vell connector— amb els seus dos modes d'egress, Pub/Sub push amb OIDC sense ni un sol secret compartit, i Eventarc, amb la revelació que Cloud Functions 2a generació és Cloud Run per sota.

Has assegurat el servei amb les quatre decisions: compte de servei propi i mínim —evitant la fallada més comuna, heretar el compte per defecte amb Editor—, --no-allow-unauthenticated, ingress restringit al balancejador perquè ningú no pugui saltar-se el WAF per la URL run.app, i IAP per al que és intern.

Has fet la migració sense tall: preparació amb llista de comprovació, desplegament sense trànsit, NEG sense servidor afegit al balancejador existent, transvasament progressiu amb capacity-scaler vigilant quatre mètriques, tres dies de convivència amb reversió d'una comanda, i retirada en dos passos amb resize 0 abans de l'esborrat des de Terraform. I coneixes les quatre particularitats del NEG sense servidor: sense comprovacions d'estat, sense capacitat, regional encara que el balancejador sigui global, i amb la CDN funcionant igual.

Tens el cost desglossat: vCPU-segon, GiB-segon i peticions, amb nivell gratuït, i el càlcul comparat que baixa el còmput del catàleg d'uns 50 € a uns 7 € al mes — amb l'honestedat d'assenyalar que amb trànsit constant alt el MIG amb compromís guanyaria, que en campanya la factura puja perquè es paga l'ús, i que ara el balancejador és el 74 % del que costa la botiga. Coneixes els límits i els set escenaris on Cloud Run no encaixa. I t'emportes la taula madura amb les quatre preguntes que resolen gairebé qualsevol decisió de còmput.

Finalment, has vist el que desapareix en apagar el MIG: deu elements —plantilla, script d'arrencada, apedaçament, agent, autoescalat, actualitzacions progressives, claus SSH, discos, instantànies, reserva de campanya— que ja no cal mantenir, entendre ni explicar. Per a un equip d'una persona, això val més que l'estalvi.

I tanmateix, dos apartats d'aquesta lliçó han deixat un serrell que apunta al mateix lloc. La connexió a Cloud SQL seria millor per IP privada a través de la VPC. Sortir cap a la passarel·la de pagament amb IP fixa exigeix passar pel Cloud NAT. I l'ERP de Sabadell, que DA-004 va deixar a l'altra banda d'un túnel que encara no existeix, necessita que Cloud Run pugui abastar-lo per la xarxa privada.

Els tres són el mateix problema: la xarxa d'AlpinaShop es va quedar en el bàsic al mòdul 3 i ja no basta. Una sola VPC en un sol projecte, tres projectes que s'ignoren entre si, cap connectivitat amb la botiga física i cap perímetre que impedeixi que les dades d'alpinashop-datos surtin per la porta. La lliçó següent ho resol: VPC compartida, aparellament, Private Service Connect, Cloud VPN HA cap a Sabadell i controls de servei de VPC.

Curs de Google Cloud Platform (GCP)

Mòdul 1: Introducció a Google Cloud Platform

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats