Fins ara, la migració d'AlpinaShop ha seguit el camí més conservador: moure el que hi havia a màquines virtuals. Funciona, però la Marta continua mantenint sistemes operatius, escrivint startup scripts, versionant plantilles d'instància i vigilant pedaços de seguretat. És feina que no distingeix AlpinaShop de cap altra botiga: ningú no compra més motxilles perquè el seu servidor estigui ben apedaçat.

App Engine va ser el primer servei de Google Cloud, llançat el 2008, i continua sent una de les maneres més ràpides de posar una aplicació web en producció: puges el codi, Google s'encarrega de tota la resta —servidors, escalat, balanceig, certificats TLS, desplegaments— i pagues per l'ús. En aquesta lliçó desplegaràs el catàleg Flask d'AlpinaShop a App Engine, aprendràs a controlar-ne l'escalat i el cost, dividiràs trànsit entre versions per fer un desplegament canary, connectaràs amb Cloud SQL i Cloud Storage, i acabaràs amb una valoració honesta de per què avui molts equips trien Cloud Run en el seu lloc.

Contingut

  1. Què és un PaaS i què desapareix de la teva feina
  2. Entorn estàndard i entorn flexible
  3. Anatomia d'app.yaml
  4. Desplegar el catàleg d'AlpinaShop
  5. Versions, divisió de trànsit i desplegament canary
  6. Tipus d'escalat i el seu efecte en el cost
  7. Serveis i encaminament amb dispatch.yaml
  8. Accés a Cloud SQL i Cloud Storage des d'App Engine
  9. Variables d'entorn i configuració
  10. Registres i errors
  11. Tasques en segon pla: Cloud Tasks i cron.yaml
  12. Limitacions reals i per què avui molts equips trien Cloud Run

  1. Què és un PaaS i què desapareix de la teva feina

Plataforma com a servei significa que el proveïdor gestiona tot el que hi ha per sota del teu codi. Comparat amb el que vam fer a 02-01:

Responsabilitat Compute Engine (IaaS) App Engine (PaaS)
Maquinari i xarxa física Google Google
Sistema operatiu i pedaços Tu Google
Runtime (Python, dependències del sistema) Tu Google
Servidor web i balanceig Tu Google
Certificat TLS i HTTPS Tu Google (automàtic)
Escalat i health checks Tu (MIG + escalador automàtic) Google
Desplegament i rollback Tu (plantilles + rolling update) Google (una comanda)
Codi de l'aplicació Tu Tu
Esquema de dades i consultes Tu Tu

Tota la lliçó 02-01 —plantilles, grups gestionats, health checks, escalat automàtic, actualitzacions progressives— es redueix a App Engine a un fitxer de configuració i gcloud app deploy. I surt de franc HTTPS amb domini propi, cosa que a Compute Engine exigirà un balancejador de càrrega i un certificat gestionat (03-02 i 03-07).

El que pagues per això: menys control i més acoblament. No tries el sistema operatiu, no instal·les paquets arbitraris a l'estàndard, i l'app.yaml és un format propi de Google que no funciona enlloc més.

Un detall organitzatiu que sorprèn: una aplicació d'App Engine per projecte, i la seva regió es tria en crear l'aplicació i no es pot canviar. Si t'equivoques, cal crear un projecte nou. Per a AlpinaShop, europe-west1, coherent amb tota la resta.

gcloud app create --region=europe-west1 --project=alpinashop-dev

  1. Entorn estàndard i entorn flexible

App Engine té dos entorns que es comporten de manera molt diferent sota el mateix nom:

Estàndard Flexible
Base d'execució Sandbox gestionat de Google VM de Compute Engine amb Docker
Llenguatges Python, Java, Node.js, Go, PHP, Ruby (versions suportades) Qualsevol, mitjançant contenidor propi
Temps d'arrencada Segons Minuts
Escalat a zero Sí No (mínim 1 instància)
Cost sense trànsit Zero (o gairebé) El d'almenys una VM 24×7
Accés al sistema de fitxers Només /tmp, en memòria Disc de la VM, escriptura permesa
Executar binaris propis No Sí
SSH a la instància No Sí
Durada màxima d'una petició 10 min (escalat automàtic) / 24 h (bàsic i manual) 60 min
Desplegament Segons a un parell de minuts Diversos minuts (construeix imatge)
Nivell gratuït Sí, quota diària d'hores d'instància No

Quan cadascun. L'estàndard és l'elecció natural per a aplicacions web en un llenguatge suportat: arrenca ràpid, escala a zero i és barat. El flexible té sentit si necessites una dependència del sistema que el sandbox no permet, un binari propi o un llenguatge no suportat.

I aquí la valoració honesta que anticipa l'apartat 12: si estàs pensant en App Engine flexible, gairebé sempre Cloud Run és millor opció avui —també executa el teu contenidor, però escala a zero, es factura per petició i no està lligat al format d'App Engine.

AlpinaShop utilitza l'entorn estàndard amb Python 3.12: el catàleg és Flask pur amb dependències que s'instal·len amb pip, sense res exòtic.

  1. Anatomia d'app.yaml

app.yaml és el fitxer que descriu l'aplicació sencera. Construirem el d'AlpinaShop comentant cada bloc.

# Runtime gestionat: Google mante la imatge base, l'interpret i els pedacos.
runtime: python312

# Comanda que arrenca l'aplicacio. Sense aixo, App Engine busca per convencio
# un objecte 'app' a main.py, pero conve ser explicit.
# 'gunicorn' es el servidor WSGI; App Engine exposa l'app a $PORT.
entrypoint: gunicorn -b :$PORT -w 2 --timeout 60 main:app

# Classe d'instancia: defineix CPU i memoria de cada instancia.
#   F1 = 256 MB / 600 MHz  (mes barata, la del nivell gratuit)
#   F2 = 512 MB / 1,2 GHz
#   F4 = 1 GB   / 2,4 GHz
#   F4_1G = 2 GB / 2,4 GHz
instance_class: F2

# Politica d'escalat: veure apartat 6.
automatic_scaling:
  min_instances: 0            # escala a zero quan no hi ha transit
  max_instances: 10           # sostre de seguretat per a la factura
  target_cpu_utilization: 0.6 # crea instancies en superar el 60 % de CPU
  target_throughput_utilization: 0.6
  max_concurrent_requests: 20 # peticions simultanies per instancia
  min_pending_latency: 100ms  # espera abans de decidir crear instancia
  max_pending_latency: 500ms  # si una peticio espera mes, crea instancia ja

# Variables d'entorn NO sensibles. Els secrets, a Secret Manager (03-06).
env_variables:
  BUCKET_CATALOGO: "alpinashop-catalogo"
  INSTANCIA_SQL: "alpinashop-prod:europe-west1:alpinashop-pedidos"
  DB_USER: "app_catalogo"
  DB_NAME: "tienda"
  ENTORNO: "produccion"

# Connector per accedir a Cloud SQL per IP privada o a recursos de la VPC (03-01).
vpc_access_connector:
  name: projects/alpinashop-prod/locations/europe-west1/connectors/conector-vpc

# Encaminament de peticions, en ordre. La primera coincidencia guanya.
handlers:
  # Recursos estatics servits directament per la infraestructura de Google,
  # sense consumir instancies de l'aplicacio ni temps de CPU.
  - url: /estatico
    static_dir: estatico
    secure: always            # forca HTTPS
    expiration: "30d"         # capcalera Cache-Control de 30 dies

  - url: /favicon\.ico
    static_files: estatico/favicon.ico
    upload: estatico/favicon\.ico

  # Tauler intern: nomes usuaris autenticats amb compte de l'organitzacio.
  - url: /admin/.*
    script: auto
    secure: always
    login: admin

  # Tota la resta va a l'aplicacio.
  - url: /.*
    script: auto
    secure: always

Tres idees d'aquest fitxer que convé retenir:

  • Els handlers de tipus estàtic són gratuïts en temps de CPU. Google serveix aquests fitxers des de la seva pròpia infraestructura, sense arrencar ni ocupar una instància. Servir CSS, JS i imatges per allà en lloc de fer-ho amb Flask redueix el cost i la latència de manera notable.
  • secure: always hauria de ser a tots els handlers. Redirigeix HTTP a HTTPS automàticament.
  • max_concurrent_requests és la palanca més important del cost. Si la teva aplicació passa la major part del temps esperant la base de dades, pot atendre moltes peticions simultànies per instància; pujar aquest valor redueix el nombre d'instàncies necessàries. Si l'aplicació consumeix CPU, pujar-lo empitjora la latència.

Al costat de l'app.yaml hi van els fitxers habituals d'un projecte Python:

alpinashop-catalogo/
├── app.yaml
├── main.py
├── requirements.txt
├── .gcloudignore
└── estatico/
    ├── estilo.css
    └── favicon.ico
# requirements.txt
Flask==3.0.*
gunicorn==22.0.*
SQLAlchemy==2.0.*
cloud-sql-python-connector[pg8000]==1.*
google-cloud-storage==2.*
# .gcloudignore: el que NO es puja en el desplegament
.git/
.gitignore
__pycache__/
*.pyc
venv/
.env
tests/
README.md

El .gcloudignore no és un detall menor: sense ell, pujaries el directori .git i l'entorn virtual sencer, amb desplegaments lents i el risc real de publicar un fitxer .env amb credencials.

  1. Desplegar el catàleg d'AlpinaShop

L'aplicació, amb les peces que ja coneixem de les lliçons anteriors:

# main.py
import os
import sqlalchemy
from flask import Flask, jsonify, render_template_string
from google.cloud.sql.connector import Connector, IPTypes
from google.cloud import storage

app = Flask(__name__)

connector = Connector()
client_storage = storage.Client()
bucket = client_storage.bucket(os.environ["BUCKET_CATALOGO"])


def _connectar_bd():
    return connector.connect(
        os.environ["INSTANCIA_SQL"],
        "pg8000",
        user=os.environ["DB_USER"],
        password=os.environ["DB_PASS"],
        db=os.environ["DB_NAME"],
        ip_type=IPTypes.PUBLIC,
    )


engine = sqlalchemy.create_engine(
    "postgresql+pg8000://",
    creator=_connectar_bd,
    pool_size=2,          # baix a proposit: App Engine crea MOLTES instancies
    max_overflow=1,
    pool_recycle=1800,
    pool_pre_ping=True,
)


@app.route("/")
def cataleg():
    consulta = sqlalchemy.text(
        "SELECT sku, nombre, precio FROM tienda.productos "
        "WHERE activo = true ORDER BY nombre LIMIT 50"
    )
    with engine.connect() as conn:
        productes = conn.execute(consulta).mappings().all()

    files = "".join(
        f"<li>{p['sku']} — {p['nombre']} — {p['precio']:.2f} €</li>" for p in productes
    )
    versio = os.environ.get("GAE_VERSION", "local")
    return render_template_string(
        f"<h1>AlpinaShop</h1><ul>{files}</ul><p>Versio: {versio}</p>"
    )


@app.route("/salud")
def salut():
    return "ok", 200


if __name__ == "__main__":
    # Nomes per a desenvolupament local; a App Engine arrenca gunicorn.
    app.run(host="127.0.0.1", port=8080, debug=True)

Fixa't en pool_size=2. És un canvi deliberat respecte de la lliçó anterior: App Engine pot crear desenes d'instàncies en un pic, i cadascuna obriria el seu propi pool. Amb pool_size=5 i 20 instàncies serien 100 connexions només del catàleg. En entorns que escalen de manera agressiva, els pools han de ser petits.

Observa també GAE_VERSION: App Engine injecta variables d'entorn amb informació de l'entorn d'execució (GAE_SERVICE, GAE_VERSION, GAE_INSTANCE, GOOGLE_CLOUD_PROJECT). Mostrar la versió a la pàgina és un truc molt pràctic per verificar desplegaments i divisions de trànsit.

Desplegament:

# Provar en local abans de desplegar
export BUCKET_CATALOGO=alpinashop-catalogo
export INSTANCIA_SQL=alpinashop-prod:europe-west1:alpinashop-pedidos
export DB_USER=app_catalogo DB_PASS=... DB_NAME=tienda
python main.py

# Desplegar amb un identificador de versio explicit
gcloud app deploy app.yaml \
  --version=v1-catalogo \
  --project=alpinashop-dev

# Obrir al navegador
gcloud app browse

# Veure la URL assignada
gcloud app describe --format="value(defaultHostname)"

En desplegar, App Engine construeix el paquet, instal·la les dependències de requirements.txt, crea una versió nova i —tret que diguis el contrari— li envia el 100 % del trànsit. La URL per defecte és https://<projectId>.<region>.r.appspot.com, amb certificat TLS vàlid des del primer segon i sense haver configurat res.

Utilitza sempre --version amb un nom significatiu. Sense ell, App Engine genera un identificador amb marca temporal, difícil de llegir a la llista de versions i a les comandes de repartiment de trànsit.

  1. Versions, divisió de trànsit i desplegament canary

Cada desplegament crea una versió immutable que continua disponible. Això habilita una cosa molt potent: repartir el trànsit entre versions.

# Desplegar sense enviar transit: la versio queda disponible pero inactiva
gcloud app deploy app.yaml --version=v2-precios --no-promote

# Provar-la a la seva URL propia, sense afectar els clients
gcloud app browse --version=v2-precios
# https://v2-precios-dot-alpinashop-dev.ew.r.appspot.com

# Canary: 10 % del transit a la versio nova
gcloud app services set-traffic default \
  --splits=v1-catalogo=0.9,v2-precios=0.1

# Si tot va be, augmentar progressivament
gcloud app services set-traffic default \
  --splits=v1-catalogo=0.5,v2-precios=0.5

# Promocio completa
gcloud app services set-traffic default --splits=v2-precios=1

# Rollback immediat si alguna cosa falla
gcloud app services set-traffic default --splits=v1-catalogo=1

La URL amb el prefix <versio>-dot-<servei>-dot-<projecte> és un detall molt útil: qualsevol versió desplegada és accessible directament, cosa que permet validar a l'entorn real abans de dirigir-hi usuaris.

Sobre el criteri de repartiment, hi ha dos modes i la diferència importa:

# Repartiment aleatori per peticio: cada peticio pot anar a una versio diferent
gcloud app services set-traffic default --splits=v1=0.9,v2=0.1 --split-by=random

# Repartiment per cookie: un mateix usuari veu sempre la mateixa versio
gcloud app services set-traffic default --splits=v1=0.9,v2=0.1 --split-by=cookie

Per a un canary d'una interfície d'usuari, cookie és gairebé sempre el correcte: amb random, un client podria veure una pàgina a la versió antiga i la següent a la nova, amb resultats desconcertants.

I un advertiment de cost: les versions antigues amb instàncies mínimes configurades continuen facturant encara que no rebin trànsit. Neteja periòdicament:

gcloud app versions list --filter="traffic_split=0"
gcloud app versions delete v1-catalogo --quiet

  1. Tipus d'escalat i el seu efecte en el cost

App Engine estàndard ofereix tres modes d'escalat, i triar malament és la principal font de factures inesperades.

Automàtic Bàsic Manual
Crea instàncies segons Trànsit, CPU i latència Arribada de peticions Mai: nombre fix
Escala a zero Sí (si min_instances: 0) Sí, després d'inactivitat No
Arrencada en fred Sí Sí No
Durada màx. de petició 10 minuts 24 hores 24 hores
Ús típic Aplicacions web Tasques llargues i esporàdiques Serveis amb estat en memòria, càrrega constant
# Automatic: el cas general
automatic_scaling:
  min_instances: 0
  max_instances: 10
  min_idle_instances: 0        # instancies "calentes" en reserva
  max_concurrent_requests: 20
  target_cpu_utilization: 0.6
# Basic: per a un servei de processament per lots
basic_scaling:
  max_instances: 3
  idle_timeout: 10m            # apaga la instancia despres de 10 min sense peticions
# Manual: nombre fix d'instancies, sempre enceses
manual_scaling:
  instances: 2

El problema de l'arrencada en fred. Amb min_instances: 0 no pagues res quan no hi ha trànsit, però la primera petició després d'un període d'inactivitat ha d'esperar que arrenqui una instància: carregar l'intèrpret, importar les dependències, obrir el pool de connexions. En Python amb Flask solen ser 1–3 segons; si el codi importa biblioteques pesades, més.

La solució és reservar instàncies calentes:

automatic_scaling:
  min_instances: 1        # sempre 1 instancia viva
  min_idle_instances: 1   # a mes, 1 de reserva llesta per a pics

I aquí hi ha el compromís, amb números orientatius:

Configuració Cost aproximat sense trànsit Latència de la primera petició
min_instances: 0 ~0 € 1–3 s (arrencada en fred)
min_instances: 1, classe F2 Una instància F2 24×7: de l'ordre de 30–40 €/mes Immediata
min_instances: 2 El doble Immediata, amb redundància

Xifres orientatives per raonar sobre l'ordre de magnitud. Consulta la calculadora oficial de Google Cloud.

Decisió d'AlpinaShop, coherent amb la seva realitat: a alpinashop-dev, min_instances: 0 —al Dani no li importa esperar dos segons i l'entorn passa la nit sense trànsit. En producció, min_instances: 1 i max_instances: 10, perquè un client que veu la botiga trigar tres segons a carregar és un client que se'n va. Durant la campanya de tardor es puja el mínim a 3 i el màxim a 20.

Consells addicionals per reduir arrencades en fred: importa les biblioteques pesades de manera mandrosa, no executis feina costosa a l'arrencada del mòdul, i mantén requirements.txt sense dependències que no facis servir.

  1. Serveis i encaminament amb dispatch.yaml

Una aplicació d'App Engine pot tenir diversos serveis (abans anomenats mòduls), cadascun amb el seu propi app.yaml, el seu escalat i les seves versions. És la manera de separar components amb perfils diferents.

Per a AlpinaShop:

alpinashop/
├── dispatch.yaml
├── web/          -> app.yaml amb "service: default"  (catàleg públic)
├── api/          -> app.yaml amb "service: api"      (API de comandes)
└── admin/        -> app.yaml amb "service: admin"    (tauler intern)
# api/app.yaml
runtime: python312
service: api
instance_class: F2
entrypoint: gunicorn -b :$PORT -w 2 main:app
automatic_scaling:
  min_instances: 1
  max_instances: 20
# admin/app.yaml
runtime: python312
service: admin
instance_class: F1
entrypoint: gunicorn -b :$PORT -w 1 main:app
basic_scaling:            # el tauler s'utilitza poc: no val la pena tenir-lo calent
  max_instances: 2
  idle_timeout: 15m

Cada servei té la seva URL pròpia (https://api-dot-alpinashop-prod.ew.r.appspot.com). Per servir-los tots sota un mateix domini amb rutes diferents, s'utilitza dispatch.yaml:

dispatch:
  - url: "*/api/*"
    service: api

  - url: "*/admin/*"
    service: admin

  - url: "*/*"
    service: default
gcloud app deploy web/app.yaml api/app.yaml admin/app.yaml dispatch.yaml

Avantatges de separar serveis: cadascun escala segons la seva pròpia càrrega (el tauler d'administració no necessita 10 instàncies perquè hi hagi un pic a la botiga), es despleguen de manera independent, i una fallada al tauler no tomba el catàleg. És una separació de dominis de fallada, la mateixa idea que vam aplicar a 01-05 a les zones.

Tingues en compte que les regles de dispatch.yaml s'avaluen en ordre i només se n'admeten un nombre limitat; per a encaminament complex s'utilitza un balancejador de càrrega (03-02).

  1. Accés a Cloud SQL i Cloud Storage des d'App Engine

Aquí es veu el valor d'haver utilitzat biblioteques client a les lliçons anteriors: el codi no canvia.

Cada aplicació d'App Engine té un compte de servei per defecte, <projectId>@appspot.gserviceaccount.com. N'hi ha prou de donar-li els permisos adequats:

PROJECTE=alpinashop-prod
SA="[email protected]"

# Acces a Cloud SQL
gcloud projects add-iam-policy-binding $PROJECTE \
  --member="serviceAccount:$SA" --role="roles/cloudsql.client"

# Lectura i escriptura d'objectes al bucket del cataleg
gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
  --member="serviceAccount:$SA" --role="roles/storage.objectUser"

# Lectura del secret amb la contrasenya de la base de dades (03-06)
gcloud secrets add-iam-policy-binding db-password-catalogo \
  --member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"

Amb això, el mateix storage.Client() i el mateix connector de Cloud SQL de les lliçons 02-02 i 02-03 funcionen sense tocar ni una línia, perquè les Application Default Credentials resolen ara al compte de servei d'App Engine.

Una particularitat de l'entorn estàndard: el sistema de fitxers és de només lectura tret de /tmp, que a més viu en memòria i compta contra la memòria de la instància. No hi desis mai res que hagi de persistir ni fitxers grans. Les imatges que pugen els usuaris van directes a Cloud Storage, exactament com vam programar a 02-02 amb upload_from_file sobre el flux.

  1. Variables d'entorn i configuració

Les variables no sensibles van a app.yaml, sota env_variables. Les sensibles, no: app.yaml acaba al repositori de Git i els seus valors són visibles per a qualsevol que pugui veure la configuració de la versió.

El patró correcte és llegir els secrets a l'arrencada des de Secret Manager:

import os
from google.cloud import secretmanager


def llegir_secret(nom: str, versio: str = "latest") -> str:
    """Llegeix un secret de Secret Manager amb el compte de servei d'App Engine."""
    projecte = os.environ["GOOGLE_CLOUD_PROJECT"]
    client = secretmanager.SecretManagerServiceClient()
    ruta = f"projects/{projecte}/secrets/{nom}/versions/{versio}"
    resposta = client.access_secret_version(request={"name": ruta})
    return resposta.payload.data.decode("UTF-8")


# Es llegeix UNA vegada en arrencar la instancia, no en cada peticio:
# cada crida a Secret Manager te latencia i cost per operacio.
DB_PASS = llegir_secret("db-password-catalogo")

Per distingir entorns, l'habitual és tenir un app.yaml per entorn (app-dev.yaml, app-prod.yaml) i desplegar el que toqui:

gcloud app deploy app-dev.yaml  --project=alpinashop-dev
gcloud app deploy app-prod.yaml --project=alpinashop-prod

El detall complet de Secret Manager, la rotació de secrets i el xifratge amb Cloud KMS és a la lliçó 03-06.

  1. Registres i errors

App Engine envia automàticament a Cloud Logging les peticions i tot el que l'aplicació escrigui a la sortida estàndard. No cal configurar res.

# Seguir els registres en temps real
gcloud app logs tail -s default

# Ultimes 50 entrades d'una versio concreta
gcloud app logs read --version=v2-precios --limit=50

# Consulta avancada: errors 5xx de l'ultima hora
gcloud logging read \
  'resource.type="gae_app" AND httpRequest.status>=500' \
  --limit=20 --freshness=1h --format="value(timestamp, httpRequest.status, httpRequest.requestUrl)"

Perquè els registres siguin realment útils, escriu-los estructurats en JSON: Cloud Logging els indexa per camps i podràs filtrar per sku o per severitat en lloc de buscar cadenes.

import json
import logging
import sys

def log_estructurat(missatge, severitat="INFO", **camps):
    entrada = {"message": missatge, "severity": severitat, **camps}
    print(json.dumps(entrada), file=sys.stdout)

log_estructurat("producte consultat", sku="MOC-40", duracio_ms=42)
log_estructurat("estoc insuficient", severitat="WARNING", sku="BOT-GTX", sollicitat=5)

Els errors no capturats arriben automàticament a Error Reporting, que els agrupa per traça de pila, compta ocurrències i pot avisar quan apareix un error nou. És de les coses més rendibles que ofereix la plataforma sense configuració.

El tractament en profunditat de Cloud Logging, les mètriques i les traces és a 06-06 i 06-04.

  1. Tasques en segon pla: Cloud Tasks i cron.yaml

Una petició HTTP no ha de fer feina llarga: l'usuari espera, i en escalat automàtic hi ha un límit de 10 minuts. Quan AlpinaShop confirma una comanda, cal enviar un correu, actualitzar l'estoc i avisar el magatzem; res d'això no ha de bloquejar la resposta al client.

Cloud Tasks és una cua gestionada: encues una tasca, que consisteix en una petició HTTP diferida a la teva pròpia aplicació, i el servei la lliura amb reintents i control de ritme.

gcloud tasks queues create cola-pedidos \
  --location=europe-west1 \
  --max-dispatches-per-second=10 \
  --max-concurrent-dispatches=20 \
  --max-attempts=5
import json
from google.cloud import tasks_v2

client_tasques = tasks_v2.CloudTasksClient()
CUA = client_tasques.queue_path("alpinashop-prod", "europe-west1", "cola-pedidos")


def encuar_confirmacio(pedido_id: int):
    """Encua el processament posterior a la confirmacio d'una comanda."""
    tasca = {
        "app_engine_http_request": {
            "http_method": tasks_v2.HttpMethod.POST,
            "relative_uri": "/tareas/procesar-pedido",
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"pedido_id": pedido_id}).encode(),
        }
    }
    return client_tasques.create_task(request={"parent": CUA, "task": tasca})


@app.route("/tareas/procesar-pedido", methods=["POST"])
def processar_comanda():
    # App Engine garanteix que aquesta capcalera nomes la pot posar Cloud Tasks:
    # peticions externes amb aquesta capcalera son rebutjades per la infraestructura.
    if "X-AppEngine-TaskName" not in request.headers:
        return "prohibit", 403

    dades = request.get_json()
    # ... enviar correu, actualitzar estoc, notificar el magatzem ...

    # Retornar 2xx marca la tasca com a completada.
    # Qualsevol altre codi provoca reintent amb retroces exponencial,
    # aixi que el manejador HA DE ser idempotent.
    return "", 204

Dos punts crítics aquí: la comprovació de la capçalera X-AppEngine-TaskName com a control d'accés, i la idempotència. Cloud Tasks garanteix lliurament almenys una vegada, no exactament una vegada: si el teu manejador triga i hi ha un reintent, podries enviar dos correus o descomptar l'estoc dues vegades. Registra quines comandes ja has processat i comprova-ho abans d'actuar.

Cron. Per a tasques programades, cron.yaml invoca una URL de la teva aplicació segons un horari:

cron:
  - description: "Exportacio diaria de comandes per a analitica"
    url: /tareas/exportar-pedidos
    schedule: every day 03:00
    timezone: Europe/Madrid
    target: api

  - description: "Recordatori de cistelles abandonades"
    url: /tareas/carritos-abandonados
    schedule: every 6 hours

  - description: "Informe setmanal de vendes per a la Lucia"
    url: /tareas/informe-semanal
    schedule: every monday 07:00
    timezone: Europe/Madrid
gcloud app deploy cron.yaml
gcloud app describe --format="value(name)"   # verificar el desplegament

Els endpoints invocats per cron reben la capçalera X-Appengine-Cron: true, que es comprova igual que la de Tasks. Protegeix aquestes rutes també amb un handler login: admin a app.yaml.

El servei equivalent fora d'App Engine és Cloud Scheduler, que pot invocar HTTP, Pub/Sub o Cloud Functions i funciona amb qualsevol servei de còmput. És l'opció a utilitzar si la teva aplicació no viu a App Engine.

  1. Limitacions reals i per què avui molts equips trien Cloud Run

App Engine continua sent un bon producte i hi ha aplicacions que fa més d'una dècada que hi funcionen sense sobresalts. Però convé ser sincer sobre els seus límits el 2026:

  • Acoblament al format de Google. app.yaml, dispatch.yaml i cron.yaml no existeixen fora d'App Engine. Migrar a un altre proveïdor —o fins i tot a un altre servei de Google— implica refer la configuració.
  • Runtimes limitats i amb calendari propi. Només els llenguatges i versions que Google suporta; quan una versió de Python queda obsoleta, cal migrar en el termini que marqui Google.
  • Sandbox restrictiu a l'entorn estàndard. Res de binaris propis, dependències del sistema o escriptura al disc.
  • L'entorn flexible ha quedat desplaçat. Arrenca en minuts, no escala a zero i costa més que les alternatives basades en contenidors.
  • Regió immutable i una aplicació per projecte. Rígid per a arquitectures que evolucionen.
  • La inversió no s'acumula. El temps dedicat a dominar app.yaml no serveix en cap altre lloc; el temps dedicat a dominar contenidors serveix a tot arreu.

Cloud Run (lliçó 07-02) ocupa avui aquest espai: executa el teu contenidor, escala a zero, es factura per ús, ofereix divisió de trànsit entre revisions igual que App Engine, HTTPS automàtic, i el que desplegues és una imatge de contenidor estàndard que també funcionaria a GKE, al teu portàtil o en un altre núvol.

App Engine estàndard Cloud Run
Unitat de desplegament Codi + app.yaml Imatge de contenidor
Llenguatges Els suportats Qualsevol
Escalat a zero Sí Sí
Divisió de trànsit Sí Sí (revisions)
Portabilitat Baixa Alta
Configuració Fitxer propi de Google Contenidor estàndard + flags
Dependències del sistema Limitades Les que posis al Dockerfile

Quan continua tenint sentit App Engine: ja el fas servir i funciona; vols el camí més curt des de codi Python a una URL amb HTTPS; no tens contenidors ni ganes d'aprendre'ls ara mateix; o necessites específicament dispatch.yaml i el cron integrat.

Quan triar Cloud Run: projecte nou, ja treballes amb contenidors, vols portabilitat, o necessites control sobre l'entorn d'execució.

Per a AlpinaShop, la decisió que documentarem a 02-07 és començar per Compute Engine (lift-and-shift, ja fet) i evolucionar cap a contenidors. App Engine es queda com una opció coneguda i descartada de manera raonada, que és la millor manera de descartar alguna cosa.

Errors habituals i consells

  • Crear l'aplicació a la regió equivocada. És irreversible: exigeix projecte nou. Verifica-ho abans de gcloud app create.
  • Desplegar sense .gcloudignore. Desplegaments lents i risc de pujar .env o .git.
  • Oblidar --no-promote en provar. Enviaràs el 100 % del trànsit a una versió sense validar.
  • Utilitzar --split-by=random per a un canary d'interfície. L'usuari veu versions diferents en peticions consecutives.
  • Deixar versions antigues amb min_instances > 0. Facturen sense rebre trànsit.
  • Posar min_instances: 0 en producció sense mesurar. L'arrencada en fred se't menja la conversió.
  • Pools de connexions grans. Multiplicats per moltes instàncies, esgoten max_connections de Cloud SQL.
  • Desar fitxers al sistema local. Només /tmp, en memòria i efímer.
  • Secrets a env_variables d'app.yaml. Acaben a Git. Utilitza Secret Manager.
  • Manejadors de Cloud Tasks no idempotents. El lliurament és almenys una vegada.
  • Servir estàtics des de Flask. Utilitza handlers static_dir: són més ràpids i no consumeixen instàncies.
  • Consell: posa secure: always a tots els handlers.
  • Consell: utilitza --version amb noms llegibles.
  • Consell: mostra GAE_VERSION a l'aplicació per verificar d'un cop d'ull quina versió serveix cada petició.
  • Consell: defineix max_instances sempre. És el teu fre d'emergència davant d'un pic anòmal o un atac.

Exercicis

Exercici 1: primer desplegament i control de l'escalat

  1. Crea l'aplicació d'App Engine a europe-west1 sobre alpinashop-dev.
  2. Escriu una aplicació Flask mínima que mostri "AlpinaShop" i la versió (GAE_VERSION), amb una ruta /salud.
  3. Escriu un app.yaml amb runtime python312, classe F1, escalat automàtic de 0 a 4 instàncies i un handler estàtic per a /estatico.
  4. Desplega com a versió v1 i comprova la URL.
  5. Explica què canviaries a app.yaml per a producció i justifica el cost associat.

Exercici 2: canary i rollback

  1. Modifica l'aplicació per mostrar els preus amb IVA i desplega-la com a v2 sense enviar-li trànsit.
  2. Comprova v2 a la seva URL específica.
  3. Envia el 20 % del trànsit a v2 amb repartiment per cookie.
  4. Comprova el repartiment fent diverses peticions i observant la versió mostrada.
  5. Simula una fallada i executa un rollback complet a v1. Després neteja les versions sense trànsit.

Exercici 3: serveis, cron i tasques

  1. Crea un segon servei api amb el seu propi app.yaml i escalat propi, que exposi /api/pedidos.
  2. Escriu un dispatch.yaml que enviï /api/* al servei api i la resta al default.
  3. Afegeix un cron.yaml amb una tasca diària a les 3:00 (hora de Madrid) que cridi /tareas/exportar-pedidos al servei api.
  4. Protegeix l'endpoint de cron comprovant la capçalera corresponent.
  5. Explica per què el manejador d'una tasca ha de ser idempotent i com ho garantiries per a l'enviament del correu de confirmació d'una comanda.

Solucions

Solució 1

gcloud app create --region=europe-west1 --project=alpinashop-dev
# main.py
import os
from flask import Flask

app = Flask(__name__)


@app.route("/")
def inici():
    versio = os.environ.get("GAE_VERSION", "local")
    instancia = os.environ.get("GAE_INSTANCE", "local")[:8]
    return f"<h1>AlpinaShop</h1><p>Versio: {versio}</p><p>Instancia: {instancia}</p>"


@app.route("/salud")
def salut():
    return "ok", 200
# app.yaml
runtime: python312
entrypoint: gunicorn -b :$PORT -w 2 main:app
instance_class: F1

automatic_scaling:
  min_instances: 0
  max_instances: 4
  max_concurrent_requests: 20
  target_cpu_utilization: 0.6

handlers:
  - url: /estatico
    static_dir: estatico
    secure: always
    expiration: "7d"
  - url: /.*
    script: auto
    secure: always
gcloud app deploy app.yaml --version=v1 --project=alpinashop-dev
gcloud app browse

Per a producció canviaria instance_class: F2 (més memòria per al pool de connexions i les biblioteques client), min_instances: 1 per eliminar l'arrencada en fred del primer client, i max_instances: 10 per absorbir els pics de tardor. El cost afegit és el de mantenir una instància F2 encesa les 24 hores, de l'ordre de 30–40 € al mes: una xifra petita davant del valor que la portada de la botiga carregui sempre en menys d'un segon. Convé verificar l'import a la calculadora oficial abans de comprometre's.

Solució 2

# 1. Desplegar sense promoure
gcloud app deploy app.yaml --version=v2 --no-promote

# 2. Provar la versio concreta
gcloud app browse --version=v2
# o directament:
curl -s "https://v2-dot-alpinashop-dev.ew.r.appspot.com/"

# 3. Canary del 20 % amb afinitat per cookie
gcloud app services set-traffic default \
  --splits=v1=0.8,v2=0.2 --split-by=cookie

# 4. Comprovar el repartiment (amb -c/-b es conserva la cookie entre peticions)
for i in $(seq 1 10); do
  curl -s "https://alpinashop-dev.ew.r.appspot.com/" | grep -o "Versio: v[0-9]"
done

# 5. Rollback i neteja
gcloud app services set-traffic default --splits=v1=1
gcloud app versions list --filter="traffic_split=0"
gcloud app versions delete v2 --quiet

Amb --split-by=cookie, si mantens la cookie entre peticions veuràs sempre la mateixa versió: és el comportament desitjat per no desconcertar l'usuari. Sense cookie —com al bucle de curl anterior, on cada crida és una sessió nova— el repartiment es comporta com a aleatori, i al voltant de 2 de cada 10 respostes mostraran v2.

Solució 3

# api/app.yaml
runtime: python312
service: api
instance_class: F2
entrypoint: gunicorn -b :$PORT -w 2 main:app
automatic_scaling:
  min_instances: 0
  max_instances: 5
handlers:
  - url: /tareas/.*
    script: auto
    secure: always
    login: admin        # bloqueja l'acces extern als endpoints de tasques
  - url: /.*
    script: auto
    secure: always
# dispatch.yaml
dispatch:
  - url: "*/api/*"
    service: api
  - url: "*/tareas/*"
    service: api
  - url: "*/*"
    service: default
# cron.yaml
cron:
  - description: "Exportacio diaria de comandes"
    url: /tareas/exportar-pedidos
    schedule: every day 03:00
    timezone: Europe/Madrid
    target: api
# 4. Proteccio de l'endpoint de cron
from flask import request

@app.route("/tareas/exportar-pedidos")
def exportar_comandes():
    if request.headers.get("X-Appengine-Cron") != "true":
        return "prohibit", 403
    # ... exportar a gs://alpinashop-catalogo/exportaciones/<data>/ ...
    return "", 204
gcloud app deploy web/app.yaml api/app.yaml dispatch.yaml cron.yaml
  1. El manejador ha de ser idempotent perquè Cloud Tasks garanteix lliurament almenys una vegada: si el manejador triga més del previst, si retorna un error transitori o si la instància es recicla a mitges, la tasca es reintenta i la mateixa comanda arriba dues vegades. Sense protecció, el client rebria dos correus de confirmació i l'estoc es descomptaria dues vegades.

La manera de garantir-ho per al correu de confirmació és registrar el fet de manera transaccional a la base de dades:

CREATE TABLE tienda.correos_enviados (
    pedido_id  BIGINT PRIMARY KEY,
    tipo       TEXT NOT NULL,
    enviado_en TIMESTAMPTZ NOT NULL DEFAULT now()
);
with engine.begin() as conn:
    resultat = conn.execute(
        sqlalchemy.text(
            "INSERT INTO tienda.correos_enviados (pedido_id, tipo) "
            "VALUES (:pid, 'confirmacion') ON CONFLICT DO NOTHING"
        ),
        {"pid": pedido_id},
    )
    if resultat.rowcount == 0:
        return "", 204   # ja s'ha enviat: la tasca acaba sense repetir el correu

enviar_correu_confirmacio(pedido_id)

La clau és a ON CONFLICT DO NOTHING sobre una clau primària: la base de dades garanteix que només una execució aconsegueix inserir la fila, i les altres surten sense enviar res. Retornar 204 al segon intent evita a més reintents infinits.

Conclusió

Has vist què significa realment pujar un nivell d'abstracció. A App Engine desapareixen el sistema operatiu, els pedaços, el servidor web, el balanceig, els certificats TLS, els health checks i les plantilles d'instància: tot el que va ocupar la lliçó 02-01 es redueix a un app.yaml i un gcloud app deploy. Coneixes la diferència decisiva entre l'entorn estàndard —sandbox, arrencada en segons, escalat a zero, barat— i el flexible —contenidors, arrencada en minuts, sense escalat a zero—, i saps que avui el flexible ha quedat desplaçat per Cloud Run.

Has dissecat app.yaml línia a línia: runtime, entrypoint, classe d'instància, política d'escalat, variables d'entorn i handlers, inclosa la idea de servir els estàtics des de la infraestructura de Google per no gastar instàncies. Has desplegat el catàleg Flask d'AlpinaShop i has obtingut una URL amb HTTPS sense configurar res. Has après a desplegar amb --no-promote, validar a la URL de la versió, repartir trànsit amb afinitat per cookie per a un canary i fer rollback amb una sola comanda: pràctiques de desplegament que a Compute Engine exigien plantilles versionades i actualitzacions progressives. Has entès els tres modes d'escalat i el compromís central entre min_instances: 0 i l'arrencada en fred, amb la decisió raonada d'AlpinaShop: zero en desenvolupament, un en producció, tres en campanya. Has separat serveis amb dispatch.yaml perquè el tauler d'administració i el catàleg escalin per separat, has connectat Cloud SQL i Cloud Storage sense canviar ni una línia de codi gràcies a les credencials per defecte, has llegit secrets de Secret Manager a l'arrencada, has vist els registres estructurats i Error Reporting, i has mogut la feina pesada fora de la petició amb Cloud Tasks i cron.yaml, aprenent pel camí què significa que un lliurament sigui almenys una vegada i com escriure un manejador idempotent.

I has acabat amb una valoració honesta: App Engine és còmode però acoblat al format de Google, i la inversió que hi fas no s'acumula fora d'ell. L'alternativa que sí que s'acumula són els contenidors. A 02-05, Google Kubernetes Engine, farem aquest pas: repassarem Kubernetes des de zero —contenidor, pod, Deployment, Service, node i pla de control—, compararem els modes Autopilot i Standard, crearem el clúster alpinashop-cluster, construirem i publicarem la imatge del catàleg a Artifact Registry, escriurem els manifestos de Deployment i Service, escalarem amb HorizontalPodAutoscaler i farem actualitzacions sense tall amb el seu rollback. És el nivell d'abstracció amb més poder i també amb més responsabilitat de tot el mòdul.

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