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
- Què és un PaaS i què desapareix de la teva feina
- Entorn estàndard i entorn flexible
- Anatomia d'
app.yaml - Desplegar el catàleg d'AlpinaShop
- Versions, divisió de trànsit i desplegament canary
- Tipus d'escalat i el seu efecte en el cost
- Serveis i encaminament amb
dispatch.yaml - Accés a Cloud SQL i Cloud Storage des d'App Engine
- Variables d'entorn i configuració
- Registres i errors
- Tasques en segon pla: Cloud Tasks i
cron.yaml - Limitacions reals i per què avui molts equips trien Cloud Run
- 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 | ||
| Sistema operatiu i pedaços | Tu | |
| Runtime (Python, dependències del sistema) | Tu | |
| Servidor web i balanceig | Tu | |
| Certificat TLS i HTTPS | Tu | Google (automàtic) |
| Escalat i health checks | Tu (MIG + escalador automàtic) | |
| 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.
- 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.
- Anatomia d'
app.yaml
app.yamlapp.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: alwaysTres idees d'aquest fitxer que convé retenir:
- Els
handlersde 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: alwayshauria 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.mdEl .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.
- 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.
- 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=1La 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=cookiePer 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:
- 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 peticionsEl 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 picsI 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.
- Serveis i encaminament amb
dispatch.yaml
dispatch.yamlUna 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: 15mCada 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: defaultAvantatges 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).
- 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.
- 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-prodEl detall complet de Secret Manager, la rotació de secrets i el xifratge amb Cloud KMS és a la lliçó 03-06.
- 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.
- Tasques en segon pla: Cloud Tasks i
cron.yaml
cron.yamlUna 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=5import 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 "", 204Dos 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/MadridEls 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.
- 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.yamlicron.yamlno 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.yamlno 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.envo.git. - Oblidar
--no-promoteen provar. Enviaràs el 100 % del trànsit a una versió sense validar. - Utilitzar
--split-by=randomper 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: 0en producció sense mesurar. L'arrencada en fred se't menja la conversió. - Pools de connexions grans. Multiplicats per moltes instàncies, esgoten
max_connectionsde Cloud SQL. - Desar fitxers al sistema local. Només
/tmp, en memòria i efímer. - Secrets a
env_variablesd'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: alwaysa tots els handlers. - Consell: utilitza
--versionamb noms llegibles. - Consell: mostra
GAE_VERSIONa l'aplicació per verificar d'un cop d'ull quina versió serveix cada petició. - Consell: defineix
max_instancessempre. É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
- Crea l'aplicació d'App Engine a
europe-west1sobrealpinashop-dev. - Escriu una aplicació Flask mínima que mostri "AlpinaShop" i la versió (
GAE_VERSION), amb una ruta/salud. - Escriu un
app.yamlamb runtimepython312, classeF1, escalat automàtic de 0 a 4 instàncies i un handler estàtic per a/estatico. - Desplega com a versió
v1i comprova la URL. - Explica què canviaries a
app.yamlper a producció i justifica el cost associat.
Exercici 2: canary i rollback
- Modifica l'aplicació per mostrar els preus amb IVA i desplega-la com a
v2sense enviar-li trànsit. - Comprova
v2a la seva URL específica. - Envia el 20 % del trànsit a
v2amb repartiment per cookie. - Comprova el repartiment fent diverses peticions i observant la versió mostrada.
- Simula una fallada i executa un rollback complet a
v1. Després neteja les versions sense trànsit.
Exercici 3: serveis, cron i tasques
- Crea un segon servei
apiamb el seu propiapp.yamli escalat propi, que exposi/api/pedidos. - Escriu un
dispatch.yamlque enviï/api/*al serveiapii la resta aldefault. - Afegeix un
cron.yamlamb una tasca diària a les 3:00 (hora de Madrid) que cridi/tareas/exportar-pedidosal serveiapi. - Protegeix l'endpoint de cron comprovant la capçalera corresponent.
- 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
# 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: alwaysPer 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 --quietAmb --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- 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
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
