La red de AlpinaShop ya está construida: alpinashop-vpc con sus subredes, el firewall que solo deja pasar lo necesario, Cloud NAT para la salida y la base de datos en IP privada. Pero el catálogo sigue sirviéndose por la dirección de una máquina concreta. Si esa máquina cae, la tienda cae. Si el tráfico crece, no hay nada que reparta. Y no hay HTTPS, ni dominio, ni un punto único donde aplicar seguridad. Esta lección pone delante la pieza que resuelve todo eso a la vez: el balanceador de carga.

Cloud Load Balancing no es un aparato ni una máquina virtual que tú administras. Es un servicio distribuido, implementado en la propia infraestructura de red de Google, sin instancias que dimensionar ni escalar. Su versión global tiene una propiedad que conviene entender bien porque cambia el diseño: una sola dirección IP anycast que se anuncia desde más de cien puntos de presencia repartidos por el mundo, de modo que un cliente de Madrid y otro de Helsinki entran por el punto más cercano a cada uno y viajan el resto del trayecto por la red privada de Google.

Además, el balanceador es el sitio donde se enchufa todo lo demás: el certificado TLS (03-07), la caché de Cloud CDN (03-03) y el cortafuegos de aplicación Cloud Armor (03-05) se configuran sobre él. Construirlo bien es la condición previa de las cuatro lecciones siguientes.

En esta lección aprenderás a orientarte en el catálogo de balanceadores sin perderte en la nomenclatura, montarás pieza a pieza el balanceador HTTP(S) externo global de AlpinaShop con gcloud, enrutarás /imagenes/* directamente al bucket alpinashop-catalogo y el resto al MIG alpinashop-web-mig, y dominarás las comprobaciones de estado, que son la causa del 90 % de los problemas de un balanceador recién creado.

Contenido

  1. Por qué un balanceador y qué problemas resuelve
  2. El catálogo de balanceadores de Google Cloud y cómo elegir
  3. Anatomía de un balanceador HTTP(S) externo global
  4. Preparativos: comprobación de estado y puerto con nombre
  5. El servicio de backend sobre el MIG
  6. El backend de bucket para las imágenes
  7. El mapa de URL: enrutar por ruta
  8. Proxy de destino y regla de reenvío
  9. Comprobaciones de estado en profundidad: por qué fallan
  10. Políticas de balanceo, capacidad y afinidad de sesión
  11. Redirección de HTTP a HTTPS
  12. Escalado global: por qué no hay que "precalentar"
  13. Balanceadores internos para servicios internos
  14. NEG sin servidor: Cloud Run detrás del mismo balanceador
  15. Registro y métricas del balanceador

  1. Por qué un balanceador y qué problemas resuelve

Un balanceador de carga hace mucho más que repartir peticiones. En el caso de AlpinaShop resuelve simultáneamente:

Problema actual Cómo lo resuelve el balanceador
Una sola VM: punto único de fallo Reparte entre las instancias del MIG y saca de rotación las que fallan
El tráfico solo entra por una zona El MIG regional cubre tres zonas; el balanceador reparte entre ellas
Clientes lejanos con latencia alta IP anycast: se entra por el PoP más cercano y se viaja por la red de Google
No hay HTTPS ni certificado Termina TLS con certificados gestionados (03-07)
Las imágenes las sirve la aplicación Un backend de bucket sirve alpinashop-catalogo sin tocar la aplicación
No hay dónde poner WAF ni caché Cloud Armor (03-05) y Cloud CDN (03-03) se aplican aquí
Actualizar la app implica corte Drenaje de conexiones y actualización progresiva del MIG

La idea clave: el balanceador es el borde de tu arquitectura. Todo lo que quieras hacer una sola vez para todo el tráfico —cifrar, filtrar, cachear, medir— se hace en el borde.

  1. El catálogo de balanceadores de Google Cloud y cómo elegir

Aquí es donde mucha gente se pierde, porque la nomenclatura ha cambiado con los años y los nombres son largos. Vamos a ordenarlo con tres preguntas.

Pregunta 1: ¿capa 7 o capa 4? Es decir, ¿el balanceador entiende HTTP (rutas, cabeceras, cookies) o solo mueve paquetes TCP/UDP?

Pregunta 2: ¿externo o interno? ¿Lo llaman desde internet o solo desde dentro de tu VPC?

Pregunta 3: ¿global o regional? ¿Una IP única para todo el mundo o una IP por región?

Con esas tres respuestas, el balanceador está determinado:

Balanceador Capa Externo/interno Alcance Protocolos Caso de uso
Balanceador de aplicaciones externo global 7 Externo Global HTTP, HTTPS, HTTP/2, HTTP/3 El de AlpinaShop. Web pública mundial, CDN, WAF
Balanceador de aplicaciones externo regional 7 Externo Regional HTTP, HTTPS Requisitos de residencia de datos en una región
Balanceador de aplicaciones interno 7 Interno Regional (o entre regiones) HTTP, HTTPS Microservicios internos con enrutamiento por ruta
Balanceador de red de proxy externo 4 (proxy) Externo Global o regional TCP, SSL Protocolos no HTTP que necesitan terminación TLS
Balanceador de red de paso a través externo 4 (paso a través) Externo Regional TCP, UDP, ESP, ICMP Preserva la IP de origen; juegos, protocolos raros
Balanceador de red de paso a través interno 4 Interno Regional TCP, UDP Backend interno de alto rendimiento; base de datos, gRPC

Dos distinciones que conviene tener claras:

  • Proxy frente a paso a través. Un balanceador proxy termina la conexión del cliente y abre otra hacia el backend; el backend ve la IP del balanceador y recibe la del cliente en la cabecera X-Forwarded-For. Un balanceador de paso a través no termina nada: el paquete llega al backend con la IP de origen original.
  • Global frente a regional. El global usa anycast y un único mapa de URL para todas las regiones; el regional vive en una sola región y su IP también. El global es el que permite CDN y Cloud Armor a escala mundial.

Regla práctica para elegir: si es tráfico web público, es el balanceador de aplicaciones externo global. Solo se baja de ahí por un requisito concreto: residencia de datos, un protocolo que no es HTTP o la necesidad de conservar la IP de origen a nivel de paquete.

  1. Anatomía de un balanceador HTTP(S) externo global

El balanceador no es un objeto: es una cadena de objetos que se crean por separado y se enlazan. Entender la cadena es entender el servicio.

flowchart TB
    C([Cliente en Europa])
    IP["Dirección IP global anycast<br/>alpinashop-lb-ip"]
    FR["Regla de reenvío<br/>puerto 443"]
    TP["Proxy HTTPS de destino<br/>+ certificado TLS (03-07)<br/>+ política SSL"]
    UM["Mapa de URL<br/>alpinashop-url-map"]
    BS["Servicio de backend<br/>bs-catalogo-web"]
    BB["Backend de bucket<br/>bb-catalogo-imagenes"]
    HC["Comprobación de estado<br/>hc-catalogo /salud"]
    MIG["MIG alpinashop-web-mig<br/>2–10 instancias, 3 zonas"]
    GCS[("Bucket alpinashop-catalogo")]

    C --> IP --> FR --> TP --> UM
    UM -->|"/imagenes/*"| BB --> GCS
    UM -->|"resto"| BS --> MIG
    HC -.comprueba.-> MIG
    BS -.usa.-> HC

Pieza a pieza, de fuera hacia dentro:

Pieza Qué hace Recurso gcloud
Dirección IP global La IP anycast pública que anuncian los PoP compute addresses --global
Regla de reenvío Une IP + puerto con un proxy de destino compute forwarding-rules
Proxy de destino Termina la conexión; en HTTPS, aplica el certificado y la política SSL compute target-https-proxies
Mapa de URL Decide, según host y ruta, qué servicio de backend atiende compute url-maps
Servicio de backend Agrupa backends, define política de balanceo, CDN, Cloud Armor, timeouts compute backend-services
Backend El grupo real: MIG, NEG o bucket backend-services add-backend
Comprobación de estado Decide qué instancias reciben tráfico compute health-checks

Se construyen de dentro hacia fuera: primero la comprobación de estado, luego los servicios de backend, después el mapa de URL, el proxy y por último la regla de reenvío. Si lo intentas al revés, gcloud se queja de que el recurso referenciado no existe.

  1. Preparativos: comprobación de estado y puerto con nombre

La aplicación Flask de AlpinaShop escucha en el puerto 8080 bajo gunicorn y expone una ruta de salud. Antes de nada, asegúrate de que esa ruta existe y es barata:

# app.py — ruta de salud de AlpinaShop
@app.route("/salud")
def salud():
    """Comprobación de estado para el balanceador.

    Debe ser BARATA y RÁPIDA: se invoca cada pocos segundos desde
    varios comprobadores. No consultes la base de datos aquí salvo
    que quieras que un problema de BD saque de rotación toda la flota.
    """
    return {"estado": "ok"}, 200

Ese matiz merece una explicación, porque es una decisión de arquitectura y no un detalle:

  • Si /salud no consulta la base de datos, un problema de base de datos deja los backends "sanos" y los clientes reciben errores 500 de la aplicación. Malo, pero acotado.
  • Si /salud sí consulta la base de datos, un problema de base de datos marca todos los backends como no sanos a la vez, el balanceador se queda sin destinos y devuelve 502 a todo el mundo. Peor: has convertido un fallo parcial en una caída total.

La práctica habitual es tener dos rutas: /salud superficial (para el balanceador) y /salud/completo profunda (para la monitorización de 06-04, que alerta pero no saca de rotación).

Ahora, la comprobación de estado y el puerto con nombre:

gcloud config set project alpinashop-prod

# Comprobación de estado HTTP contra /salud en el 8080
gcloud compute health-checks create http hc-catalogo \
  --port=8080 \
  --request-path=/salud \
  --check-interval=10s \
  --timeout=5s \
  --healthy-threshold=2 \
  --unhealthy-threshold=3 \
  --description="Salud del catalogo de AlpinaShop"

# El grupo de instancias debe declarar un "puerto con nombre":
# el servicio de backend se referirá al nombre, no al número.
gcloud compute instance-groups managed set-named-ports alpinashop-web-mig \
  --region=europe-west1 \
  --named-ports=http-catalogo:8080

El puerto con nombre es una fuente habitual de confusión. El servicio de backend no dice "manda el tráfico al 8080"; dice "manda el tráfico al puerto llamado http-catalogo", y cada grupo de instancias traduce ese nombre a un número. La ventaja es que puedes cambiar el puerto de la aplicación sin tocar el balanceador. El inconveniente es que si olvidas definir el puerto con nombre, el balanceador usa el http por defecto (puerto 80), no encuentra nada y todo aparece como no saludable.

Parámetros de la comprobación, explicados:

Parámetro Valor Significado
--check-interval 10s Cada cuánto se comprueba
--timeout 5s Cuánto se espera la respuesta antes de contarla como fallo
--healthy-threshold 2 Comprobaciones correctas seguidas para volver a rotación
--unhealthy-threshold 3 Fallos seguidos para salir de rotación

Con estos valores, una instancia rota tarda unos 30 segundos en salir de rotación y una recuperada unos 20 en volver. Bajar el intervalo detecta antes pero genera más carga; subir el umbral de fallo evita expulsiones por un pico transitorio.

  1. El servicio de backend sobre el MIG

El servicio de backend es la pieza central: define cómo se reparte el tráfico, qué comprobación se usa, cuánto se espera una respuesta, si hay CDN y si hay Cloud Armor.

# 1) Crear el servicio de backend (esquema EXTERNAL_MANAGED = balanceador global moderno)
gcloud compute backend-services create bs-catalogo-web \
  --global \
  --load-balancing-scheme=EXTERNAL_MANAGED \
  --protocol=HTTP \
  --port-name=http-catalogo \
  --health-checks=hc-catalogo \
  --timeout=30s \
  --connection-draining-timeout=60s \
  --description="Backend del catalogo web de AlpinaShop"

# 2) Añadir el MIG regional como backend
gcloud compute backend-services add-backend bs-catalogo-web \
  --global \
  --instance-group=alpinashop-web-mig \
  --instance-group-region=europe-west1 \
  --balancing-mode=RATE \
  --max-rate-per-instance=80 \
  --capacity-scaler=1.0

Opciones importantes:

  • --load-balancing-scheme=EXTERNAL_MANAGED: es el esquema del balanceador de aplicaciones externo global actual, basado en el proxy Envoy de Google. El valor antiguo EXTERNAL corresponde al balanceador clásico. Usa siempre EXTERNAL_MANAGED para desarrollos nuevos: soporta enrutamiento avanzado, reintentos, reescritura de cabeceras y espejo de tráfico.
  • --timeout=30s: cuánto espera el balanceador la respuesta completa del backend. Si la aplicación tiene alguna operación lenta (una exportación de catálogo, por ejemplo), súbelo o mueve esa operación a un proceso asíncrono. Un 502 con backend_timeout en los logs es exactamente esto.
  • --connection-draining-timeout=60s: cuando una instancia sale del grupo (por autoescalado o por una actualización), el balanceador deja de mandarle peticiones nuevas pero le da 60 segundos para terminar las que tiene en curso. Sin drenaje, cada reducción del autoescalado corta conexiones vivas.
  • --balancing-mode=RATE con --max-rate-per-instance=80: cada instancia se considera "llena" a 80 peticiones por segundo. Lo desarrollamos en el apartado 10.

  1. El backend de bucket para las imágenes

Las 60 GB de imágenes del catálogo no deben pasar por la aplicación Flask. Un backend de bucket conecta el balanceador directamente con alpinashop-catalogo: Google sirve el objeto sin que ninguna instancia intervenga.

gcloud compute backend-buckets create bb-catalogo-imagenes \
  --gcs-bucket-name=alpinashop-catalogo \
  --description="Imagenes de producto servidas directamente desde Cloud Storage"

Ventajas frente a servirlas desde la aplicación:

Aspecto Desde Flask Desde backend de bucket
Consumo de CPU de las VM Alto (cada imagen ocupa un worker) Cero
Escalado Limitado por el MIG Ilimitado, es Cloud Storage
Caché en CDN Requiere configurar cabeceras en la app Se activa con una opción (03-03)
Coste Instancias más grandes Solo almacenamiento y egress
Latencia Un salto extra Directo

Para que el bucket sea servible por el balanceador, sus objetos deben ser legibles públicamente o bien accesibles mediante cookies firmadas (que se ven en 03-03). Las imágenes de producto de AlpinaShop son públicas por naturaleza —están en la tienda—, así que:

# Lectura pública SOLO del prefijo de las imágenes web y miniaturas.
# Los originales de alta resolución NO deben ser públicos.
gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
  --member=allUsers \
  --role=roles/storage.objectViewer \
  --condition='expression=resource.name.startsWith("projects/_/buckets/alpinashop-catalogo/objects/productos/") && (resource.name.contains("/web/") || resource.name.contains("/thumb/")),title=solo-web-y-thumb,description=Publico solo el contenido optimizado para la tienda'

Fíjate en la condición IAM: hace pública solo la parte web/ y thumb/ del convenio de rutas que fijamos en 02-02, dejando original/ privado. Las condiciones de IAM se explican en 03-04; aquí sirven para no exponer los originales de alta resolución por descuido.

  1. El mapa de URL: enrutar por ruta

El mapa de URL es el cerebro del balanceador: mira el host y la ruta de cada petición y decide qué backend la atiende.

# 1) Crear el mapa con el servicio por defecto (la aplicación)
gcloud compute url-maps create alpinashop-url-map \
  --default-service=bs-catalogo-web \
  --description="Enrutamiento del sitio de AlpinaShop"

# 2) Añadir el matcher de rutas que desvía /imagenes/* al bucket
gcloud compute url-maps add-path-matcher alpinashop-url-map \
  --path-matcher-name=matcher-principal \
  --default-service=bs-catalogo-web \
  --backend-bucket-path-rules="/imagenes/*=bb-catalogo-imagenes,/estatico/*=bb-catalogo-imagenes" \
  --new-hosts="alpinashop.example,www.alpinashop.example"

El resultado se puede ver y editar como YAML, que es la forma más clara de razonar sobre él:

gcloud compute url-maps describe alpinashop-url-map --format=yaml
name: alpinashop-url-map
defaultService: .../backendServices/bs-catalogo-web
hostRules:
- hosts:
  - alpinashop.example
  - www.alpinashop.example
  pathMatcher: matcher-principal
pathMatchers:
- name: matcher-principal
  defaultService: .../backendServices/bs-catalogo-web
  pathRules:
  - paths:
    - /imagenes/*
    service: .../backendBuckets/bb-catalogo-imagenes
  - paths:
    - /estatico/*
    service: .../backendBuckets/bb-catalogo-imagenes

Cómo se evalúa una petición, con ejemplos reales de AlpinaShop:

Petición Regla que aplica Backend
https://alpinashop.example/ defaultService del matcher bs-catalogo-web
https://alpinashop.example/producto/moc-40 defaultService del matcher bs-catalogo-web
https://alpinashop.example/imagenes/productos/moc-40/web/frontal-800.webp /imagenes/* bb-catalogo-imagenes
https://alpinashop.example/estatico/css/tienda.css /estatico/* bb-catalogo-imagenes
https://otrodominio.example/ Ningún hostRule coincide defaultService del mapa

Tres detalles que ahorran horas de depuración:

  • La coincidencia de rutas es por prefijo con * al final, no una expresión regular. /imagenes/* coincide con todo lo que empieza por /imagenes/.
  • La ruta no se reescribe por defecto. Una petición a /imagenes/productos/moc-40/web/frontal-800.webp busca en el bucket el objeto imagenes/productos/moc-40/web/frontal-800.webp, con el prefijo imagenes/ incluido. Como nuestro convenio de 02-02 guarda los objetos en productos/<sku>/web/..., sin ese prefijo, hay que reescribir:
# Exportar, editar y volver a importar el mapa para añadir urlRewrite
gcloud compute url-maps export alpinashop-url-map \
  --destination=/tmp/url-map.yaml --global
# Fragmento a añadir dentro de la pathRule de /imagenes/*
  pathRules:
  - paths:
    - /imagenes/*
    service: .../backendBuckets/bb-catalogo-imagenes
    routeAction:
      urlRewrite:
        pathPrefixRewrite: /
gcloud compute url-maps import alpinashop-url-map \
  --source=/tmp/url-map.yaml --global --quiet

Con pathPrefixRewrite: /, la petición /imagenes/productos/moc-40/web/frontal-800.webp se convierte en /productos/moc-40/web/frontal-800.webp antes de llegar al bucket, que es exactamente la ruta del objeto.

  • gcloud compute url-maps validate comprueba el mapa antes de aplicarlo, incluyendo pruebas de enrutamiento declaradas en el propio YAML. Es muy recomendable en un pipeline (06-01).

  1. Proxy de destino y regla de reenvío

Las dos últimas piezas. Aquí montamos primero la versión HTTP para validar que todo funciona; el certificado y el proxy HTTPS son de 03-07, pero dejamos ya escrita la forma final.

# --- Versión HTTP (para validar la cadena antes de tener certificado) ---
gcloud compute target-http-proxies create alpinashop-http-proxy \
  --url-map=alpinashop-url-map

gcloud compute forwarding-rules create alpinashop-fr-http \
  --global \
  --address=alpinashop-lb-ip \
  --target-http-proxy=alpinashop-http-proxy \
  --ports=80 \
  --load-balancing-scheme=EXTERNAL_MANAGED
# --- Versión HTTPS (se completa en 03-07, con el certificado gestionado) ---
# gcloud compute target-https-proxies create alpinashop-https-proxy \
#   --url-map=alpinashop-url-map \
#   --ssl-certificates=alpinashop-cert \
#   --ssl-policy=alpinashop-ssl-policy
#
# gcloud compute forwarding-rules create alpinashop-fr-https \
#   --global --address=alpinashop-lb-ip \
#   --target-https-proxy=alpinashop-https-proxy \
#   --ports=443 --load-balancing-scheme=EXTERNAL_MANAGED

Comprobación de extremo a extremo:

IP=$(gcloud compute addresses describe alpinashop-lb-ip --global --format="value(address)")
echo "IP del balanceador: $IP"

# La aplicación
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" "http://$IP/"

# Una imagen desde el bucket
curl -s -o /dev/null -w "%{http_code}\n" "http://$IP/imagenes/productos/moc-40/web/frontal-800.webp"

# Estado de los backends
gcloud compute backend-services get-health bs-catalogo-web --global

Ten paciencia: un balanceador global recién creado tarda entre 5 y 10 minutos en propagarse por todos los puntos de presencia. Durante ese tiempo es normal recibir 404 o 502. Si a los quince minutos sigue fallando, entonces sí hay un problema real.

  1. Comprobaciones de estado en profundidad: por qué fallan

Este apartado es el más rentable de la lección. La mayoría de los balanceadores rotos no están mal construidos: tienen los backends en UNHEALTHY.

Primer punto, y el que más veces es la causa: las comprobaciones de estado no se originan en internet ni en tu subred. Google las lanza desde dos rangos fijos:

  • 130.211.0.0/22
  • 35.191.0.0/16

Si el firewall de VPC no los permite, la comprobación nunca llega, la instancia se marca no saludable y el balanceador devuelve 502 Server Error sin decir por qué. Ya creamos la regla en 03-01 (fw-allow-lb-health-web), pero conviene saber verificarla:

gcloud compute firewall-rules list \
  --filter="network:alpinashop-vpc AND sourceRanges:(130.211.0.0/22 OR 35.191.0.0/16)" \
  --format="table(name, allowed[].map().firewall_rule().list(), targetTags.list(), disabled)"

Lista de comprobación completa cuando un backend está UNHEALTHY:

Nº Causa Cómo confirmarla Solución
1 Firewall no permite 130.211.0.0/22 y 35.191.0.0/16 El comando anterior no devuelve nada Crear la regla
2 La etiqueta de red de la regla no está en las instancias gcloud compute instances list --format="table(name,tags.items.list())" Añadir la etiqueta a la plantilla del MIG
3 Puerto con nombre mal definido gcloud compute instance-groups managed describe alpinashop-web-mig --region=europe-west1 --format="value(namedPorts)" set-named-ports
4 La ruta /salud no existe o devuelve 301/302 curl -i http://IP_INTERNA:8080/salud desde otra VM Corregir la aplicación; la comprobación exige 200, no sigue redirecciones
5 La aplicación escucha en 127.0.0.1 en vez de 0.0.0.0 ss -tlnp dentro de la VM gunicorn --bind 0.0.0.0:8080
6 /salud consulta la base de datos y esta va lenta Latencia de /salud mayor que el timeout Hacer /salud superficial
7 La instancia aún está arrancando Recién creada por el autoescalado Ajustar --initial-delay del MIG
8 Protocolo equivocado (HTTPS en la comprobación, HTTP en la app) describe de la health check Usar http

Diagnóstico desde dentro de una VM, sin IP pública, gracias a IAP:

gcloud compute ssh alpinashop-web-1 --zone=europe-west1-b --tunnel-through-iap

# Dentro de la VM
curl -i http://localhost:8080/salud        # ¿responde la app?
ss -tlnp | grep 8080                       # ¿en qué interfaz escucha?
sudo journalctl -u gunicorn -n 50 --no-pager

Y la vista agregada, que dice qué instancia concreta está mal:

gcloud compute backend-services get-health bs-catalogo-web --global \
  --format="table(status.healthStatus[].instance, status.healthStatus[].healthState)"

Hay dos tipos de comprobación en la plataforma y conviene no confundirlos:

Tipo Para qué sirve Efecto de un fallo
Comprobación del balanceador Decidir si una instancia recibe tráfico Sale de rotación
Comprobación de autorreparación del MIG Decidir si una instancia se recrea Se destruye y se crea otra

Si usas la misma comprobación agresiva para ambas cosas, un fallo transitorio de la aplicación puede provocar que el MIG destruya toda la flota en cadena. La práctica recomendada es que la comprobación de autorreparación sea más laxa (umbral de fallo más alto y ruta más simple) que la del balanceador.

  1. Políticas de balanceo, capacidad y afinidad de sesión

Modo de balanceo

Cada backend declara cuándo se considera lleno. Hay tres modos:

Modo Métrica Cuándo usarlo
RATE Peticiones por segundo Aplicaciones web con peticiones homogéneas. El de AlpinaShop
UTILIZATION Uso de CPU del grupo Cargas con coste de CPU muy variable por petición
CONNECTION Conexiones simultáneas Balanceadores de capa 4 y conexiones largas

Para AlpinaShop, RATE con --max-rate-per-instance=80 significa: "cada instancia se da por llena a 80 req/s". Ese número no se inventa; se mide. La forma correcta de obtenerlo:

  1. Lanzar una prueba de carga contra una sola instancia.
  2. Subir la tasa hasta que la latencia del percentil 95 se degrade o aparezcan errores.
  3. Tomar entre el 60 % y el 70 % de esa cifra como max-rate-per-instance, dejando margen para picos.

Si el número es demasiado alto, el balanceador satura las instancias antes de desbordar a otras regiones. Si es demasiado bajo, el autoescalado añade máquinas innecesarias y sube el coste.

capacity-scaler: la palanca de operación más útil

--capacity-scaler es un multiplicador entre 0.0 y 1.0 sobre la capacidad declarada. Sirve para vaciar un backend sin borrarlo:

# Drenar el backend a la mitad (por ejemplo, durante un despliegue)
gcloud compute backend-services update-backend bs-catalogo-web \
  --global \
  --instance-group=alpinashop-web-mig \
  --instance-group-region=europe-west1 \
  --capacity-scaler=0.5

# Vaciarlo del todo (deja de recibir tráfico nuevo, respeta el drenaje)
gcloud compute backend-services update-backend bs-catalogo-web \
  --global --instance-group=alpinashop-web-mig \
  --instance-group-region=europe-west1 \
  --capacity-scaler=0.0

Es la forma limpia de hacer un despliegue azul/verde o de sacar de servicio una región con problemas.

Afinidad de sesión

Por defecto no hay afinidad: cada petición puede ir a una instancia distinta. Eso es lo correcto para una aplicación sin estado, que es como debe diseñarse.

Tipo de afinidad Basada en Coste
NONE (por defecto) Nada Reparto óptimo
CLIENT_IP IP del cliente Mal reparto tras NAT corporativo
GENERATED_COOKIE Cookie que genera el balanceador Reparto aceptable
HEADER_FIELD Una cabecera concreta Requiere control del cliente

AlpinaShop no necesita afinidad, y es importante entender por qué: el carrito y la sesión viven en Firestore (decidido en 02-06), no en la memoria de la instancia. La afinidad es un parche para aplicaciones con estado local, y siempre degrada el reparto: una instancia puede quedar sobrecargada mientras otras están ociosas, y el autoescalado no puede aliviarla porque los clientes ya están pegados a ella.

Otras opciones del servicio de backend

# Política de reintentos y detección de valores atípicos (solo EXTERNAL_MANAGED)
gcloud compute backend-services update bs-catalogo-web --global \
  --custom-request-header="X-Cliente-Region:{client_region}" \
  --custom-response-header="X-Cache-Estado:{cdn_cache_status}"

Las cabeceras personalizadas con variables ({client_region}, {client_city}, {cdn_cache_status}) son muy útiles: permiten que la aplicación sepa desde qué país entra el cliente sin geolocalizar nada, y que puedas depurar la caché mirando una cabecera de respuesta (lo usaremos en 03-03).

  1. Redirección de HTTP a HTTPS

Servir la tienda por HTTP sin cifrar no es aceptable. La redirección se hace en el propio balanceador, no en la aplicación, para que ni un solo byte de contenido viaje en claro.

Se implementa con un mapa de URL específico que no tiene backends, solo una acción de redirección:

# Mapa de URL que solo redirige
gcloud compute url-maps import alpinashop-redir-map --global --quiet --source=- <<'EOF'
name: alpinashop-redir-map
defaultUrlRedirect:
  httpsRedirect: true
  redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
  stripQuery: false
EOF

# El proxy HTTP apunta a ese mapa en vez de al mapa real
gcloud compute target-http-proxies update alpinashop-http-proxy \
  --url-map=alpinashop-redir-map

Explicación de las opciones:

  • httpsRedirect: true: cambia el esquema a https conservando host y ruta.
  • redirectResponseCode: MOVED_PERMANENTLY_DEFAULT: es un 301. Los navegadores lo cachean, así que las visitas siguientes ni siquiera hacen la petición en claro. Si estás probando y no quieres que se quede pegado en el navegador, usa FOUND (302) mientras tanto.
  • stripQuery: false: conserva la cadena de consulta. Ponerlo a true rompería los enlaces de campaña con parámetros utm_*.

La cabecera HSTS, que hace que el navegador ni siquiera intente HTTP, la emite la aplicación y se explica en 03-07.

  1. Escalado global: por qué no hay que "precalentar"

Quien viene de otros proveedores está acostumbrado a "precalentar" el balanceador antes de una campaña: avisar al proveedor de que va a llegar tráfico para que amplíe la capacidad del balanceador. En Google Cloud eso no existe y no hace falta.

El motivo es arquitectónico:

  • El balanceador global no es un conjunto de instancias tuyas. Es una función de la infraestructura de red de Google, la misma que atiende a Búsqueda, YouTube y Gmail. Su capacidad no está dimensionada para ti.
  • La IP anycast se anuncia desde todos los PoP simultáneamente. Un pico de tráfico no llega a un punto, sino que se reparte geográficamente por construcción.
  • El balanceador absorbe además los ataques volumétricos de capa 3 y 4 sin que tú configures nada, porque la mitigación está en la propia red.

Lo que sí hay que dimensionar es lo que hay detrás: el MIG alpinashop-web-mig está configurado para 2–10 instancias, y si la campaña de otoño necesita más, el límite se sube en el autoescalado. El cuello de botella nunca será el balanceador; será tu aplicación o tu base de datos.

Consecuencia práctica para el equipo de AlpinaShop: antes de la campaña, la lista de comprobación no incluye "avisar al balanceador", sino subir max-num-replicas del MIG, revisar max-rate-per-instance con datos reales y comprobar que la réplica de lectura de Cloud SQL aguanta.

  1. Balanceadores internos para servicios internos

No todo el tráfico es público. Cuando AlpinaShop tenga servicios internos —el motor de recomendaciones, la API de stock del almacén, el generador de informes de Lucía— esos servicios no deben tener IP pública. La pieza correcta es un balanceador interno:

# Balanceador de aplicaciones interno (capa 7) para un servicio interno
gcloud compute health-checks create http hc-stock-interno \
  --port=8080 --request-path=/salud --region=europe-west1

gcloud compute backend-services create bs-stock-interno \
  --region=europe-west1 \
  --load-balancing-scheme=INTERNAL_MANAGED \
  --protocol=HTTP \
  --health-checks=hc-stock-interno \
  --health-checks-region=europe-west1

# Subred de solo proxy: obligatoria para los balanceadores internos de capa 7.
# Es un rango reservado del que toman IP los proxies gestionados por Google.
gcloud compute networks subnets create sn-proxy-euw1 \
  --network=alpinashop-vpc \
  --region=europe-west1 \
  --range=10.10.10.0/24 \
  --purpose=REGIONAL_MANAGED_PROXY \
  --role=ACTIVE

Dos cosas nuevas y una advertencia:

  • El esquema es INTERNAL_MANAGED, no EXTERNAL_MANAGED.
  • Los balanceadores internos de capa 7 necesitan una subred de solo proxy (--purpose=REGIONAL_MANAGED_PROXY) por región. Es un rango que no aloja VM tuyas: lo usan los proxies de Google. Sin ella, la creación falla con un mensaje que no siempre es evidente. En el plan de direccionamiento de 03-01 hay que reservarle sitio: aquí hemos usado 10.10.10.0/24, dentro del bloque libre previsto.
  • Reglas de firewall: el tráfico llega desde el rango de la subred de solo proxy, no desde el cliente original. Hay que permitir 10.10.10.0/24 hacia los backends.

Cuándo usar cada uno:

Servicio de AlpinaShop Balanceador
Catálogo público Aplicaciones externo global (el que hemos construido)
API de stock consumida solo por la tienda Aplicaciones interno
Base de datos de sesión con protocolo propio Red de paso a través interno

  1. NEG sin servidor: Cloud Run detrás del mismo balanceador

En 02-07 decidimos (DA-001) que el catálogo acabará en Cloud Run. Cloud Run ya da una URL HTTPS propia, así que ¿para qué ponerle un balanceador delante? Por cuatro motivos que ya conoces:

  1. Dominio propio y certificado gestionado unificados (03-07).
  2. Cloud CDN sobre el contenido de la aplicación (03-03).
  3. Cloud Armor: la URL nativa de Cloud Run no se puede proteger con WAF; la del balanceador sí (03-05).
  4. Un único mapa de URL que reparte entre Cloud Run, el bucket y lo que venga.

La pieza que lo hace posible es el NEG sin servidor (serverless network endpoint group): un backend que no apunta a máquinas, sino a un servicio gestionado.

# NEG sin servidor que apunta al futuro servicio Cloud Run alpinashop-web
gcloud compute network-endpoint-groups create neg-alpinashop-web \
  --region=europe-west1 \
  --network-endpoint-type=serverless \
  --cloud-run-service=alpinashop-web

# Servicio de backend que lo usa (sin comprobación de estado: la gestiona Cloud Run)
gcloud compute backend-services create bs-alpinashop-run \
  --global \
  --load-balancing-scheme=EXTERNAL_MANAGED

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

Y entonces la migración del MIG a Cloud Run es un cambio en el mapa de URL, no una reconstrucción:

# Enviar solo /nuevo/* a Cloud Run para probar en producción con tráfico real
gcloud compute url-maps add-path-matcher alpinashop-url-map \
  --path-matcher-name=matcher-migracion \
  --default-service=bs-catalogo-web \
  --path-rules="/nuevo/*=bs-alpinashop-run" \
  --new-hosts="alpinashop.example"

Esto es lo que hace que el trabajo de hoy no se tire a la basura mañana: el balanceador sobrevive al cambio de backend. La IP, el dominio, el certificado, las reglas de Cloud Armor y la configuración de CDN permanecen; solo cambia a dónde apunta el mapa. Los detalles de Cloud Run son de 07-02.

Existen NEG de varios tipos, y conviene tenerlos ubicados:

Tipo de NEG Apunta a Uso
Zonal (GCE_VM_IP_PORT) IP:puerto de VM o pods GKE con Ingress/Gateway nativo
Sin servidor Cloud Run, Cloud Functions, App Engine El caso de arriba
Internet (INTERNET_FQDN_PORT) Un servicio externo por nombre Poner CDN o Armor delante de un origen ajeno
Híbrido Un servidor local Migraciones (07-03)

  1. Registro y métricas del balanceador

Sin observabilidad, un balanceador es una caja negra. Actívalo desde el principio:

gcloud compute backend-services update bs-catalogo-web \
  --global \
  --enable-logging \
  --logging-sample-rate=1.0

--logging-sample-rate=1.0 registra el 100 % de las peticiones. En un sitio con tráfico alto se baja a 0.1 para contener el coste de logs; en AlpinaShop, con picos de 20 req/s, el 100 % es perfectamente asumible y facilita mucho el diagnóstico.

Consultas útiles:

# Peticiones que terminaron en 5xx en la última hora
gcloud logging read '
  resource.type="http_load_balancer"
  AND httpRequest.status>=500
' --project=alpinashop-prod --limit=20 --freshness=1h \
  --format="table(
    timestamp,
    httpRequest.requestUrl,
    httpRequest.status,
    jsonPayload.statusDetails
  )"

El campo jsonPayload.statusDetails es el más valioso del log del balanceador. Traduce el 502 genérico a una causa concreta:

statusDetails Significado Qué mirar
failed_to_pick_backend Ningún backend saludable Comprobaciones de estado (apartado 9)
backend_timeout El backend no respondió a tiempo --timeout y el rendimiento de la app
backend_connection_closed_before_data_sent_to_client El backend cortó la conexión Keepalive de gunicorn menor que el del balanceador
response_sent_by_backend Todo normal El código lo devolvió la aplicación
denied_by_security_policy Bloqueado por Cloud Armor 03-05
client_disconnected_before_any_response El cliente se fue Normal en tráfico móvil

El caso de backend_connection_closed_before_data_sent_to_client merece un apunte porque es sutil y muy común: el balanceador global mantiene conexiones persistentes con el backend durante 10 minutos. Si gunicorn cierra la conexión antes (su keepalive por defecto son 2 segundos), el balanceador se encuentra la conexión cerrada a mitad y devuelve 502 esporádicos, imposibles de reproducir a mano. La solución es configurar gunicorn con un keepalive superior al del balanceador:

gunicorn --bind 0.0.0.0:8080 --workers 4 --keep-alive 620 app:app

Métricas recomendadas para el panel de Marta (se construye en 06-04):

Métrica Para qué
loadbalancing.googleapis.com/https/request_count Volumen, por código de respuesta
loadbalancing.googleapis.com/https/total_latencies Latencia extremo a extremo (p50, p95, p99)
loadbalancing.googleapis.com/https/backend_latencies Latencia solo del backend: separa el problema
loadbalancing.googleapis.com/https/backend_request_count Peticiones que llegaron al backend (frente a las servidas por CDN)

La diferencia entre total_latencies y backend_latencies es la que dice si el problema es tuyo o de la red: si la total sube y la del backend no, el problema está en el trayecto hasta el cliente.

Errores Comunes y Consejos

  • Olvidar el firewall de 130.211.0.0/22 y 35.191.0.0/16. Es la causa número uno de un balanceador que devuelve 502 recién creado. Antes de depurar nada más, comprueba esa regla.
  • No definir el puerto con nombre en el MIG. El servicio de backend usa --port-name, y si el grupo no lo declara, el tráfico va al puerto 80 y nada responde.
  • Impacientarse. Un balanceador global tarda entre 5 y 10 minutos en propagarse. Los primeros 404 y 502 son normales.
  • Que /salud consulte la base de datos. Convierte un fallo parcial en una caída total: todos los backends se marcan no saludables a la vez.
  • Usar la misma comprobación para el balanceador y para la autorreparación del MIG. Un fallo transitorio puede provocar que el grupo destruya y recree toda la flota.
  • Confundir EXTERNAL con EXTERNAL_MANAGED. Para desarrollos nuevos usa EXTERNAL_MANAGED; mezclarlos entre el servicio de backend y la regla de reenvío produce errores de validación confusos.
  • Olvidar el urlRewrite del backend de bucket. Si el mapa envía /imagenes/* al bucket sin reescribir, se busca un objeto cuyo nombre empieza por imagenes/ y se obtiene 404 sistemático.
  • Activar afinidad de sesión "por si acaso". Degrada el reparto y esconde un problema de diseño. Si la aplicación necesita afinidad, lo que hay que arreglar es el estado, no el balanceador.
  • max-rate-per-instance puesto a ojo. Mídelo con una prueba de carga sobre una instancia y aplica un margen del 30-40 %.
  • No activar el registro del balanceador. Sin statusDetails estás depurando a ciegas.
  • keepalive de gunicorn demasiado bajo. Provoca 502 esporádicos irreproducibles. Ponlo por encima de los 600 segundos del balanceador.
  • No borrar los recursos de una prueba. La cadena tiene siete objetos; se borran en orden inverso al de creación. Las IP estáticas huérfanas se siguen facturando.

Ejercicios

Ejercicio 1 — Elegir el balanceador correcto

Para cada escenario de AlpinaShop, indica qué balanceador corresponde y justifícalo en una línea:

  1. La tienda pública alpinashop.example, con clientes en toda Europa, HTTPS, caché de imágenes y WAF.
  2. Una API interna de stock que solo consumen las instancias del catálogo dentro de alpinashop-vpc.
  3. Un servicio de sincronización con el almacén que habla un protocolo propio sobre TCP en el puerto 9000 y necesita ver la IP real del cliente.
  4. Un panel interno de informes para Lucía que enruta /informes/* a un servicio y /exportar/* a otro, accesible solo desde la red interna.

Ejercicio 2 — Depurar un balanceador roto

Marta ha construido el balanceador siguiendo la lección, pero curl http://$IP/ devuelve 502 Server Error y gcloud compute backend-services get-health bs-catalogo-web --global muestra todas las instancias en UNHEALTHY. Se sabe que:

  • Desde otra VM de sn-web-euw1, curl http://10.10.0.7:8080/salud devuelve 200 OK.
  • La regla fw-allow-lb-health-web existe y permite tcp:80,tcp:8080 desde los rangos correctos hacia la etiqueta web-catalogo.

Escribe la secuencia de comandos de diagnóstico y la causa más probable.

Ejercicio 3 — Ampliar el mapa de URL

AlpinaShop quiere tres cosas nuevas:

  1. Que blog.alpinashop.example vaya a un servicio de backend distinto llamado bs-blog.
  2. Que /api/* tenga un tiempo de espera de 120 segundos porque hay una exportación lenta, sin cambiar el del resto del sitio.
  3. Que /descargas/manual-mochilas.pdf redirija de forma permanente a /imagenes/documentos/manual-mochilas-2026.pdf.

Indica qué recursos hay que crear o modificar para cada punto y escribe el fragmento de YAML del mapa de URL para el punto 3.


Soluciones

Solución 1

  1. Balanceador de aplicaciones externo global. Es tráfico HTTP público mundial, y es el único que admite Cloud CDN y Cloud Armor con IP anycast global.
  2. Balanceador de aplicaciones interno (INTERNAL_MANAGED), regional en europe-west1. No debe tener IP pública y el enrutamiento por ruta puede ser útil más adelante. Requiere subred de solo proxy.
  3. Balanceador de red de paso a través externo (capa 4, regional). Es el único que no termina la conexión y por tanto conserva la IP de origen real; además, el protocolo no es HTTP, así que un balanceador de capa 7 no sirve.
  4. Balanceador de aplicaciones interno. Necesita enrutamiento por ruta (capa 7) y no debe ser accesible desde internet. Si además hiciera falta acceso desde fuera de la oficina, la alternativa correcta sería el balanceador externo global protegido con IAP (03-04), no abrirlo.

Solución 2

# 1) ¿Están las instancias etiquetadas como web-catalogo?
gcloud compute instances list \
  --filter="name~alpinashop-web-mig" \
  --format="table(name, zone, status, tags.items.list())"

# 2) ¿Tiene el MIG definido el puerto con nombre?
gcloud compute instance-groups managed describe alpinashop-web-mig \
  --region=europe-west1 \
  --format="value(namedPorts)"

# 3) ¿A qué puerto y ruta apunta la comprobación?
gcloud compute health-checks describe hc-catalogo \
  --format="yaml(type, httpHealthCheck)"

# 4) ¿Qué port-name usa el servicio de backend?
gcloud compute backend-services describe bs-catalogo-web --global \
  --format="value(portName, protocol, timeoutSec)"

Causa más probable: falta el puerto con nombre. El firewall está bien y la aplicación responde en el 8080 desde dentro de la subred, así que el paquete de comprobación sí puede llegar. Lo que ocurre es que el servicio de backend pide el puerto llamado http-catalogo y el grupo de instancias no lo tiene definido, con lo que el balanceador dirige la comprobación y el tráfico al puerto por defecto (80), donde no hay nada escuchando.

gcloud compute instance-groups managed set-named-ports alpinashop-web-mig \
  --region=europe-west1 \
  --named-ports=http-catalogo:8080

La segunda causa candidata sería que la plantilla de instancia del MIG no aplique la etiqueta web-catalogo a las máquinas nuevas: la regla existiría pero no se aplicaría a nadie. Lo confirma el comando 1.

Solución 3

Punto 1 — blog.alpinashop.example. Se crea el servicio de backend bs-blog y se añade al mapa una hostRule nueva con su propio pathMatcher. Además hay que incluir el nombre en el certificado gestionado (03-07) y crear el registro DNS correspondiente.

gcloud compute url-maps add-path-matcher alpinashop-url-map \
  --path-matcher-name=matcher-blog \
  --default-service=bs-blog \
  --new-hosts=blog.alpinashop.example

Punto 2 — tiempo de espera de /api/*. El timeout es una propiedad del servicio de backend, no de la regla de ruta. Por tanto hay que crear un segundo servicio de backend (por ejemplo bs-catalogo-api) apuntando al mismo MIG pero con --timeout=120s, y enrutar /api/* hacia él.

gcloud compute backend-services create bs-catalogo-api \
  --global --load-balancing-scheme=EXTERNAL_MANAGED \
  --protocol=HTTP --port-name=http-catalogo \
  --health-checks=hc-catalogo --timeout=120s

gcloud compute backend-services add-backend bs-catalogo-api \
  --global --instance-group=alpinashop-web-mig \
  --instance-group-region=europe-west1 \
  --balancing-mode=RATE --max-rate-per-instance=80

Punto 3 — redirección permanente. Se hace en el propio mapa de URL, sin tocar la aplicación:

pathMatchers:
- name: matcher-principal
  defaultService: .../backendServices/bs-catalogo-web
  pathRules:
  - paths:
    - /descargas/manual-mochilas.pdf
    urlRedirect:
      pathRedirect: /imagenes/documentos/manual-mochilas-2026.pdf
      redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
      stripQuery: true

Nota: una pathRule con urlRedirect no lleva service. Y ojo con el orden: las rutas más específicas deben poder ganar a las genéricas; el balanceador aplica la coincidencia más larga, así que /descargas/manual-mochilas.pdf gana a /descargas/*.

Conclusión

AlpinaShop ya no depende de una máquina. Has entendido el catálogo de balanceadores de Google Cloud reduciéndolo a tres preguntas —capa 7 o capa 4, externo o interno, global o regional— y sabes que para tráfico web público la respuesta casi siempre es el balanceador de aplicaciones externo global, con la distinción clara entre proxy y paso a través.

Has construido la cadena completa de dentro hacia fuera: la comprobación hc-catalogo contra /salud en el 8080, el puerto con nombre http-catalogo en el MIG, el servicio de backend bs-catalogo-web con drenaje de conexiones y modo RATE a 80 req/s por instancia, el backend de bucket bb-catalogo-imagenes que sirve las imágenes de alpinashop-catalogo sin tocar la aplicación, el mapa alpinashop-url-map que envía /imagenes/* y /estatico/* al bucket con pathPrefixRewrite y el resto al MIG, y la regla de reenvío sobre la IP global alpinashop-lb-ip que reservamos en 03-01.

Has aprendido a diagnosticar lo único que se rompe de verdad: las comprobaciones de estado, con su lista de ocho causas encabezada por los rangos 130.211.0.0/22 y 35.191.0.0/16, y la distinción entre la comprobación del balanceador y la de autorreparación del MIG. Sabes ajustar capacidad con max-rate-per-instance medido, vaciar un backend con capacity-scaler, y por qué AlpinaShop no necesita afinidad de sesión: el estado vive en Firestore, no en las instancias. Has redirigido HTTP a HTTPS en el borde, has visto por qué no hace falta precalentar nada de cara a la campaña de otoño, dónde encajan los balanceadores internos y su subred de solo proxy, y cómo un NEG sin servidor permitirá mover el catálogo a Cloud Run (DA-001) cambiando solo el mapa de URL, conservando IP, dominio, certificado, caché y WAF. Y has activado el registro, que convierte un 502 opaco en un statusDetails accionable.

Queda una ineficiencia evidente. Cada vez que un cliente de Lisboa abre una ficha de producto, las imágenes viajan desde europe-west1 hasta Portugal, aunque sean los mismos bytes que se enviaron hace un minuto a otro cliente. Con 60 GB de catálogo y clientes repartidos por toda Europa, eso es latencia innecesaria y una factura de salida de datos que crece con cada visita. En la siguiente lección, 03-03, Cloud CDN, activamos la caché sobre el mismo servicio de backend que acabamos de construir: las imágenes pasarán a servirse desde el punto de presencia más cercano al cliente, aprenderás a controlar qué se cachea y durante cuánto tiempo, a evitar que los parámetros utm_* de las campañas arruinen el ratio de aciertos, y a medir el ahorro real en latencia y en euros.

Curso de Google Cloud Platform (GCP)

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

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados