Hasta ahora, la migración de AlpinaShop ha seguido el camino más conservador: mover lo que había a máquinas virtuales. Funciona, pero Marta sigue manteniendo sistemas operativos, escribiendo startup scripts, versionando plantillas de instancia y vigilando parches de seguridad. Es trabajo que no distingue a AlpinaShop de ninguna otra tienda: nadie compra más mochilas porque su servidor esté bien parcheado.
App Engine fue el primer servicio de Google Cloud, lanzado en 2008, y sigue siendo una de las formas más rápidas de poner una aplicación web en producción: subes el código, Google se encarga de todo lo demás —servidores, escalado, balanceo, certificados TLS, despliegues— y pagas por el uso. En esta lección desplegarás el catálogo Flask de AlpinaShop en App Engine, aprenderás a controlar su escalado y su coste, dividirás tráfico entre versiones para hacer un despliegue canary, conectarás con Cloud SQL y Cloud Storage, y terminarás con una valoración honesta de por qué hoy muchos equipos eligen Cloud Run en su lugar.
Contenido
- Qué es un PaaS y qué desaparece de tu trabajo
- Entorno estándar y entorno flexible
- Anatomía de
app.yaml - Desplegar el catálogo de AlpinaShop
- Versiones, división de tráfico y despliegue canary
- Tipos de escalado y su efecto en el coste
- Servicios y enrutamiento con
dispatch.yaml - Acceso a Cloud SQL y Cloud Storage desde App Engine
- Variables de entorno y configuración
- Logs y errores
- Tareas en segundo plano: Cloud Tasks y
cron.yaml - Limitaciones reales y por qué hoy muchos equipos eligen Cloud Run
- Qué es un PaaS y qué desaparece de tu trabajo
Plataforma como servicio significa que el proveedor gestiona todo lo que hay por debajo de tu código. Comparado con lo que hicimos en 02-01:
| Responsabilidad | Compute Engine (IaaS) | App Engine (PaaS) |
|---|---|---|
| Hardware y red física | ||
| Sistema operativo y parches | Tú | |
| Runtime (Python, dependencias del sistema) | Tú | |
| Servidor web y balanceo | Tú | |
| Certificado TLS y HTTPS | Tú | Google (automático) |
| Escalado y health checks | Tú (MIG + autoescalador) | |
| Despliegue y rollback | Tú (plantillas + rolling update) | Google (un comando) |
| Código de la aplicación | Tú | Tú |
| Esquema de datos y consultas | Tú | Tú |
Toda la lección 02-01 —plantillas, grupos gestionados, health checks, autoescalado, actualizaciones progresivas— se reduce en App Engine a un fichero de configuración y gcloud app deploy. Y sale gratis HTTPS con dominio propio, algo que en Compute Engine exigirá un balanceador de carga y un certificado gestionado (03-02 y 03-07).
Lo que pagas por ello: menos control y más acoplamiento. No eliges el sistema operativo, no instalas paquetes arbitrarios en el estándar, y el app.yaml es un formato propio de Google que no funciona en ninguna otra parte.
Un detalle organizativo que sorprende: una aplicación de App Engine por proyecto, y su región se elige al crear la aplicación y no se puede cambiar. Si te equivocas, hay que crear un proyecto nuevo. Para AlpinaShop, europe-west1, coherente con todo lo demás.
- Entorno estándar y entorno flexible
App Engine tiene dos entornos que se comportan de forma muy distinta bajo el mismo nombre:
| Estándar | Flexible | |
|---|---|---|
| Base de ejecución | Sandbox gestionado de Google | VM de Compute Engine con Docker |
| Lenguajes | Python, Java, Node.js, Go, PHP, Ruby (versiones soportadas) | Cualquiera, mediante contenedor propio |
| Tiempo de arranque | Segundos | Minutos |
| Escalado a cero | Sí | No (mínimo 1 instancia) |
| Coste sin tráfico | Cero (o casi) | El de al menos una VM 24×7 |
| Acceso al sistema de ficheros | Solo /tmp, en memoria |
Disco de la VM, escritura permitida |
| Ejecutar binarios propios | No | Sí |
| SSH a la instancia | No | Sí |
| Duración máxima de una petición | 10 min (escalado automático) / 24 h (básico y manual) | 60 min |
| Despliegue | Segundos a un par de minutos | Varios minutos (construye imagen) |
| Nivel gratuito | Sí, cuota diaria de horas de instancia | No |
Cuándo cada uno. El estándar es la elección natural para aplicaciones web en un lenguaje soportado: arranca rápido, escala a cero y es barato. El flexible tiene sentido si necesitas una dependencia del sistema que el sandbox no permite, un binario propio o un lenguaje no soportado.
Y aquí la valoración honesta que anticipa el apartado 12: si estás pensando en App Engine flexible, casi siempre Cloud Run es mejor opción hoy —también ejecuta tu contenedor, pero escala a cero, se factura por petición y no está atado al formato de App Engine.
AlpinaShop usa el entorno estándar con Python 3.12: el catálogo es Flask puro con dependencias que se instalan con pip, sin nada exótico.
- Anatomía de
app.yaml
app.yamlapp.yaml es el fichero que describe la aplicación entera. Vamos a construir el de AlpinaShop comentando cada bloque.
# Runtime gestionado: Google mantiene la imagen base, el interprete y los parches.
runtime: python312
# Comando que arranca la aplicacion. Sin esto, App Engine busca por convencion
# un objeto 'app' en main.py, pero conviene ser explicito.
# 'gunicorn' es el servidor WSGI; App Engine expone la app en $PORT.
entrypoint: gunicorn -b :$PORT -w 2 --timeout 60 main:app
# Clase de instancia: define CPU y memoria de cada instancia.
# F1 = 256 MB / 600 MHz (mas barata, la del nivel gratuito)
# F2 = 512 MB / 1,2 GHz
# F4 = 1 GB / 2,4 GHz
# F4_1G = 2 GB / 2,4 GHz
instance_class: F2
# Politica de escalado: ver apartado 6.
automatic_scaling:
min_instances: 0 # escala a cero cuando no hay trafico
max_instances: 10 # techo de seguridad para la factura
target_cpu_utilization: 0.6 # crea instancias al superar el 60 % de CPU
target_throughput_utilization: 0.6
max_concurrent_requests: 20 # peticiones simultaneas por instancia
min_pending_latency: 100ms # espera antes de decidir crear instancia
max_pending_latency: 500ms # si una peticion espera mas, crea instancia ya
# Variables de entorno NO sensibles. Los secretos, en 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"
# Conector para acceder a Cloud SQL por IP privada o a recursos de la VPC (03-01).
vpc_access_connector:
name: projects/alpinashop-prod/locations/europe-west1/connectors/conector-vpc
# Enrutamiento de peticiones, en orden. La primera coincidencia gana.
handlers:
# Recursos estaticos servidos directamente por la infraestructura de Google,
# sin consumir instancias de la aplicacion ni tiempo de CPU.
- url: /estatico
static_dir: estatico
secure: always # fuerza HTTPS
expiration: "30d" # cabecera Cache-Control de 30 dias
- url: /favicon\.ico
static_files: estatico/favicon.ico
upload: estatico/favicon\.ico
# Panel interno: solo usuarios autenticados con cuenta de la organizacion.
- url: /admin/.*
script: auto
secure: always
login: admin
# Todo lo demas va a la aplicacion.
- url: /.*
script: auto
secure: alwaysTres ideas de este fichero que conviene retener:
- Los
handlersde tipo estático son gratis en tiempo de CPU. Google sirve esos ficheros desde su propia infraestructura, sin arrancar ni ocupar una instancia. Servir CSS, JS e imágenes por ahí en lugar de con Flask reduce el coste y la latencia de forma notable. secure: alwaysdebería estar en todos los handlers. Redirige HTTP a HTTPS automáticamente.max_concurrent_requestses la palanca más importante del coste. Si tu aplicación pasa la mayor parte del tiempo esperando a la base de datos, puede atender muchas peticiones simultáneas por instancia; subir este valor reduce el número de instancias necesarias. Si la aplicación consume CPU, subirlo empeora la latencia.
Junto al app.yaml van los ficheros habituales de un proyecto 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: lo que NO se sube en el despliegue
.git/
.gitignore
__pycache__/
*.pyc
venv/
.env
tests/
README.mdEl .gcloudignore no es un detalle menor: sin él, subirías el directorio .git y el entorno virtual entero, con despliegues lentos y el riesgo real de publicar un fichero .env con credenciales.
- Desplegar el catálogo de AlpinaShop
La aplicación, con las piezas que ya conocemos de las lecciones anteriores:
# 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()
cliente_storage = storage.Client()
bucket = cliente_storage.bucket(os.environ["BUCKET_CATALOGO"])
def _conectar_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=_conectar_bd,
pool_size=2, # bajo a proposito: App Engine crea MUCHAS instancias
max_overflow=1,
pool_recycle=1800,
pool_pre_ping=True,
)
@app.route("/")
def catalogo():
consulta = sqlalchemy.text(
"SELECT sku, nombre, precio FROM tienda.productos "
"WHERE activo = true ORDER BY nombre LIMIT 50"
)
with engine.connect() as conn:
productos = conn.execute(consulta).mappings().all()
filas = "".join(
f"<li>{p['sku']} — {p['nombre']} — {p['precio']:.2f} €</li>" for p in productos
)
version = os.environ.get("GAE_VERSION", "local")
return render_template_string(
f"<h1>AlpinaShop</h1><ul>{filas}</ul><p>Version: {version}</p>"
)
@app.route("/salud")
def salud():
return "ok", 200
if __name__ == "__main__":
# Solo para desarrollo local; en App Engine arranca gunicorn.
app.run(host="127.0.0.1", port=8080, debug=True)Fíjate en pool_size=2. Es un cambio deliberado respecto a la lección anterior: App Engine puede crear decenas de instancias en un pico, y cada una abriría su propio pool. Con pool_size=5 y 20 instancias serían 100 conexiones solo del catálogo. En entornos que escalan de forma agresiva, los pools deben ser pequeños.
Observa también GAE_VERSION: App Engine inyecta variables de entorno con información del entorno de ejecución (GAE_SERVICE, GAE_VERSION, GAE_INSTANCE, GOOGLE_CLOUD_PROJECT). Mostrar la versión en la página es un truco muy práctico para verificar despliegues y divisiones de tráfico.
Despliegue:
# Probar en local antes 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 con un identificador de version explicito
gcloud app deploy app.yaml \
--version=v1-catalogo \
--project=alpinashop-dev
# Abrir en el navegador
gcloud app browse
# Ver la URL asignada
gcloud app describe --format="value(defaultHostname)"Al desplegar, App Engine construye el paquete, instala las dependencias de requirements.txt, crea una versión nueva y —salvo que digas lo contrario— le envía el 100 % del tráfico. La URL por defecto es https://<projectId>.<region>.r.appspot.com, con certificado TLS válido desde el primer segundo y sin haber configurado nada.
Usa siempre --version con un nombre significativo. Sin él, App Engine genera un identificador con marca temporal, difícil de leer en la lista de versiones y en los comandos de reparto de tráfico.
- Versiones, división de tráfico y despliegue canary
Cada despliegue crea una versión inmutable que permanece disponible. Esto habilita algo muy potente: repartir el tráfico entre versiones.
# Desplegar sin enviar trafico: la version queda disponible pero inactiva
gcloud app deploy app.yaml --version=v2-precios --no-promote
# Probarla en su URL propia, sin afectar a los clientes
gcloud app browse --version=v2-precios
# https://v2-precios-dot-alpinashop-dev.ew.r.appspot.com
# Canary: 10 % del trafico a la version nueva
gcloud app services set-traffic default \
--splits=v1-catalogo=0.9,v2-precios=0.1
# Si todo va bien, aumentar progresivamente
gcloud app services set-traffic default \
--splits=v1-catalogo=0.5,v2-precios=0.5
# Promocion completa
gcloud app services set-traffic default --splits=v2-precios=1
# Rollback inmediato si algo falla
gcloud app services set-traffic default --splits=v1-catalogo=1La URL con el prefijo <version>-dot-<servicio>-dot-<proyecto> es un detalle muy útil: cualquier versión desplegada es accesible directamente, lo que permite validar en el entorno real antes de dirigirle usuarios.
Sobre el criterio de reparto, hay dos modos y la diferencia importa:
# Reparto aleatorio por peticion: cada peticion puede ir a una version distinta
gcloud app services set-traffic default --splits=v1=0.9,v2=0.1 --split-by=random
# Reparto por cookie: un mismo usuario ve siempre la misma version
gcloud app services set-traffic default --splits=v1=0.9,v2=0.1 --split-by=cookiePara un canary de una interfaz de usuario, cookie es casi siempre lo correcto: con random, un cliente podría ver una página en la versión antigua y la siguiente en la nueva, con resultados desconcertantes.
Y una advertencia de coste: las versiones antiguas con instancias mínimas configuradas siguen facturando aunque no reciban tráfico. Limpia periódicamente:
- Tipos de escalado y su efecto en el coste
App Engine estándar ofrece tres modos de escalado, y elegir mal es la principal fuente de facturas inesperadas.
| Automático | Básico | Manual | |
|---|---|---|---|
| Crea instancias según | Tráfico, CPU y latencia | Llegada de peticiones | Nunca: número fijo |
| Escala a cero | Sí (si min_instances: 0) |
Sí, tras inactividad | No |
| Arranque en frío | Sí | Sí | No |
| Duración máx. de petición | 10 minutos | 24 horas | 24 horas |
| Uso típico | Aplicaciones web | Tareas largas y esporádicas | Servicios con estado en memoria, carga constante |
# Automatico: el caso general
automatic_scaling:
min_instances: 0
max_instances: 10
min_idle_instances: 0 # instancias "calientes" en reserva
max_concurrent_requests: 20
target_cpu_utilization: 0.6# Basico: para un servicio de procesamiento por lotes
basic_scaling:
max_instances: 3
idle_timeout: 10m # apaga la instancia tras 10 min sin peticionesEl problema del arranque en frío. Con min_instances: 0 no pagas nada cuando no hay tráfico, pero la primera petición después de un periodo de inactividad tiene que esperar a que arranque una instancia: cargar el intérprete, importar las dependencias, abrir el pool de conexiones. En Python con Flask suelen ser 1–3 segundos; si el código importa bibliotecas pesadas, más.
La solución es reservar instancias calientes:
automatic_scaling:
min_instances: 1 # siempre 1 instancia viva
min_idle_instances: 1 # ademas, 1 de reserva lista para picosY aquí está el compromiso, con números orientativos:
| Configuración | Coste aproximado sin tráfico | Latencia de la primera petición |
|---|---|---|
min_instances: 0 |
~0 € | 1–3 s (arranque en frío) |
min_instances: 1, clase F2 |
Una instancia F2 24×7: del orden de 30–40 €/mes | Inmediata |
min_instances: 2 |
El doble | Inmediata, con redundancia |
Cifras orientativas para razonar sobre el orden de magnitud. Consulta la calculadora oficial de Google Cloud.
Decisión de AlpinaShop, coherente con su realidad: en alpinashop-dev, min_instances: 0 —a Dani no le importa esperar dos segundos y el entorno pasa la noche sin tráfico. En producción, min_instances: 1 y max_instances: 10, porque un cliente que ve la tienda tardar tres segundos en cargar es un cliente que se va. Durante la campaña de otoño se sube el mínimo a 3 y el máximo a 20.
Consejos adicionales para reducir arranques en frío: importa las bibliotecas pesadas de forma perezosa, no ejecutes trabajo costoso en el arranque del módulo, y mantén requirements.txt sin dependencias que no uses.
- Servicios y enrutamiento con
dispatch.yaml
dispatch.yamlUna aplicación de App Engine puede tener varios servicios (antes llamados módulos), cada uno con su propio app.yaml, su escalado y sus versiones. Es la forma de separar componentes con perfiles distintos.
Para AlpinaShop:
alpinashop/ ├── dispatch.yaml ├── web/ -> app.yaml con "service: default" (catálogo público) ├── api/ -> app.yaml con "service: api" (API de pedidos) └── admin/ -> app.yaml con "service: admin" (panel interno)
# 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 panel se usa poco: no vale la pena tenerlo caliente
max_instances: 2
idle_timeout: 15mCada servicio tiene su URL propia (https://api-dot-alpinashop-prod.ew.r.appspot.com). Para servirlos todos bajo un mismo dominio con rutas distintas, se usa dispatch.yaml:
dispatch:
- url: "*/api/*"
service: api
- url: "*/admin/*"
service: admin
- url: "*/*"
service: defaultVentajas de separar servicios: cada uno escala según su propia carga (el panel de administración no necesita 10 instancias porque haya un pico en la tienda), se despliegan de forma independiente, y un fallo en el panel no tumba el catálogo. Es una separación de dominios de fallo, la misma idea que aplicamos en 01-05 a las zonas.
Ten en cuenta que las reglas de dispatch.yaml se evalúan en orden y solo se admiten un número limitado; para enrutamiento complejo se usa un balanceador de carga (03-02).
- Acceso a Cloud SQL y Cloud Storage desde App Engine
Aquí se ve el valor de haber usado bibliotecas cliente en las lecciones anteriores: el código no cambia.
Cada aplicación de App Engine tiene una cuenta de servicio por defecto, <projectId>@appspot.gserviceaccount.com. Basta con darle los permisos adecuados:
PROYECTO=alpinashop-prod
SA="[email protected]"
# Acceso a Cloud SQL
gcloud projects add-iam-policy-binding $PROYECTO \
--member="serviceAccount:$SA" --role="roles/cloudsql.client"
# Lectura y escritura de objetos en el bucket del catalogo
gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
--member="serviceAccount:$SA" --role="roles/storage.objectUser"
# Lectura del secreto con la contraseña de la base de datos (03-06)
gcloud secrets add-iam-policy-binding db-password-catalogo \
--member="serviceAccount:$SA" --role="roles/secretmanager.secretAccessor"Con eso, el mismo storage.Client() y el mismo conector de Cloud SQL de las lecciones 02-02 y 02-03 funcionan sin tocar una línea, porque las Application Default Credentials resuelven ahora a la cuenta de servicio de App Engine.
Una particularidad del entorno estándar: el sistema de ficheros es de solo lectura salvo /tmp, que además vive en memoria y cuenta contra la memoria de la instancia. Nunca guardes ahí nada que deba persistir ni ficheros grandes. Las imágenes que suben los usuarios van directas a Cloud Storage, exactamente como programamos en 02-02 con upload_from_file sobre el flujo.
- Variables de entorno y configuración
Las variables no sensibles van en app.yaml, bajo env_variables. Las sensibles, no: app.yaml acaba en el repositorio de Git y sus valores son visibles para cualquiera que pueda ver la configuración de la versión.
El patrón correcto es leer los secretos en el arranque desde Secret Manager:
import os
from google.cloud import secretmanager
def leer_secreto(nombre: str, version: str = "latest") -> str:
"""Lee un secreto de Secret Manager usando la cuenta de servicio de App Engine."""
proyecto = os.environ["GOOGLE_CLOUD_PROJECT"]
cliente = secretmanager.SecretManagerServiceClient()
ruta = f"projects/{proyecto}/secrets/{nombre}/versions/{version}"
respuesta = cliente.access_secret_version(request={"name": ruta})
return respuesta.payload.data.decode("UTF-8")
# Se lee UNA vez al arrancar la instancia, no en cada peticion:
# cada llamada a Secret Manager tiene latencia y coste por operacion.
DB_PASS = leer_secreto("db-password-catalogo")Para distinguir entornos, lo habitual es tener un app.yaml por entorno (app-dev.yaml, app-prod.yaml) y desplegar el que toque:
gcloud app deploy app-dev.yaml --project=alpinashop-dev
gcloud app deploy app-prod.yaml --project=alpinashop-prodEl detalle completo de Secret Manager, la rotación de secretos y el cifrado con Cloud KMS está en la lección 03-06.
- Logs y errores
App Engine envía automáticamente a Cloud Logging las peticiones y todo lo que la aplicación escriba en la salida estándar. No hay que configurar nada.
# Seguir los logs en tiempo real
gcloud app logs tail -s default
# Ultimas 50 entradas de una version concreta
gcloud app logs read --version=v2-precios --limit=50
# Consulta avanzada: errores 5xx de la ultima hora
gcloud logging read \
'resource.type="gae_app" AND httpRequest.status>=500' \
--limit=20 --freshness=1h --format="value(timestamp, httpRequest.status, httpRequest.requestUrl)"Para que los logs sean realmente útiles, escríbelos estructurados en JSON: Cloud Logging los indexa por campos y podrás filtrar por sku o por severidad en lugar de buscar cadenas.
import json
import logging
import sys
def log_estructurado(mensaje, severidad="INFO", **campos):
entrada = {"message": mensaje, "severity": severidad, **campos}
print(json.dumps(entrada), file=sys.stdout)
log_estructurado("producto consultado", sku="MOC-40", duracion_ms=42)
log_estructurado("stock insuficiente", severidad="WARNING", sku="BOT-GTX", solicitado=5)Los errores no capturados llegan automáticamente a Error Reporting, que los agrupa por traza de pila, cuenta ocurrencias y puede avisar cuando aparece un error nuevo. Es de lo más rentable que ofrece la plataforma sin configuración.
El tratamiento en profundidad de Cloud Logging, las métricas y las trazas está en 06-06 y 06-04.
- Tareas en segundo plano: Cloud Tasks y
cron.yaml
cron.yamlUna petición HTTP no debe hacer trabajo largo: el usuario espera, y en escalado automático hay un límite de 10 minutos. Cuando AlpinaShop confirma un pedido, hay que enviar un correo, actualizar el stock y avisar al almacén; nada de eso debe bloquear la respuesta al cliente.
Cloud Tasks es una cola gestionada: encolas una tarea, que consiste en una petición HTTP diferida a tu propia aplicación, y el servicio la entrega con reintentos y control de ritmo.
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
cliente_tareas = tasks_v2.CloudTasksClient()
COLA = cliente_tareas.queue_path("alpinashop-prod", "europe-west1", "cola-pedidos")
def encolar_confirmacion(pedido_id: int):
"""Encola el procesamiento posterior a la confirmacion de un pedido."""
tarea = {
"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 cliente_tareas.create_task(request={"parent": COLA, "task": tarea})
@app.route("/tareas/procesar-pedido", methods=["POST"])
def procesar_pedido():
# App Engine garantiza que esta cabecera solo la puede poner Cloud Tasks:
# peticiones externas con esta cabecera son rechazadas por la infraestructura.
if "X-AppEngine-TaskName" not in request.headers:
return "prohibido", 403
datos = request.get_json()
# ... enviar correo, actualizar stock, notificar al almacen ...
# Devolver 2xx marca la tarea como completada.
# Cualquier otro codigo provoca reintento con retroceso exponencial,
# asi que el manejador DEBE ser idempotente.
return "", 204Dos puntos críticos aquí: la comprobación de la cabecera X-AppEngine-TaskName como control de acceso, y la idempotencia. Cloud Tasks garantiza entrega al menos una vez, no exactamente una vez: si tu manejador tarda y hay un reintento, podrías enviar dos correos o descontar el stock dos veces. Registra qué pedidos ya has procesado y comprueba antes de actuar.
Cron. Para tareas programadas, cron.yaml invoca una URL de tu aplicación según un horario:
cron:
- description: "Exportacion diaria de pedidos para analitica"
url: /tareas/exportar-pedidos
schedule: every day 03:00
timezone: Europe/Madrid
target: api
- description: "Recordatorio de carritos abandonados"
url: /tareas/carritos-abandonados
schedule: every 6 hours
- description: "Informe semanal de ventas para Lucia"
url: /tareas/informe-semanal
schedule: every monday 07:00
timezone: Europe/MadridLos endpoints invocados por cron reciben la cabecera X-Appengine-Cron: true, que se comprueba igual que la de Tasks. Protege esas rutas también con un handler login: admin en app.yaml.
El servicio equivalente fuera de App Engine es Cloud Scheduler, que puede invocar HTTP, Pub/Sub o Cloud Functions y funciona con cualquier servicio de cómputo. Es la opción a usar si tu aplicación no vive en App Engine.
- Limitaciones reales y por qué hoy muchos equipos eligen Cloud Run
App Engine sigue siendo un buen producto y hay aplicaciones que llevan más de una década funcionando en él sin sobresaltos. Pero conviene ser sincero sobre sus límites en 2026:
- Acoplamiento al formato de Google.
app.yaml,dispatch.yamlycron.yamlno existen fuera de App Engine. Migrar a otro proveedor —o incluso a otro servicio de Google— implica rehacer la configuración. - Runtimes limitados y con calendario propio. Solo los lenguajes y versiones que Google soporta; cuando una versión de Python queda obsoleta, hay que migrar en el plazo que marque Google.
- Sandbox restrictivo en el entorno estándar. Nada de binarios propios, dependencias del sistema o escritura en disco.
- El entorno flexible ha quedado desplazado. Arranca en minutos, no escala a cero y cuesta más que las alternativas basadas en contenedores.
- Región inmutable y una aplicación por proyecto. Rígido para arquitecturas que evolucionan.
- La inversión no se acumula. El tiempo dedicado a dominar
app.yamlno sirve en ningún otro sitio; el tiempo dedicado a dominar contenedores sirve en todas partes.
Cloud Run (lección 07-02) ocupa hoy ese espacio: ejecuta tu contenedor, escala a cero, se factura por uso, ofrece división de tráfico entre revisiones igual que App Engine, HTTPS automático, y lo que despliegas es una imagen de contenedor estándar que también funcionaría en GKE, en tu portátil o en otra nube.
| App Engine estándar | Cloud Run | |
|---|---|---|
| Unidad de despliegue | Código + app.yaml |
Imagen de contenedor |
| Lenguajes | Los soportados | Cualquiera |
| Escalado a cero | Sí | Sí |
| División de tráfico | Sí | Sí (revisiones) |
| Portabilidad | Baja | Alta |
| Configuración | Fichero propio de Google | Contenedor estándar + flags |
| Dependencias del sistema | Limitadas | Las que pongas en el Dockerfile |
Cuándo sigue teniendo sentido App Engine: ya lo usas y funciona; quieres el camino más corto desde código Python a una URL con HTTPS; no tienes contenedores ni ganas de aprenderlos ahora mismo; o necesitas específicamente dispatch.yaml y el cron integrado.
Cuándo elegir Cloud Run: proyecto nuevo, ya trabajas con contenedores, quieres portabilidad, o necesitas control sobre el entorno de ejecución.
Para AlpinaShop, la decisión que documentaremos en 02-07 es empezar por Compute Engine (lift-and-shift, ya hecho) y evolucionar hacia contenedores. App Engine se queda como una opción conocida y descartada de forma razonada, que es la mejor manera de descartar algo.
Errores Comunes y Consejos
- Crear la aplicación en la región equivocada. Es irreversible: exige proyecto nuevo. Verifica antes de
gcloud app create. - Desplegar sin
.gcloudignore. Despliegues lentos y riesgo de subir.envo.git. - Olvidar
--no-promoteal probar. Enviarás el 100 % del tráfico a una versión sin validar. - Usar
--split-by=randompara un canary de interfaz. El usuario ve versiones distintas en peticiones consecutivas. - Dejar versiones antiguas con
min_instances > 0. Facturan sin recibir tráfico. - Poner
min_instances: 0en producción sin medir. El arranque en frío se lo come la conversión. - Pools de conexiones grandes. Multiplicados por muchas instancias, agotan
max_connectionsde Cloud SQL. - Guardar ficheros en el sistema local. Solo
/tmp, en memoria y efímero. - Secretos en
env_variablesdeapp.yaml. Acaban en Git. Usa Secret Manager. - Manejadores de Cloud Tasks no idempotentes. La entrega es al menos una vez.
- Servir estáticos desde Flask. Usa handlers
static_dir: son más rápidos y no consumen instancias. - Consejo: pon
secure: alwaysen todos los handlers. - Consejo: usa
--versioncon nombres legibles. - Consejo: muestra
GAE_VERSIONen la aplicación para verificar de un vistazo qué versión sirve cada petición. - Consejo: define
max_instancessiempre. Es tu freno de emergencia ante un pico anómalo o un ataque.
Ejercicios
Ejercicio 1: primer despliegue y control del escalado
- Crea la aplicación de App Engine en
europe-west1sobrealpinashop-dev. - Escribe una aplicación Flask mínima que muestre "AlpinaShop" y la versión (
GAE_VERSION), con una ruta/salud. - Escribe un
app.yamlcon runtimepython312, claseF1, escalado automático de 0 a 4 instancias y un handler estático para/estatico. - Despliega como versión
v1y comprueba la URL. - Explica qué cambiarías en
app.yamlpara producción y justifica el coste asociado.
Ejercicio 2: canary y rollback
- Modifica la aplicación para mostrar los precios con IVA y despliégala como
v2sin enviarle tráfico. - Comprueba
v2en su URL específica. - Envía el 20 % del tráfico a
v2con reparto por cookie. - Comprueba el reparto haciendo varias peticiones y observando la versión mostrada.
- Simula un fallo y ejecuta un rollback completo a
v1. Después limpia las versiones sin tráfico.
Ejercicio 3: servicios, cron y tareas
- Crea un segundo servicio
apicon su propioapp.yamly escalado propio, que exponga/api/pedidos. - Escribe un
dispatch.yamlque envíe/api/*al servicioapiy el resto aldefault. - Añade un
cron.yamlcon una tarea diaria a las 3:00 (hora de Madrid) que llame a/tareas/exportar-pedidosen el servicioapi. - Protege el endpoint de cron comprobando la cabecera correspondiente.
- Explica por qué el manejador de una tarea debe ser idempotente y cómo lo garantizarías para el envío del correo de confirmación de un pedido.
Soluciones
Solución 1
# main.py
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
version = os.environ.get("GAE_VERSION", "local")
instancia = os.environ.get("GAE_INSTANCE", "local")[:8]
return f"<h1>AlpinaShop</h1><p>Version: {version}</p><p>Instancia: {instancia}</p>"
@app.route("/salud")
def salud():
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: alwaysPara producción cambiaría instance_class: F2 (más memoria para el pool de conexiones y las bibliotecas cliente), min_instances: 1 para eliminar el arranque en frío del primer cliente, y max_instances: 10 para absorber los picos de otoño. El coste añadido es el de mantener una instancia F2 encendida las 24 horas, del orden de 30–40 € al mes: una cifra pequeña frente al valor de que la portada de la tienda cargue siempre en menos de un segundo. Conviene verificar el importe en la calculadora oficial antes de comprometerse.
Solución 2
# 1. Desplegar sin promover
gcloud app deploy app.yaml --version=v2 --no-promote
# 2. Probar la version concreta
gcloud app browse --version=v2
# o directamente:
curl -s "https://v2-dot-alpinashop-dev.ew.r.appspot.com/"
# 3. Canary del 20 % con afinidad por cookie
gcloud app services set-traffic default \
--splits=v1=0.8,v2=0.2 --split-by=cookie
# 4. Comprobar el reparto (con -c/-b se conserva la cookie entre peticiones)
for i in $(seq 1 10); do
curl -s "https://alpinashop-dev.ew.r.appspot.com/" | grep -o "Version: v[0-9]"
done
# 5. Rollback y limpieza
gcloud app services set-traffic default --splits=v1=1
gcloud app versions list --filter="traffic_split=0"
gcloud app versions delete v2 --quietCon --split-by=cookie, si mantienes la cookie entre peticiones verás siempre la misma versión: es el comportamiento deseado para no desconcertar al usuario. Sin cookie —como en el bucle de curl anterior, donde cada llamada es una sesión nueva— el reparto se comporta como aleatorio, y en torno a 2 de cada 10 respuestas mostrarán v2.
Solución 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 # bloquea el acceso externo a los endpoints de tareas
- url: /.*
script: auto
secure: always# dispatch.yaml
dispatch:
- url: "*/api/*"
service: api
- url: "*/tareas/*"
service: api
- url: "*/*"
service: default# cron.yaml
cron:
- description: "Exportacion diaria de pedidos"
url: /tareas/exportar-pedidos
schedule: every day 03:00
timezone: Europe/Madrid
target: api# 4. Proteccion del endpoint de cron
from flask import request
@app.route("/tareas/exportar-pedidos")
def exportar_pedidos():
if request.headers.get("X-Appengine-Cron") != "true":
return "prohibido", 403
# ... exportar a gs://alpinashop-catalogo/exportaciones/<fecha>/ ...
return "", 204- El manejador debe ser idempotente porque Cloud Tasks garantiza entrega al menos una vez: si el manejador tarda más de lo previsto, si devuelve un error transitorio o si la instancia se recicla a mitad, la tarea se reintenta y el mismo pedido llega dos veces. Sin protección, el cliente recibiría dos correos de confirmación y el stock se descontaría dos veces.
La forma de garantizarlo para el correo de confirmación es registrar el hecho de forma transaccional en la base de datos:
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:
resultado = conn.execute(
sqlalchemy.text(
"INSERT INTO tienda.correos_enviados (pedido_id, tipo) "
"VALUES (:pid, 'confirmacion') ON CONFLICT DO NOTHING"
),
{"pid": pedido_id},
)
if resultado.rowcount == 0:
return "", 204 # ya se envio: la tarea termina sin repetir el correo
enviar_correo_confirmacion(pedido_id)La clave está en ON CONFLICT DO NOTHING sobre una clave primaria: la base de datos garantiza que solo una ejecución consigue insertar la fila, y las demás salen sin enviar nada. Devolver 204 en el segundo intento evita además reintentos infinitos.
Conclusión
Has visto qué significa realmente subir un nivel de abstracción. En App Engine desaparecen el sistema operativo, los parches, el servidor web, el balanceo, los certificados TLS, los health checks y las plantillas de instancia: todo lo que ocupó la lección 02-01 se reduce a un app.yaml y un gcloud app deploy. Conoces la diferencia decisiva entre el entorno estándar —sandbox, arranque en segundos, escalado a cero, barato— y el flexible —contenedores, arranque en minutos, sin escalado a cero—, y sabes que hoy el flexible ha quedado desplazado por Cloud Run.
Has diseccionado app.yaml línea a línea: runtime, entrypoint, clase de instancia, política de escalado, variables de entorno y handlers, incluida la idea de servir los estáticos desde la infraestructura de Google para no gastar instancias. Has desplegado el catálogo Flask de AlpinaShop y has obtenido una URL con HTTPS sin configurar nada. Has aprendido a desplegar con --no-promote, validar en la URL de la versión, repartir tráfico con afinidad por cookie para un canary y hacer rollback con un solo comando: prácticas de despliegue que en Compute Engine exigían plantillas versionadas y actualizaciones progresivas. Has entendido los tres modos de escalado y el compromiso central entre min_instances: 0 y el arranque en frío, con la decisión razonada de AlpinaShop: cero en desarrollo, uno en producción, tres en campaña. Has separado servicios con dispatch.yaml para que el panel de administración y el catálogo escalen por separado, has conectado Cloud SQL y Cloud Storage sin cambiar una línea de código gracias a las credenciales por defecto, has leído secretos de Secret Manager en el arranque, has visto los logs estructurados y Error Reporting, y has movido el trabajo pesado fuera de la petición con Cloud Tasks y cron.yaml, aprendiendo por el camino qué significa que una entrega sea al menos una vez y cómo escribir un manejador idempotente.
Y has terminado con una valoración honesta: App Engine es cómodo pero acoplado al formato de Google, y la inversión que haces en él no se acumula fuera de él. La alternativa que sí se acumula son los contenedores. En 02-05, Google Kubernetes Engine, daremos ese paso: repasaremos Kubernetes desde cero —contenedor, pod, Deployment, Service, nodo y plano de control—, compararemos los modos Autopilot y Standard, crearemos el clúster alpinashop-cluster, construiremos y publicaremos la imagen del catálogo en Artifact Registry, escribiremos los manifiestos de Deployment y Service, escalaremos con HorizontalPodAutoscaler y haremos actualizaciones sin corte con su rollback. Es el nivel de abstracción con más poder y también con más responsabilidad de todo el módulo.
Curso de Google Cloud Platform (GCP)
Módulo 1: Introducción a Google Cloud Platform
- ¿Qué es Google Cloud Platform?
- Configuración de tu cuenta de GCP
- Descripción general de la consola de GCP
- Proyectos, jerarquía de recursos y facturación
- Regiones, zonas y modelo de responsabilidad compartida
- Cloud Shell y la CLI de gcloud
Módulo 2: Servicios principales de GCP
- Compute Engine: máquinas virtuales en Google Cloud
- Cloud Storage: almacenamiento de objetos
- Cloud SQL: bases de datos relacionales gestionadas
- App Engine: plataforma como servicio
- Google Kubernetes Engine (GKE)
- Bases de datos NoSQL: Firestore, Bigtable y Spanner
- Cómo elegir el servicio de cómputo adecuado
Módulo 3: Redes y seguridad
- Redes VPC
- Balanceo de carga en la nube
- Cloud CDN
- Gestión de identidad y acceso (IAM)
- Cloud Armor
- Secretos y cifrado: Secret Manager y Cloud KMS
- Cloud DNS, certificados TLS y publicación segura de servicios
Módulo 4: Datos y análisis
- BigQuery: el almacén de datos analítico
- Cloud Dataflow: procesamiento de datos por lotes y en streaming
- Cloud Dataproc: Spark y Hadoop gestionados
- Cloud Pub/Sub: mensajería asíncrona
- Cloud Data Fusion: integración de datos sin código
- Orquestación de pipelines con Cloud Composer y Workflows
- Gobierno del dato y cuadros de mando con Dataplex y Looker Studio
Módulo 5: Aprendizaje automático e IA
- Vertex AI: la plataforma de machine learning de GCP
- AutoML: modelos a medida sin escribir código
- TensorFlow en GCP: entrenamiento y servicio de modelos
- API de lenguaje natural
- API de visión
- IA generativa en Vertex AI: modelos Gemini y embeddings
- MLOps: del modelo al producto con Vertex AI Pipelines
Módulo 6: DevOps y monitoreo
- Cloud Build: integración continua en GCP
- Cloud Source Repositories y gestión del código fuente
- Cloud Functions: funciones sin servidor
- Cloud Monitoring (antes Stackdriver): métricas, paneles y alertas
- Cloud Deployment Manager e infraestructura como código nativa
- Cloud Logging y Cloud Trace: logs, trazas y diagnóstico
- Terraform en GCP: infraestructura como código en la práctica
Módulo 7: Temas avanzados de GCP
- Híbrido y multinube con Anthos
- Computación sin servidor con Cloud Run
- Redes avanzadas: VPC compartida, peering y conectividad híbrida
- Mejores prácticas de seguridad
- Gestión y optimización de costos
- Fiabilidad: SLO, alta disponibilidad y recuperación ante desastres
- Gobierno a escala: organización, políticas y auditoría
