Día 67 - Deploy de Guestbook App en Kubernetes (3-tier: PHP + Redis master + Redis slaves)
Problema / Desafío
El equipo de Nautilus terminó el desarrollo de una Guestbook app (clásica de los tutoriales de Kubernetes — frontend PHP + backend Redis con replicación master/slave). Hay que desplegarla en el cluster con su topología completa.
Requirements en bloques:
Back-end (Redis)
- Deployment
redis-master: replicas: 1- Container
master-redis-datacenter, imagenredis requests: CPU100m, memory100MicontainerPort: 6379- Service
redis-master: port/targetPort6379 - Deployment
redis-slave: replicas: 2- Container
slave-redis-datacenter, imagengcr.io/google_samples/gb-redisslave:v3 requests: CPU100m, memory100Mi- Env var
GET_HOSTS_FROM=dns containerPort: 6379- Service
redis-slave: port6379(selector → pods con labelapp: redis-slave) - Service
redis-follower: port/targetPort6379, selectorapp: redis-slave(alias del anterior)
Front-end (PHP)
- Deployment
frontend: replicas: 3- Container
php-redis-datacenter, imagengcr.io/google-samples/gb-frontend@sha256:a908df8486ff66f2c4daa0d3d8a2fa09846a1fc8efd65649c0109695c7c5cbff requests: CPU100m, memory100Mi- Env var
GET_HOSTS_FROM=dns containerPort: 80- Service
frontend: tipoNodePort, port80,nodePort: 30009
Conceptos clave
Arquitectura del stack
┌─────────────────────────────── cluster ───────────────────────────────────────┐
│ │
│ Cliente externo → nodePort 30009 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Service frontend (NodePort) │ │
│ │ port 80 → targetPort 80 │ │
│ └──────────┬───────────────────────────┘ │
│ │ selector: app=frontend │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Deployment frontend (3 replicas) │ │
│ │ image: gb-frontend@sha256:... │ │
│ │ GET_HOSTS_FROM=dns │ │
│ └──────────┬───────────────────────────┘ │
│ │ DNS lookup "redis-master" (writes) y "redis-slave" (reads) │
│ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Service redis-master │ │ Service redis-slave / redis-follower │ │
│ │ port 6379 │ │ port 6379 (dos nombres, mismos pods) │ │
│ └──────────┬───────────────┘ └────────────────┬─────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────────────────────┐ │
│ │ Deployment redis-master │ │ Deployment redis-slave (2 replicas) │ │
│ │ image: redis │ │ image: gb-redisslave:v3 │ │
│ │ 1 replica │ │ GET_HOSTS_FROM=dns │ │
│ └──────────────────────────┘ └──────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────┘
Patrón master/slave en Redis (replicación clásica)
| Rol | Cantidad | Operaciones aceptadas | Comportamiento |
|---|---|---|---|
| Master | 1 | Lectura y escritura | Recibe los writes; replica async hacia los slaves |
| Slave | N (típico 2+) | Solo lectura | Recibe el stream de replicación del master; sirve lecturas |
El frontend escribe a redis-master:6379 (SET, INCR, etc.) y lee de redis-slave:6379 (GET, LRANGE, etc.). Este patrón distribuye carga y permite scale horizontal de las lecturas.
Nota terminológica: Redis 5.0+ desaprecia el término "slave" en favor de "replica" (
REPLICAOFreemplazó aSLAVEOF). KodeKloud mantiene el viejo término en el lab para compatibilidad con el tutorial original de K8s, pero pide también el Serviceredis-follower— el nombre moderno del mismo concepto.
GET_HOSTS_FROM=dns — la pieza que ata el frontend con Redis
La imagen gb-frontend lee esta env var para decidir cómo descubrir los Redis. Dos modos posibles:
| Valor | Cómo descubre los hosts | Problema |
|---|---|---|
env (legacy) |
Lee REDIS_MASTER_SERVICE_HOST y REDIS_SLAVE_SERVICE_HOST (env vars que K8s inyecta automáticamente para cada Service del namespace) |
Requiere que el Service exista antes de que el Pod del cliente arranque |
dns (este lab) |
Hace gethostbyname("redis-master") y gethostbyname("redis-slave") — usa kube-dns |
Los Services pueden crearse en cualquier orden; el DNS se actualiza al instante |
dns es el modo moderno y robusto — funciona aunque los Services se creen después del Pod (kube-dns añade los registros al cluster DNS al instante).
Por qué dos Services (redis-slave + redis-follower) con el mismo selector
Los dos Services apuntan a los mismos Pods vía selector: app=redis-slave. Es un patrón de aliasing — el código viejo de la app busca redis-slave, el código nuevo busca redis-follower. K8s sirve los mismos endpoints bajo ambos nombres sin overhead adicional.
# Service A
metadata:
name: redis-slave
spec:
selector:
app: redis-slave # ← misma key/value
ports: [...]
---
# Service B
metadata:
name: redis-follower
spec:
selector:
app: redis-slave # ← misma key/value
ports: [...]
kubectl get endpoints sobre ambos Services devolvería las mismas IPs — solo cambia el nombre DNS por el que se accede.
Service Discovery por DNS — qué nombres resuelven
Dentro del cluster, los Services se acceden por:
<service-name>.<namespace>.svc.cluster.local
<service-name>.<namespace> # forma corta cuando el namespace está en search path
<service-name> # más corta — funciona si el cliente está en el mismo namespace
Para este lab, el frontend (en default) puede hacer:
redis-master # ← short form (mismo namespace)
redis-master.default # ← con namespace
redis-master.default.svc.cluster.local # ← FQDN completo
Todos resuelven a la misma ClusterIP del Service redis-master.
Glosario del manifest (lo nuevo)
| Campo | Significado |
|---|---|
Imagen por digest (@sha256:...) |
Pinning inmutable de la imagen. Garantiza que el binario sea exactamente el mismo aunque alguien re-tageé latest |
containerPort sin Service en el manifest |
El frontend Pod expone :80 aunque su Service mapea 80→80. Si se omite, el Service igual rutea porque K8s no valida que la app escuche en el puerto declarado |
selector distinto entre dos Services |
Permite aliasing — múltiples nombres DNS apuntando a los mismos Pods |
Pasos
- Aplicar el manifest del back-end:
- Deployment
redis-master - Service
redis-master - Aplicar el manifest del slave:
- Deployment
redis-slave - Service
redis-slave - Service
redis-follower(alias del anterior) - Aplicar el manifest del front-end:
- Deployment
frontend - Service
frontend(NodePort) - Validar con
kubectl get podsque los 6 Pods (1 master + 2 slaves + 3 frontend) esténRunning - Confirmar la página de Guestbook pegando al NodePort externo
Comandos / Código
1. Back-end: redis-master
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-master
labels:
app: redis-master-deployment
spec:
replicas: 1
selector:
matchLabels:
app: redis-master-deployment
template:
metadata:
labels:
app: redis-master-deployment
spec:
containers:
- name: master-redis-datacenter
image: redis:latest
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-master
labels:
app: redis-master
spec:
selector:
app: redis-master-deployment
ports:
- port: 6379
targetPort: 6379
2. Back-end: redis-slave + dos Services (slave + follower)
Detalle del lab: el requirement pide tres Services para Redis:
redis-master,redis-slaveyredis-follower. El primer envío del manifest omitió el Serviceredis-slavey solo creó dos (redis-masteryredis-follower). La aplicación cargó funcionalmente igual (HTTP 200), pero el validador del lab exige los tres por nombre. El fix consistió en agregar el Serviceredis-slavecon el mismo selectorapp: redis-slaveque elredis-follower— los dos terminan apuntando a los mismos Pods. Manifest correcto con los tres Services:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-slave
labels:
app: redis-slave-deployment
spec:
replicas: 2
selector:
matchLabels:
app: redis-slave
template:
metadata:
labels:
app: redis-slave
spec:
containers:
- name: slave-redis-datacenter
image: gcr.io/google_samples/gb-redisslave:v3
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: "dns"
ports:
- containerPort: 6379
---
# Service 1 — redis-slave (nombre clásico)
apiVersion: v1
kind: Service
metadata:
name: redis-slave
labels:
app: redis-slave
spec:
selector:
app: redis-slave
ports:
- port: 6379
targetPort: 6379
---
# Service 2 — redis-follower (alias moderno, mismo selector)
apiVersion: v1
kind: Service
metadata:
name: redis-follower
labels:
app: redis-follower
spec:
selector:
app: redis-slave
ports:
- port: 6379
targetPort: 6379
Después del fix (agregando el Service redis-slave que faltaba):
Validación de los tres Services Redis ya creados:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
redis-master ClusterIP 10.43.103.55 <none> 6379/TCP xx
redis-slave ClusterIP 10.43.37.187 <none> 6379/TCP xx
redis-follower ClusterIP 10.43.72.126 <none> 6379/TCP xx
Las tres ClusterIPs son distintas porque cada Service es un objeto K8s independiente, aunque
redis-slaveyredis-followerapunten al mismo conjunto de Pods. El kube-proxy instala reglas iptables/IPVS separadas para cada ClusterIP.
3. Front-end (PHP + Service NodePort)
Detalle del lab (2do bug del envío): el primer manifest del frontend omitió el bloque
ports:dentro del container. La app PHP igual escucha en:80(la imagengb-frontendestá construida para servir en ese puerto) y el Service NodePort rutea tráfico correctamente — elcurldevolvióHTTP 200. Pero el validador del lab lee la spec literal y rechaza concontainerPort for guestbook deployment is not '80'. Eldescribelo confirma:Containers: php-redis-datacenter: Image: gcr.io/google-samples/gb-frontend@sha256:... Port: <none> ← debería ser 80/TCP Host Port: <none>Fix: agregar
ports: - containerPort: 80al container del Deployment. Manifest correcto:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: php-redis-datacenter
image: gcr.io/google-samples/gb-frontend@sha256:a908df8486ff66f2c4daa0d3d8a2fa09846a1fc8efd65649c0109695c7c5cbff
resources:
requests:
cpu: 100m
memory: 100Mi
env:
- name: GET_HOSTS_FROM
value: "dns"
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
app: frontend
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30009
Verificación general
NAME READY STATUS RESTARTS AGE
frontend-847cd9797d-47jqq 1/1 Running 0 68s
frontend-847cd9797d-76shm 1/1 Running 0 68s
frontend-847cd9797d-tjxgm 1/1 Running 0 68s
redis-master-6c4c4b9d75-phsrv 1/1 Running 0 19m
redis-slave-5476897f9-dpsrw 1/1 Running 0 6m56s
redis-slave-5476897f9-x9qxp 1/1 Running 0 6m56s
Seis Pods esperados: 1 master + 2 slaves + 3 frontends.
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend NodePort 10.43.x.x <none> 80:30009/TCP xx
redis-master ClusterIP 10.43.103.55 <none> 6379/TCP xx
redis-slave ClusterIP 10.43.y.y <none> 6379/TCP xx
redis-follower ClusterIP 10.43.72.126 <none> 6379/TCP xx
Cuatro Services esperados: 3 internos (Redis) + 1 NodePort (frontend).
Validación funcional — Guestbook accesible vía NodePort
200 OK con la página HTML cargada confirma que el frontend está sirviendo. La conectividad real al Redis se prueba abriendo la página, escribiendo un mensaje en el guestbook y confirmando que persiste tras refrescar.
Validación de DNS interno (opcional, exploratoria)
Desde un Pod del frontend, confirmar que los nombres resuelven a las ClusterIPs esperadas:
Server: 10.43.0.10
Address: 10.43.0.10:53
Name: redis-master.default.svc.cluster.local
Address: 10.43.103.55
kubectl exec -it deployment/frontend -- nslookup redis-slave
kubectl exec -it deployment/frontend -- nslookup redis-follower
Ambos resuelven al mismo conjunto de IPs (las de los 2 Pods redis-slave-*).
Patrón observado: dos Services apuntando a los mismos Pods
kubectl get endpoints confirma el aliasing en la práctica:
NAME ENDPOINTS AGE
redis-slave 10.22.0.10:6379,10.22.0.11:6379 xx
redis-follower 10.22.0.10:6379,10.22.0.11:6379 xx
Las mismas IPs, dos nombres DNS distintos. Es el equivalente K8s de un CNAME / alias de DNS — útil cuando un mismo conjunto de Pods debe ser accesible bajo múltiples nombres por compatibilidad de código viejo y nuevo.
Conexión con días anteriores
- Día 56 (Deployment + Service NodePort): el frontend de hoy usa exactamente el mismo patrón — Deployment + NodePort para exposición externa.
- Día 57 (env vars + ConfigMap): los Pods de hoy usan
env: name/valueliteral (sin ConfigMap), pero el patrón es el mismo.GET_HOSTS_FROM=dnses la decisión de runtime que conecta el frontend con Redis. - Día 63 (stack multi-componente): la primera vez que apareció un stack de 4+ recursos. Hoy es de 9 recursos (3 Deployments + 4 Services + 6 Pods). El patrón escala bien con el mismo manifest declarativo.
- Día 64 (troubleshooting con 2 bugs): la herramienta
kubectl eventsse vuelve a usar para seguir el cronograma de scheduling/pull/start de cada Pod. - Día 65 (Redis con ConfigMap): primera vez con Redis. Hoy se agrega la dimensión de replicación (master + slaves).
- Día 66 (MySQL stack completo): misma idea de stack productivo con DB + Service. Aquí no hay PV (Redis es in-memory por design), pero la estructura general es la misma.
Reflexión: la primera arquitectura "real" del journal
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
| Validador del lab marca Redis incompleto aunque la app cargue | Falta uno de los tres Services pedidos (redis-master, redis-slave, redis-follower) |
Crear los tres explícitamente en el manifest, aunque dos compartan selector |
Validador rechaza con containerPort for X deployment is not 'N' |
El bloque ports: está ausente en el container del Deployment, aunque la app escuche en ese puerto |
Declarar ports: - containerPort: N aunque sea solo informativo. El validador lee la spec literal |
| Frontend devuelve HTTP 500 al guardar un guestbook entry | Frontend no resuelve redis-master (Service no existe o nombre distinto) |
kubectl exec -it <frontend-pod> -- nslookup redis-master para confirmar resolución |
| Frontend devuelve la página vacía o sin entries cargados | Frontend no resuelve redis-slave (Service no existe) |
Mismo enfoque — nslookup y kubectl get endpoints |
Pod del frontend en Pending |
No hay capacidad — requests de los 3 Pods de frontend (100m × 3) + los 3 Pods Redis no caben en el nodo |
kubectl describe pod para ver el mensaje de scheduler |
Pod en ImagePullBackOff con gcr.io/google_samples/... |
El registry gcr.io requiere internet — en clusters airgapped no funciona |
Pre-pull la imagen al nodo o usar mirror interno |
Endpoints: <none> en el Service redis-slave o redis-follower |
El selector no matchea ningún Pod (typo o label distinto del Pod template) | kubectl get pods -l app=redis-slave --show-labels para confirmar labels reales |
GET_HOSTS_FROM=dns ignorado por el frontend |
La env var debe llamarse exactamente así (case-sensitive) | Verificar en kubectl describe pod que aparece en Environment: |
| Page tarda mucho en cargar la primera vez | Pull de imagen gb-frontend es ~341 MB — primera vez requiere descarga completa |
Esperar; siguientes pulls usan cache del nodo |
| Frontend conecta solo al master, no al slave | GET_HOSTS_FROM no está seteado o está mal escrito (defaults a env) |
Confirmar en describe que GET_HOSTS_FROM=dns aparece |
Service redis-slave y redis-follower devuelven Endpoints distintos |
Los selectors no son idénticos | Ambos Services deben tener el mismo spec.selector para apuntar a los mismos Pods |
| Conectividad al NodePort 30009 falla con timeout | El proxy externo de KodeKloud puede tardar 30s+ en mapear el nuevo nodePort | Esperar y reintentar; verificar que el Service tiene endpoints |