Cerrábamos la lección anterior con una conclusión: la inversión en App Engine no se acumula fuera de App Engine, pero la inversión en contenedores sirve en todas partes. Kubernetes es el estándar de facto para orquestar contenedores, y Google Kubernetes Engine es la implementación gestionada de Google —significativamente, la casa donde nació Kubernetes, derivado del sistema interno Borg.
Kubernetes tiene fama de complejo, y en parte es merecida. Pero esa complejidad responde a un problema real: cuando tienes decenas de contenedores repartidos en varias máquinas, alguien tiene que decidir dónde se ejecuta cada uno, reiniciar los que fallan, repartir el tráfico, actualizar sin cortar el servicio y crecer cuando llega un pico. Si no lo hace Kubernetes, lo haces tú a mano.
En esta lección aprenderás Kubernetes desde cero con lo justo y suficiente, crearás el clúster alpinashop-cluster, publicarás la imagen del catálogo Flask en Artifact Registry y desplegarás alpinashop-web con manifiestos completos, escalado automático, configuración, secretos y actualizaciones sin corte con su rollback.
Contenido
- Kubernetes en diez minutos: los objetos que importan
- Nodos y plano de control
- Qué añade GKE
- Autopilot frente a Standard
- Crear
alpinashop-clustery conectarkubectl - Construir y publicar la imagen en Artifact Registry
- El Deployment de
alpinashop-web - El Service: exponer la aplicación
- Escalado: manual, HPA y autoescalado del clúster
- ConfigMaps y Secrets
- Actualizaciones sin corte y rollback
- Observabilidad básica del clúster
- Kubernetes en diez minutos: los objetos que importan
Kubernetes es un sistema declarativo: tú describes el estado deseado en ficheros YAML y el sistema trabaja continuamente para que la realidad coincida con esa descripción. Si pides tres réplicas y una muere, Kubernetes crea otra. No das órdenes; declaras objetivos.
Los objetos imprescindibles:
| Objeto | Qué es | Analogía |
|---|---|---|
| Contenedor | Un proceso empaquetado con todas sus dependencias | La aplicación y su entorno, en una caja |
| Pod | La unidad mínima que Kubernetes despliega: uno o varios contenedores que comparten red y almacenamiento | Un "servidor lógico" desechable |
| ReplicaSet | Garantiza que existan N pods iguales | El que cuenta y repone |
| Deployment | Gestiona ReplicaSets y orquesta las actualizaciones | Lo que tú escribes de verdad |
| Service | Un nombre y una IP estables que reparten tráfico entre pods | El balanceador interno |
| Ingress | Enrutamiento HTTP(S) desde fuera hacia varios Services | El proxy inverso |
| Namespace | Partición lógica del clúster | Carpeta con permisos y cuotas |
| ConfigMap | Configuración no sensible | Fichero de configuración |
| Secret | Datos sensibles (con matices, apartado 10) | Sobre cerrado, no blindado |
| Node | Una máquina (VM) que ejecuta pods | El servidor físico |
Tres ideas que hay que interiorizar antes de escribir un YAML:
- Los pods son efímeros y desechables. Nacen, mueren y se recrean con otro nombre y otra IP. Nunca te conectes a un pod por su IP: para eso está el Service. Es la misma lección de las instancias del MIG en 02-01, llevada al extremo.
- Casi nunca creas pods directamente. Creas un Deployment, que crea un ReplicaSet, que crea los pods. Cada capa añade una garantía.
- Todo se identifica por labels. Un Service no conoce a sus pods por nombre: selecciona los que llevan una etiqueta determinada. Si el selector no coincide con las etiquetas de los pods, el Service existe pero no envía tráfico a nadie. Es el error número uno de los principiantes.
graph TD
subgraph "Plano de control (gestionado por Google)"
API[API Server]
SCHED[Scheduler]
CM[Controller Manager]
ETCD[(etcd)]
end
subgraph "Nodos (VMs de Compute Engine)"
subgraph "Nodo 1"
P1[Pod alpinashop-web]
P2[Pod alpinashop-web]
end
subgraph "Nodo 2"
P3[Pod alpinashop-web]
end
end
DEP[Deployment<br/>alpinashop-web] --> RS[ReplicaSet]
RS --> P1
RS --> P2
RS --> P3
SVC[Service LoadBalancer<br/>alpinashop-web] --> P1
SVC --> P2
SVC --> P3
KUBECTL[kubectl] --> API
API --> SCHED
API --> CM
API --> ETCD
INTERNET((Internet)) --> SVC
- Nodos y plano de control
Un clúster de Kubernetes tiene dos mitades:
El plano de control es el cerebro. Contiene el API Server (la única puerta de entrada: kubectl y todo lo demás hablan con él), el Scheduler (decide en qué nodo va cada pod), los Controller Managers (los bucles que comparan estado real con estado deseado y actúan) y etcd (la base de datos que guarda todo el estado del clúster).
Los nodos son las máquinas que ejecutan los pods. En GKE son VM de Compute Engine —las mismas de la lección 02-01—, cada una con el kubelet (el agente que habla con el plano de control y arranca contenedores), un runtime de contenedores y kube-proxy (que implementa la red de los Services).
Montar y mantener un plano de control por tu cuenta es un trabajo considerable: alta disponibilidad de etcd, certificados, actualizaciones coordinadas, copias de seguridad. Esa es exactamente la parte que GKE te quita.
- Qué añade GKE
Sobre Kubernetes vainilla, GKE aporta:
- Plano de control gestionado y con SLA. Google lo despliega, lo replica, lo parchea y lo actualiza. En modo regional, replicado en varias zonas.
- Actualizaciones automáticas de plano de control y nodos, con canales de versión (
rapid,regular,stable) para elegir cuánta novedad quieres. - Reparación automática de nodos: un nodo que deja de responder se recrea.
- Autoescalado de nodos: si no caben más pods, se añaden nodos; si sobran, se retiran.
- Integración con la red de Google: los Services de tipo LoadBalancer crean balanceadores nativos de Google Cloud, y los pods reciben IP de la VPC (03-01).
- Integración con IAM y Workload Identity: los pods se autentican ante las APIs de Google Cloud con una identidad propia, sin claves.
- Cloud Logging y Cloud Monitoring integrados desde el primer momento.
- Autopilot: un modo donde ni siquiera ves los nodos.
- Autopilot frente a Standard
Es la primera decisión al crear un clúster, y condiciona el día a día.
| Autopilot | Standard | |
|---|---|---|
| ¿Quién gestiona los nodos? | Google, por completo | Tú: tamaño, número, imagen, pools |
| ¿Ves las VM? | No | Sí, en Compute Engine |
| Facturación | Por CPU, memoria y disco solicitados por tus pods | Por las VM de los nodos, se usen o no |
| Escalado de nodos | Automático e invisible | Autoescalador configurable por pool |
| Seguridad | Endurecido por defecto (sin privilegios, sin acceso al host) | Configurable, más permisivo |
| DaemonSets y acceso al host | Limitado | Permitido |
| GPU y hardware especial | Soportado con restricciones | Control total |
| Sobrecarga operativa | Mínima | Media-alta |
| Cuándo elegirlo | Caso general, equipos pequeños, aplicaciones estándar | Necesidades específicas de hardware, agentes con privilegios, ajuste fino de costes a gran escala |
La diferencia de facturación es la clave para entenderlo. En Standard pagas los nodos: si tienes tres VM e2-standard-4 y tus pods usan el 15 %, pagas el 100 %. En Autopilot pagas lo que tus pods solicitan: si un pod pide 250 mCPU y 512 MiB, eso es lo que se factura. Esto tiene dos consecuencias directas: los requests de tus manifiestos dejan de ser una recomendación y pasan a ser tu factura, y no hay incentivo para "rellenar" nodos.
AlpinaShop elige Autopilot. Marta es la única responsable de infraestructura de una empresa de 40 personas; no tiene tiempo para dimensionar pools de nodos ni para ajustar el binpacking. La aplicación es un contenedor web estándar sin requisitos especiales. Autopilot elimina toda una categoría de trabajo a cambio de un sobrecoste por unidad que, a esta escala, es irrelevante frente a las horas de Marta.
- Crear
alpinashop-cluster y conectar kubectl
alpinashop-cluster y conectar kubectlgcloud services enable container.googleapis.com artifactregistry.googleapis.com
gcloud container clusters create-auto alpinashop-cluster \
--project=alpinashop-prod \
--region=europe-west1 \
--release-channel=regular \
--labels=entorno=prod,equipo=plataforma,centro-coste=tienda,aplicacion=catalogoNotas sobre el comando:
create-autocrea un clúster Autopilot. Para Standard seríacreatecon toda la configuración de pools de nodos.--region(no--zone): los clústeres Autopilot son siempre regionales, con el plano de control replicado entre zonas. Es la elección correcta para producción y coherente con el razonamiento de 01-05.--release-channel=regular: versiones estables con actualizaciones automáticas.stablees más conservador yrapidda acceso anticipado a versiones nuevas.
La creación tarda entre 5 y 10 minutos. Después hay que decirle a kubectl con qué clúster hablar:
# Instalar el plugin de autenticacion si no esta (Cloud Shell ya lo trae)
gcloud components install gke-gcloud-auth-plugin
# Obtener credenciales: escribe la configuracion en ~/.kube/config
gcloud container clusters get-credentials alpinashop-cluster \
--region=europe-west1 --project=alpinashop-prod
# Comprobar la conexion
kubectl cluster-info
kubectl get nodes
kubectl get namespacesEn Autopilot, kubectl get nodes puede devolver una lista vacía al principio: los nodos aparecen cuando hay pods que ejecutar. Es desconcertante la primera vez y es exactamente el comportamiento esperado.
Creamos un namespace propio, en lugar de trabajar en default:
Los namespaces permiten separar entornos y equipos dentro de un clúster, con cuotas de recursos y permisos RBAC propios. Usar default para todo es un mal hábito que se paga cuando el clúster crece.
- Construir y publicar la imagen en Artifact Registry
Kubernetes ejecuta imágenes de contenedor, así que hay que empaquetar el catálogo Flask. Artifact Registry es el registro de artefactos de Google Cloud —sucesor de Container Registry, que está retirado; si encuentras documentación con gcr.io, está desactualizada.
gcloud artifacts repositories create alpinashop \
--repository-format=docker \
--location=europe-west1 \
--description="Imagenes de contenedor de AlpinaShop"
# Configurar Docker para autenticarse contra este registro
gcloud auth configure-docker europe-west1-docker.pkg.devEl Dockerfile del catálogo:
# Imagen base slim: mas pequeña que la completa, con menos superficie de ataque
FROM python:3.12-slim
# Evita que Python escriba .pyc y fuerza salida sin buffer,
# imprescindible para que los logs lleguen a Cloud Logging en tiempo real.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Copiar primero SOLO requirements.txt: si no cambia, Docker reutiliza
# la capa de dependencias en las siguientes construcciones. Es la
# optimizacion de cache mas rentable de un Dockerfile.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Ahora el codigo, que cambia en cada commit
COPY . .
# Usuario sin privilegios: Autopilot RECHAZA contenedores que
# intenten ejecutarse como root.
RUN useradd --create-home --uid 1000 alpina && chown -R alpina:alpina /app
USER 1000
EXPOSE 8080
# 2 workers y 4 hilos: la app espera mucho a la base de datos,
# asi que los hilos aprovechan bien ese tiempo muerto.
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", \
"--threads", "4", "--timeout", "60", "main:app"]Construcción y publicación. Hay dos caminos:
# Opcion A: construir en local con Docker
IMAGEN="europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v1"
docker build -t "$IMAGEN" .
docker push "$IMAGEN"
# Opcion B: construir en la nube con Cloud Build (no requiere Docker local)
gcloud builds submit --tag "$IMAGEN" .La opción B es especialmente cómoda desde Cloud Shell y es el germen del pipeline de integración continua que montaremos en 06-01.
Verifica lo publicado:
gcloud artifacts docker images list \
europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop
# Analisis de vulnerabilidades (si esta activado en el repositorio)
gcloud artifacts docker images scan "$IMAGEN"Dos buenas prácticas de etiquetado: nunca uses :latest en producción —no sabrás qué versión está corriendo ni podrás hacer rollback— y etiqueta con el hash del commit (catalogo:a3f9c1d), que hace trazable qué código está en cada pod. Es la misma idea de nombres inmutables que aplicamos a los objetos de Storage en 02-02 y a las plantillas de instancia en 02-01.
- El Deployment de
alpinashop-web
alpinashop-webAhora el manifiesto principal. Cada bloque está comentado:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: alpinashop-web
namespace: tienda
labels:
app: alpinashop-web
entorno: prod
spec:
# Numero de pods deseado. El HPA (apartado 9) lo sobrescribira despues.
replicas: 3
# El Deployment gobierna los pods que coinciden con este selector.
# DEBE coincidir con las labels de la plantilla de abajo.
selector:
matchLabels:
app: alpinashop-web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # hasta 1 pod extra durante la actualizacion
maxUnavailable: 0 # nunca bajar del numero deseado: cero corte
template:
metadata:
labels:
app: alpinashop-web # etiqueta que usara el Service
entorno: prod
spec:
# Cuenta de servicio de Kubernetes vinculada por Workload Identity
# a una cuenta de servicio de Google Cloud (apartado 10).
serviceAccountName: sa-catalogo
containers:
- name: catalogo
image: europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v1
ports:
- name: http
containerPort: 8080
# requests: lo que el planificador reserva. En Autopilot, LO QUE PAGAS.
# limits: el techo. Si se supera la memoria, el contenedor muere (OOMKilled).
resources:
requests:
cpu: "250m" # 0,25 de una vCPU
memory: "512Mi"
ephemeral-storage: "1Gi"
limits:
cpu: "500m"
memory: "512Mi" # igual que requests: evita sorpresas de memoria
env:
- name: BUCKET_CATALOGO
value: "alpinashop-catalogo"
- name: ENTORNO
valueFrom:
configMapKeyRef:
name: config-catalogo
key: entorno
- name: INSTANCIA_SQL
valueFrom:
configMapKeyRef:
name: config-catalogo
key: instancia_sql
- name: DB_USER
valueFrom:
configMapKeyRef:
name: config-catalogo
key: db_user
- name: DB_PASS
valueFrom:
secretKeyRef:
name: secreto-catalogo
key: db_pass
# ¿Esta vivo el contenedor? Si falla, Kubernetes lo REINICIA.
livenessProbe:
httpGet:
path: /salud
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
failureThreshold: 3
# ¿Esta listo para recibir trafico? Si falla, se le RETIRA el trafico
# pero NO se reinicia. Es la sonda que evita servir errores durante
# el arranque o una sobrecarga puntual.
readinessProbe:
httpGet:
path: /salud
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
# Endurecimiento exigido por Autopilot
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# Reparte los pods entre zonas: si cae europe-west1-b, la tienda sigue.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: alpinashop-webLos puntos que más se equivocan:
selector.matchLabelsdebe coincidir contemplate.metadata.labels. Si no, el Deployment no reconoce a sus propios pods.requestsfrente alimits.requestses lo que el planificador reserva y, en Autopilot, lo que facturas.limitses el techo: superar el límite de memoria mata el contenedor conOOMKilled. Igualar memoria de request y limit evita sorpresas.- Liveness y readiness no son lo mismo. La primera reinicia; la segunda solo retira tráfico. Una liveness mal configurada —por ejemplo, apuntando a una ruta que consulta la base de datos— provoca reinicios en cadena cuando la base de datos va lenta, empeorando el problema. Mantén
/saludligera y sin dependencias externas. maxUnavailable: 0garantiza que la capacidad nunca baja durante un despliegue.
Aplicar y comprobar:
kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods -o wide # muestra en qué nodo y zona cae cada pod
kubectl describe pod <nombre-del-pod>
kubectl logs -f deployment/alpinashop-web
- El Service: exponer la aplicación
Los pods tienen IP efímeras y cambiantes. Un Service ofrece un nombre DNS y una IP virtual estables que reparten tráfico entre los pods que coincidan con su selector.
| Tipo de Service | Alcance | Uso |
|---|---|---|
ClusterIP (por defecto) |
Solo dentro del clúster | Comunicación entre servicios internos |
NodePort |
Puerto en cada nodo | Raro de usar directamente |
LoadBalancer |
IP pública externa | Exponer un servicio a internet |
ExternalName |
Alias DNS a un host externo | Integración con servicios de fuera |
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: alpinashop-web
namespace: tienda
annotations:
# Balanceador nativo por IP de pod: el trafico va directo al pod,
# sin salto adicional por el nodo. Menos latencia y mejor health checking.
cloud.google.com/neg: '{"ingress": true}'
spec:
type: LoadBalancer
selector:
app: alpinashop-web # DEBE coincidir con las labels de los pods
ports:
- name: http
protocol: TCP
port: 80 # puerto expuesto al exterior
targetPort: 8080 # puerto del contenedorkubectl apply -f service.yaml
# La IP externa tarda 1-2 minutos en aparecer
kubectl get service alpinashop-web --watch
IP=$(kubectl get service alpinashop-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -s "http://$IP/"Un Service de tipo LoadBalancer en GKE crea automáticamente un balanceador de red de Google Cloud. Es la forma más rápida de exponer algo, pero para una tienda real querrás HTTPS, un dominio propio, enrutamiento por ruta y protección frente a ataques. Eso se consigue con un Ingress o un Gateway delante de un balanceador HTTP(S) global, junto con Cloud Armor y certificados gestionados: contenido de las lecciones 03-02, 03-05 y 03-07. Aquí paramos deliberadamente en el LoadBalancer de tipo L4.
Dentro del clúster, cualquier pod puede llamar a este servicio por su nombre DNS:
http://alpinashop-web.tienda.svc.cluster.local http://alpinashop-web # forma corta, dentro del mismo namespace
- Escalado: manual, HPA y autoescalado del clúster
Kubernetes escala en dos niveles independientes: número de pods y número de nodos.
Escalado manual de pods:
HorizontalPodAutoscaler (HPA): ajusta las réplicas automáticamente según una métrica.
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: alpinashop-web
namespace: tienda
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: alpinashop-web
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # % sobre el REQUEST de CPU, no sobre el limit
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # reacciona rapido a los picos
policies:
- type: Percent
value: 100 # como maximo, duplicar cada 30 s
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # baja despacio: evita oscilaciones
policies:
- type: Pods
value: 1
periodSeconds: 60 # retirar como mucho 1 pod por minutokubectl apply -f hpa.yaml
kubectl get hpa alpinashop-web --watch
kubectl describe hpa alpinashop-web # muestra por qué escaló o noEl bloque behavior es lo que separa un HPA que funciona de uno que oscila. La asimetría es deliberada: subir rápido y bajar despacio. Un pico de tráfico requiere capacidad inmediata; retirar capacidad con prisa provoca flapping, con pods creándose y destruyéndose sin parar. La ventana de 300 segundos a la baja es una recomendación muy razonable como punto de partida.
Cuidado con un detalle: el porcentaje del HPA se calcula sobre el request, no sobre el límite. Con requests.cpu: 250m y objetivo del 60 %, el HPA escala cuando el uso medio supera 150 mCPU por pod. Un request mal puesto descoloca todo el autoescalado.
Autoescalado del clúster. En Autopilot es automático e invisible: si los pods nuevos no caben, Google añade capacidad; si sobra, la retira. No hay nada que configurar, que es precisamente su propuesta de valor. En Standard hay que configurarlo por pool:
gcloud container clusters update alpinashop-cluster-std \
--enable-autoscaling --min-nodes=1 --max-nodes=10 \
--node-pool=pool-principal --region=europe-west1Prueba de carga rápida para ver el HPA en acción:
kubectl run generador-carga --rm -it --image=busybox:1.36 --restart=Never -- \
/bin/sh -c "while true; do wget -q -O- http://alpinashop-web.tienda/; done"En otra terminal, kubectl get hpa --watch mostrará subir el uso de CPU y, tras unos segundos, aumentar el número de réplicas.
- ConfigMaps y Secrets
ConfigMap para configuración no sensible:
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: config-catalogo
namespace: tienda
data:
entorno: "produccion"
instancia_sql: "alpinashop-prod:europe-west1:alpinashop-pedidos"
db_user: "app_catalogo"
db_name: "tienda"
productos_por_pagina: "24"Secret para datos sensibles:
kubectl create secret generic secreto-catalogo \
--from-literal=db_pass='<contraseña>' \
--namespace=tiendaY aquí viene la advertencia más importante de este apartado: los Secrets de Kubernetes están codificados en base64, no cifrados. Cualquiera con permiso para leer secrets en ese namespace los ve en claro:
Base64 no es cifrado, es una codificación. Un Secret de Kubernetes protege frente a que la contraseña aparezca en un kubectl describe o en un manifiesto del repositorio, y nada más.
Lo correcto en producción es Secret Manager (lección 03-06), integrado con GKE mediante el complemento Secret Manager CSI driver, que monta los secretos como ficheros obtenidos en tiempo de ejecución, con rotación centralizada y auditoría. La regla práctica: los Secrets de Kubernetes valen para desarrollo y para datos de bajo impacto; las credenciales reales de producción viven en Secret Manager.
Workload Identity merece un párrafo propio porque resuelve el problema de raíz. Vincula una cuenta de servicio de Kubernetes con una de Google Cloud, de modo que los pods obtienen credenciales de Google Cloud automáticamente, sin ficheros de clave:
PROYECTO=alpinashop-prod
# 1. Cuenta de servicio de Google Cloud
gcloud iam service-accounts create sa-catalogo-gke \
--display-name="Catalogo en GKE"
gcloud projects add-iam-policy-binding $PROYECTO \
--member="serviceAccount:sa-catalogo-gke@$PROYECTO.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
gcloud storage buckets add-iam-policy-binding gs://alpinashop-catalogo \
--member="serviceAccount:sa-catalogo-gke@$PROYECTO.iam.gserviceaccount.com" \
--role="roles/storage.objectUser"
# 2. Cuenta de servicio de Kubernetes
kubectl create serviceaccount sa-catalogo --namespace=tienda
# 3. Vincular ambas
gcloud iam service-accounts add-iam-policy-binding \
"sa-catalogo-gke@$PROYECTO.iam.gserviceaccount.com" \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:$PROYECTO.svc.id.goog[tienda/sa-catalogo]"
kubectl annotate serviceaccount sa-catalogo --namespace=tienda \
iam.gke.io/gcp-service-account="sa-catalogo-gke@$PROYECTO.iam.gserviceaccount.com"Con esto, el mismo storage.Client() y el mismo conector de Cloud SQL de las lecciones anteriores funcionan dentro del pod sin ninguna credencial. Es la continuación natural de lo que ya viste en Compute Engine y App Engine: la identidad la da la plataforma, no un fichero.
- Actualizaciones sin corte y rollback
Con strategy: RollingUpdate y maxUnavailable: 0, actualizar la imagen sustituye los pods de uno en uno sin bajar la capacidad:
# Publicar la version nueva
gcloud builds submit \
--tag europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v2 .
# Actualizar el Deployment
kubectl set image deployment/alpinashop-web \
catalogo=europe-west1-docker.pkg.dev/alpinashop-prod/alpinashop/catalogo:v2
# Seguir el progreso
kubectl rollout status deployment/alpinashop-web
# Historial de revisiones
kubectl rollout history deployment/alpinashop-webEl proceso, paso a paso: se crea un pod con v2, se espera a que su readinessProbe dé el visto bueno, se retira un pod de v1, y se repite. Como maxUnavailable: 0, siempre hay al menos tres pods listos. Aquí se ve por qué la readinessProbe es imprescindible: sin ella, Kubernetes daría por buena la nueva versión en cuanto el contenedor arranca, antes de que gunicorn esté aceptando peticiones, y algunos clientes recibirían errores.
Si la versión nueva falla:
# Volver a la revision anterior
kubectl rollout undo deployment/alpinashop-web
# Volver a una revision concreta
kubectl rollout undo deployment/alpinashop-web --to-revision=3
# Pausar una actualizacion en curso (canary manual)
kubectl rollout pause deployment/alpinashop-web
kubectl rollout resume deployment/alpinashop-webPara proteger el servicio también durante operaciones de mantenimiento del clúster —actualizaciones de nodos, reescalados—, define un PodDisruptionBudget:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: alpinashop-web
namespace: tienda
spec:
minAvailable: 2
selector:
matchLabels:
app: alpinashop-webEsto le dice a Kubernetes que nunca deje menos de 2 pods disponibles cuando él decide mover pods. Sin un PDB, una actualización de nodos podría vaciar un nodo entero y dejar la tienda con menos capacidad de la necesaria en el peor momento.
- Observabilidad básica del clúster
GKE envía métricas y logs a Cloud Monitoring y Cloud Logging sin configuración adicional. Los comandos de diagnóstico de cada día:
# Estado general
kubectl get all -n tienda
kubectl get events -n tienda --sort-by=.metadata.creationTimestamp
# Consumo real de pods y nodos
kubectl top pods -n tienda
kubectl top nodes
# Logs
kubectl logs -f deployment/alpinashop-web
kubectl logs deployment/alpinashop-web --previous # logs del contenedor que murió
# Diagnostico de un pod que no arranca
kubectl describe pod <pod> # la seccion Events dice casi siempre por qué
# Abrir una shell dentro del contenedor
kubectl exec -it <pod> -- /bin/sh
# Probar el servicio sin exponerlo
kubectl port-forward service/alpinashop-web 8080:80Los estados de pod que verás con más frecuencia y qué significan:
| Estado | Causa habitual |
|---|---|
Pending |
No hay recursos para planificarlo; en Autopilot, se está añadiendo capacidad |
ImagePullBackOff |
La imagen no existe o falta permiso sobre Artifact Registry |
CrashLoopBackOff |
El contenedor arranca y muere en bucle. Mira kubectl logs --previous |
OOMKilled |
Superó limits.memory. Sube el límite o arregla la fuga |
Running pero no listo |
La readinessProbe falla; revisa la ruta y el puerto |
En Cloud Monitoring hay paneles predefinidos de GKE con uso por clúster, namespace y carga de trabajo, y se pueden definir alertas sobre reinicios de contenedor, pods no disponibles o latencia. El tratamiento completo está en 06-04 y 06-06.
Errores Comunes y Consejos
- Selector del Service que no coincide con las labels de los pods. El Service existe, no da error y no envía tráfico a nadie. Verifica con
kubectl get endpoints alpinashop-web. - No definir
requestsylimits. En Autopilot se aplican valores por defecto que probablemente no son los tuyos; en Standard, un pod sin límites puede ahogar a sus vecinos. - Igualar la liveness probe a una ruta que consulta la base de datos. Si la base va lenta, Kubernetes reinicia todos los pods y empeora el incidente.
- Usar la etiqueta
:latest. No sabes qué versión corre ni puedes hacer rollback fiable. - Guardar credenciales reales en Secrets de Kubernetes. Base64 no es cifrado. Usa Secret Manager.
- Ejecutar como root. Autopilot lo rechaza, y en Standard es un riesgo innecesario.
- Escalar a la baja demasiado rápido en el HPA. Provoca oscilaciones. Usa una ventana de estabilización amplia.
- Trabajar siempre en el namespace
default. Separa por entorno y equipo desde el principio. - Olvidar el PodDisruptionBudget. El mantenimiento del clúster puede dejarte sin capacidad.
- Consejo:
kubectl describeantes que ningún otro comando cuando algo no funciona; la sección Events suele dar la respuesta. - Consejo: etiqueta las imágenes con el hash del commit. Trazabilidad completa entre código y pod.
- Consejo: usa
kubectl apply -fsobre ficheros versionados en Git, nuncakubectl editen producción. Lo que no está en Git, no existe. - Consejo:
kubectl port-forwardte deja probar un servicio interno sin exponerlo a internet.
Ejercicios
Ejercicio 1: clúster, imagen y primer despliegue
- Crea un clúster Autopilot
alpinashop-cluster-ejeneurope-west1y conectakubectl. - Crea un repositorio de Artifact Registry en
europe-west1y publica una imagen de una aplicación Flask mínima que muestre el nombre del pod (variableHOSTNAME) y responda en/salud. - Escribe el Deployment con 2 réplicas, requests y limits razonables, sondas de liveness y readiness, y
securityContextsin root. - Escribe el Service de tipo LoadBalancer y comprueba con
curlque el reparto entre pods funciona. - Comprueba con
kubectl get endpointsque el Service tiene pods asociados.
Ejercicio 2: escalado y actualización sin corte
- Escala manualmente a 4 réplicas y comprueba en qué zonas caen los pods.
- Crea un HPA de 2 a 8 réplicas con objetivo de CPU al 60 % y comportamiento asimétrico.
- Genera carga y observa el escalado.
- Publica una
v2de la imagen con un cambio visible y actualiza el Deployment sin corte, comprobando concurlen bucle que no hay ningún error. - Haz rollback a la versión anterior y verifica el historial de revisiones.
Ejercicio 3: configuración, secretos y diagnóstico
- Crea un ConfigMap con el entorno y el número de productos por página, y un Secret con una contraseña ficticia.
- Inyecta ambos en el Deployment como variables de entorno y verifica su valor dentro del pod.
- Demuestra que el Secret no está cifrado.
- Provoca deliberadamente un
CrashLoopBackOff(por ejemplo, con un comando de arranque erróneo) y diagnostícalo paso a paso. - Explica qué usarías en producción en lugar de un Secret de Kubernetes y por qué.
Soluciones
Solución 1
gcloud container clusters create-auto alpinashop-cluster-ej \
--region=europe-west1 --release-channel=regular
gcloud container clusters get-credentials alpinashop-cluster-ej --region=europe-west1
kubectl create namespace tienda
kubectl config set-context --current --namespace=tienda
gcloud artifacts repositories create alpinashop \
--repository-format=docker --location=europe-west1# main.py
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
return f"<h1>AlpinaShop</h1><p>Pod: {os.environ.get('HOSTNAME', 'local')}</p>"
@app.route("/salud")
def salud():
return "ok", 200FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home --uid 1000 alpina && chown -R alpina:alpina /app
USER 1000
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "main:app"]IMG="europe-west1-docker.pkg.dev/$(gcloud config get-value project)/alpinashop/catalogo:v1"
gcloud builds submit --tag "$IMG" .# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: alpinashop-web
namespace: tienda
spec:
replicas: 2
selector:
matchLabels:
app: alpinashop-web
template:
metadata:
labels:
app: alpinashop-web
spec:
containers:
- name: catalogo
image: europe-west1-docker.pkg.dev/PROYECTO/alpinashop/catalogo:v1
ports:
- containerPort: 8080
resources:
requests: { cpu: "250m", memory: "512Mi" }
limits: { cpu: "500m", memory: "512Mi" }
livenessProbe:
httpGet: { path: /salud, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet: { path: /salud, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }kubectl apply -f deployment.yaml -f service.yaml
IP=$(kubectl get svc alpinashop-web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
for i in $(seq 1 10); do curl -s "http://$IP/" | grep -o "Pod: [a-z0-9-]*"; done
# 5. Comprobar que el Service tiene endpoints
kubectl get endpoints alpinashop-webSi kubectl get endpoints devuelve <none>, el selector del Service no coincide con las labels de los pods, o ningún pod está pasando la readinessProbe. Es la primera comprobación ante un Service que no responde.
Solución 2
# 1. Escalado manual y reparto por zonas
kubectl scale deployment alpinashop-web --replicas=4
kubectl get pods -o custom-columns=\
NOMBRE:.metadata.name,NODO:.spec.nodeName,ESTADO:.status.phase# 2. hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: alpinashop-web
namespace: tienda
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: alpinashop-web
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 60 }
behavior:
scaleUp:
stabilizationWindowSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300kubectl apply -f hpa.yaml
# 3. Generar carga y observar
kubectl run carga --rm -it --image=busybox:1.36 --restart=Never -- \
/bin/sh -c "while true; do wget -q -O- http://alpinashop-web.tienda/ >/dev/null; done"
# en otra terminal:
kubectl get hpa alpinashop-web --watch
# 4. Actualizacion sin corte, comprobando que no hay errores
gcloud builds submit --tag "${IMG%:v1}:v2" .
while true; do
curl -s -o /dev/null -w "%{http_code} " "http://$IP/"
sleep 0.3
done &
kubectl set image deployment/alpinashop-web catalogo="${IMG%:v1}:v2"
kubectl rollout status deployment/alpinashop-web
# 5. Rollback
kubectl rollout undo deployment/alpinashop-web
kubectl rollout history deployment/alpinashop-webDurante la actualización, el bucle de curl debe mostrar únicamente códigos 200. Si aparecieran 502 o 503, la causa casi siempre es una readinessProbe mal configurada o maxUnavailable mayor que cero.
Solución 3
# 1. ConfigMap y Secret
kubectl create configmap config-catalogo \
--from-literal=entorno=produccion \
--from-literal=productos_por_pagina=24
kubectl create secret generic secreto-catalogo \
--from-literal=db_pass='ClaveDePrueba123'# 2. Inyeccion en el Deployment
env:
- name: ENTORNO
valueFrom:
configMapKeyRef: { name: config-catalogo, key: entorno }
- name: PRODUCTOS_POR_PAGINA
valueFrom:
configMapKeyRef: { name: config-catalogo, key: productos_por_pagina }
- name: DB_PASS
valueFrom:
secretKeyRef: { name: secreto-catalogo, key: db_pass }kubectl apply -f deployment.yaml
kubectl exec -it deployment/alpinashop-web -- env | grep -E "ENTORNO|PRODUCTOS|DB_PASS"
# 3. El Secret no esta cifrado
kubectl get secret secreto-catalogo -o jsonpath='{.data.db_pass}' | base64 -d; echo
# 4. Provocar y diagnosticar un CrashLoopBackOff
kubectl set image deployment/alpinashop-web catalogo=python:3.12-slim
kubectl get pods
kubectl describe pod <pod> # Events: Back-off restarting failed container
kubectl logs <pod> --previous # salida del contenedor que murió
kubectl rollout undo deployment/alpinashop-webEl diagnóstico correcto sigue siempre este orden: kubectl get pods para ver el estado, kubectl describe pod para leer los Events (que indican si el problema es de imagen, de recursos o de sondas) y kubectl logs --previous para ver qué imprimió el contenedor justo antes de morir. En este caso, la imagen python:3.12-slim no tiene comando de arranque útil y termina de inmediato, con Kubernetes reintentando con retroceso exponencial.
- En producción usaría Secret Manager con el complemento CSI de GKE, o Workload Identity para eliminar directamente la credencial. Razones: los Secrets de Kubernetes solo están codificados en base64 y son legibles por cualquiera con permisos de lectura en el namespace; no tienen rotación automática; no dejan traza de auditoría de quién accedió; y si se versionan en Git, la credencial queda en el historial para siempre. Secret Manager cifra en reposo, controla el acceso con IAM granular, registra cada acceso y permite rotar sin volver a desplegar. Se estudia en 03-06.
Limpieza:
kubectl delete namespace tienda
gcloud container clusters delete alpinashop-cluster-ej --region=europe-west1 --quietUn clúster olvidado es de los recursos más caros que puedes dejar encendidos. Bórralo al terminar los ejercicios.
Conclusión
Has recorrido Kubernetes desde los conceptos hasta una aplicación real en producción. Sabes que es un sistema declarativo donde describes el estado deseado y el sistema converge hacia él, y conoces los objetos que importan: el pod como unidad efímera, el ReplicaSet que cuenta y repone, el Deployment que orquesta actualizaciones, el Service que da una identidad estable a un conjunto cambiante de pods, y los namespaces que particionan el clúster. Has entendido la separación entre plano de control y nodos, y por qué que Google gestione el primero es el núcleo del valor de GKE, junto con la reparación automática, el autoescalado de nodos, las actualizaciones por canales y la integración con la red y con IAM.
Has comparado Autopilot y Standard, y has visto que la diferencia esencial es qué se factura: los nodos en Standard, lo que solicitan tus pods en Autopilot. AlpinaShop eligió Autopilot porque Marta es una sola persona y la aplicación no tiene requisitos especiales de hardware. Has creado alpinashop-cluster regional, has conectado kubectl, has escrito un Dockerfile con caché de dependencias y usuario sin privilegios, y has publicado la imagen en Artifact Registry con etiquetas versionadas y nunca :latest. Has escrito el Deployment completo de alpinashop-web con requests y limits, sondas de liveness y readiness bien diferenciadas, contexto de seguridad y reparto entre zonas; y el Service de tipo LoadBalancer, sabiendo que HTTPS, dominio propio y enrutamiento avanzado llegarán en el módulo 3.
Has escalado a mano, has configurado un HorizontalPodAutoscaler con comportamiento asimétrico —rápido al subir, lento al bajar— y has comprobado que el porcentaje se calcula sobre el request. Has gestionado configuración con ConfigMaps y has descubierto que los Secrets de Kubernetes solo están codificados en base64, con Secret Manager y Workload Identity como respuesta correcta. Y has hecho una actualización sin un solo error 500 y su rollback en un comando, protegido además con un PodDisruptionBudget.
AlpinaShop ya tiene su cómputo resuelto en cuatro niveles distintos y sus datos transaccionales en Cloud SQL. Pero no todo encaja en una base de datos relacional: el carrito de la compra que cambia en cada clic, las sesiones de usuario, la telemetría de qué productos mira la gente. En 02-06, Bases de datos NoSQL: Firestore, Bigtable y Spanner, veremos por qué existen otros modelos de datos —documental, columnar ancho, relacional distribuido—, entenderemos consistencia y el teorema CAP sin dogmatismo, guardaremos el carrito de AlpinaShop en Firestore y su telemetría de clics en Bigtable, situaremos Spanner, Memorystore y BigQuery en un mapa comparativo, y cerraremos con un árbol de decisión y el reparto razonado de qué dato de AlpinaShop vive en cada sitio.
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
