Docker Compose levanta el sistema de TechCorp en un portátil, pero se queda corto en cuanto hay más de una máquina: si el servidor que ejecuta servicio-pedidos se cae, nadie lo arranca en otro sitio; si el Black Friday exige diez réplicas de servicio-catalogo, alguien tiene que decidir dónde ponerlas; si hay que desplegar la versión 1.0.1 sin cortar el servicio, hay que coordinar a mano el arranque de la nueva y la parada de la vieja. Un orquestador hace todo eso de forma declarativa: le dices qué quieres ("dos réplicas de esta imagen, sanas, accesibles con este nombre") y él se ocupa del cómo, de forma continua. Kubernetes es el orquestador de la lista corta de 04-01. En esta lección entendemos su arquitectura y sus objetos, montamos un clúster local, y escribimos y aplicamos los manifiestos completos de servicio-pedidos con los nombres que 03-05 (Service, /health/*) y 04-03 (servicio-pedidos-config, pedidos-db, pedidos-rabbitmq) dejaron fijados. El pipeline que aplicará estos manifiestos automáticamente (05-03) y las estrategias para cambiar de versión sin cortes (05-04) vienen después.
Contenido
- Por qué hace falta un orquestador
- Arquitectura de Kubernetes: plano de control y nodos
- Los objetos básicos
- Entorno local con kind y
kubectlimprescindible Namespace,ConfigMapySecretdeservicio-pedidos- Un
Jobpara las migraciones - El
Deploymentdeservicio-pedidos, línea a línea - El
Servicey el nombre DNSservicio-pedidos:3002 Ingress: exponer el gateway enapi.techcorp.example- Escalado manual y estado del despliegue
- RabbitMQ y PostgreSQL: dentro del clúster o gestionados
- Helm y Kustomize para no duplicar YAML entre seis servicios
- Por qué hace falta un orquestador
Lo que un orquestador resuelve, comparado con "contenedores en máquinas":
| Necesidad | Sin orquestador | Con Kubernetes |
|---|---|---|
| Réplicas | Scripts que ejecutan docker run en N máquinas |
replicas: 2 en un Deployment; Kubernetes mantiene siempre dos |
| Reinicios | restart: always por máquina; si la máquina muere, nada |
Si un pod o un nodo cae, se recrea en otro nodo |
| Distribución | Alguien decide en qué máquina va cada servicio | El scheduler elige nodo según CPU/memoria pedidas y reglas |
| Red y descubrimiento | Puertos y IPs a mano, o Consul (03-05) | Service + DNS interno: http://servicio-pedidos:3002 |
| Configuración y secretos | Ficheros .env copiados a cada máquina |
ConfigMap/Secret inyectados como variables (04-03) |
| Despliegues | Parar la vieja, arrancar la nueva, cruzar los dedos | RollingUpdate con readiness: sin cortes (05-04) |
| Salud | HEALTHCHECK de Docker, sin consecuencias |
Probes que reinician pods y los sacan del balanceo |
Todo se declara en YAML y se guarda en git; el clúster reconcilia continuamente el estado real con el deseado. Esa es la idea central: no ejecutas comandos imperativos, describes el resultado.
- Arquitectura de Kubernetes: plano de control y nodos
flowchart TB
subgraph CP["Plano de control"]
API[API server<br/>única puerta: kubectl, controladores, kubelets]
ETCD[(etcd<br/>estado deseado y real)]
SCH[Scheduler<br/>asigna pods a nodos]
CM[Controller manager<br/>Deployment, ReplicaSet, Job, Endpoints...]
API <--> ETCD
SCH --> API
CM --> API
end
subgraph N1["Nodo 1"]
K1[kubelet] --> R1[containerd]
R1 --> P1[pod servicio-pedidos-7d9f-abc]
R1 --> P2[pod servicio-catalogo-5c1b-xyz]
KP1[kube-proxy]
end
subgraph N2["Nodo 2"]
K2[kubelet] --> R2[containerd]
R2 --> P3[pod servicio-pedidos-7d9f-def]
KP2[kube-proxy]
end
API --> K1
API --> K2
kubectl -->|kubectl apply -f| API
- API server: recibe todas las peticiones (de
kubectl, de los controladores, de los kubelets), las valida y las persiste en etcd, la base de datos clave-valor del clúster. - Scheduler: observa pods sin nodo asignado y les elige uno según recursos solicitados, afinidades y restricciones.
- Controller manager: ejecuta los bucles de control. El controlador de
DeploymentcreaReplicaSet; el deReplicaSetcrea o borra pods hasta cuadrarreplicas; el deEndpointsmantiene la lista de pods listos de cadaService(el "registro" de 03-05). - kubelet (en cada nodo): agente que habla con el API server, arranca los contenedores de los pods asignados a su nodo a través del runtime (containerd, la misma tecnología de contenedores de 05-01) y ejecuta las probes.
- kube-proxy: programa las reglas de red para que la ClusterIP de un
Servicereparta entre sus pods.
Un desarrollador de TechCorp solo habla con el API server (kubectl), y ni siquiera eso en producción: lo hará el pipeline o Argo CD (05-03).
- Los objetos básicos
| Objeto | Qué es | Uso en TechCorp |
|---|---|---|
| Pod | Unidad mínima: uno o más contenedores que comparten red (misma IP) y almacenamiento; efímero | Un contenedor servicio-pedidos por pod (más un sidecar si hubiese mesh, 05-05) |
| ReplicaSet | Mantiene N pods idénticos vivos | Nunca se escribe a mano: lo crea el Deployment |
| Deployment | Describe la plantilla de pod, las réplicas y cómo actualizarlas | Uno por servicio stateless: los seis servicios y el gateway |
| Service | Nombre e IP estables delante de un conjunto de pods (03-05). Tipos: ClusterIP (interno, por defecto), NodePort (puerto en cada nodo), LoadBalancer (balanceador de la nube) |
ClusterIP para todos los servicios internos; el gateway se expone por Ingress |
| Ingress | Regla HTTP (host/ruta → Service) ejecutada por un ingress controller (NGINX, Traefik) | api.techcorp.example → gateway:8080 |
| ConfigMap | Pares clave-valor no sensibles | servicio-pedidos-config (04-03) |
| Secret | Pares clave-valor sensibles (base64, no cifrado por defecto) | pedidos-db, pedidos-rabbitmq (04-03) |
| Namespace | Partición lógica del clúster para nombres, cuotas y permisos | techcorp para todos los servicios; plataforma para ingress, observabilidad |
| StatefulSet | Pods con identidad y disco estables | PostgreSQL/RabbitMQ en dev (apartado 11); ningún servicio de TechCorp |
| Job / CronJob | Tarea que termina / tarea programada | Migraciones (Job); una limpieza nocturna de claves_idempotencia sería un CronJob |
- Entorno local con kind y
kubectl imprescindible
kubectl imprescindiblekind (Kubernetes in Docker) crea un clúster completo dentro de contenedores; minikube es la alternativa equivalente. TechCorp usa kind porque es el mismo que corre en CI. El fichero de configuración publica los puertos 80/443 del nodo en el portátil para que el Ingress funcione:
# techcorp/plataforma/local/kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings: # 80/443 del nodo → portátil, para que el Ingress (apartado 9) responda en localhost
- { containerPort: 80, hostPort: 80 }
- { containerPort: 443, hostPort: 443 }
- role: worker
- role: workerkind create cluster --name techcorp --config plataforma/local/kind.yaml
kubectl cluster-info # comprueba que kubectl apunta al clúster kind-techcorp
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kind load docker-image ghcr.io/techcorp/servicio-pedidos:1.0.0 --name techcorp # imagen local (05-01) sin pasar por ghcr.ioLos comandos de kubectl que se usan cada día (kubectl config set-context --current --namespace=techcorp evita repetir -n techcorp; en lo que sigue lo omitimos):
kubectl apply -f fichero.yaml # crear o actualizar (declarativo, idempotente); -k para un directorio Kustomize
kubectl get pods -n techcorp -w # listar (y -w: seguir cambios); también get deploy/svc/cm/secret/ingress/job
kubectl describe pod servicio-pedidos-7d9f-abc -n techcorp # detalle y, sobre todo, la sección Events (por qué no arranca)
kubectl logs -f deploy/servicio-pedidos -n techcorp # logs (de un pod del Deployment); --previous si se reinició
kubectl port-forward svc/servicio-pedidos 3002:3002 -n techcorp # túnel local para probar con curl sin Ingress
kubectl rollout status deploy/servicio-pedidos -n techcorp # espera a que el despliegue termine (o falle)
kubectl exec -it deploy/servicio-pedidos -n techcorp -- sh # shell en un pod; kubectl delete -f: borrar lo declarado
Namespace, ConfigMap y Secret de servicio-pedidos
Namespace, ConfigMap y Secret de servicio-pedidosLos manifiestos viven en techcorp/plataforma/k8s/servicio-pedidos/base/ (apartado 12 explica la estructura). Primero el namespace y la configuración no sensible, que es exactamente la columna "producción" de la tabla de 04-03:
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: techcorp
---
# k8s/servicio-pedidos/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: servicio-pedidos-config # el nombre acordado en 04-03
namespace: techcorp
data: # todo son cadenas: los números van entre comillas
NODE_ENV: production
PUERTO: "3002"
LOG_NIVEL: info
CATALOGO_URL: http://servicio-catalogo:3001 # nombres DNS de los Service (03-05)
CLIENTES_URL: http://servicio-clientes:3004
TIMEOUT_HTTP_MS: "2000"
OUTBOX_INTERVALO_MS: "250"
CATALOGO_REMOTO: "true"Los secretos no van en YAML dentro del repositorio. Se crean con kubectl (o los inyecta un gestor externo, 07-04):
kubectl create secret generic pedidos-db \
--from-literal=PEDIDOS_DB_URL='postgres://svc_pedidos:[email protected]:5432/pedidos'
kubectl create secret generic pedidos-rabbitmq \
--from-literal=RABBITMQ_URL='amqp://pedidos:Pr0d-Rq7t...@rabbitmq:5672'
kubectl get secret pedidos-db -o yaml
# data:
# PEDIDOS_DB_URL: cG9zdGdyZXM6Ly9zdmNfcGVkaWRvczpQcjBkLVhrM3YuLi5AcGctcGVkaWRvcy4uLg==Ese valor es base64, no cifrado: echo cG9z... | base64 -d devuelve la contraseña. Un Secret solo es "secreto" porque el acceso a él se restringe con permisos (RBAC) y porque etcd puede cifrarse en reposo; ambas cosas son de 07-04. Por eso los secretos no se versionan en texto plano y por eso el .dockerignore de 05-01 excluía .env. Las claves del Secret se llaman como las variables de entorno (PEDIDOS_DB_URL) para poder inyectarlas con envFrom.
- Un
Job para las migraciones
Job para las migracionesscripts/migrar.js (04-04) debe ejecutarse antes de que arranque la nueva versión de Pedidos, una vez, y fallar ruidosamente si no puede. Es un Job:
# k8s/servicio-pedidos/base/job-migraciones.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: servicio-pedidos-migraciones-1-0-0 # el nombre lleva la versión: un Job es inmutable, cada versión crea el suyo
namespace: techcorp
spec:
backoffLimit: 3 # reintentos si el pod falla (p. ej. PostgreSQL aún no acepta conexiones)
ttlSecondsAfterFinished: 3600 # se borra solo una hora después de terminar
template:
spec:
restartPolicy: Never # un Job no reinicia el contenedor: crea otro pod si hace falta
securityContext:
runAsNonRoot: true
containers:
- name: migraciones
image: ghcr.io/techcorp/servicio-pedidos:1.0.0 # la MISMA imagen del servicio: migraciones/ y scripts/ van dentro (05-01)
command: ["node", "scripts/migrar.js"]
envFrom:
- secretRef: { name: pedidos-db } # solo necesita PEDIDOS_DB_URL
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }kubectl apply -f k8s/servicio-pedidos/base/job-migraciones.yaml
kubectl wait --for=condition=complete job/servicio-pedidos-migraciones-1-0-0 --timeout=120s
kubectl logs job/servicio-pedidos-migraciones-1-0-0 # "aplicada 001-esquema-inicial.sql" ... o el error SQLLa alternativa es un init container dentro del pod del servicio; funciona, pero ejecuta las migraciones en cada réplica al arrancar (con dos réplicas, dos veces, y migrar.js tiene que soportar la concurrencia). El Job las ejecuta una vez por versión y se ve en kubectl get jobs. En 05-03 el pipeline lo lanza y espera antes de aplicar el Deployment.
- El
Deployment de servicio-pedidos, línea a línea
Deployment de servicio-pedidos, línea a línea# k8s/servicio-pedidos/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: servicio-pedidos
namespace: techcorp
labels:
app: servicio-pedidos
spec:
replicas: 2 # dos pods siempre; el scheduler los reparte entre nodos
selector:
matchLabels:
app: servicio-pedidos # qué pods gestiona este Deployment (debe coincidir con template.metadata.labels)
# strategy: RollingUpdate es el valor por defecto; sus parámetros se afinan en 05-04
template: # plantilla del pod
metadata:
labels:
app: servicio-pedidos
version: v1 # etiqueta extra que 05-04 y 05-05 usarán para enrutar entre versiones
spec:
terminationGracePeriodSeconds: 30 # tras SIGTERM, cuánto espera el kubelet antes de SIGKILL (el apagado de 04-02 tarda < 10 s)
securityContext:
runAsNonRoot: true # el kubelet rechaza el pod si la imagen intentase correr como root (USER node en 05-01)
runAsUser: 1000 # uid del usuario 'node' de la imagen base
containers:
- name: servicio-pedidos
image: ghcr.io/techcorp/servicio-pedidos:1.0.0 # etiqueta concreta, nunca latest (05-01)
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3002 # el PUERTO del ConfigMap; documenta y da nombre al puerto
envFrom: # todas las claves de estos objetos pasan a ser variables de entorno (04-03)
- configMapRef: { name: servicio-pedidos-config }
- secretRef: { name: pedidos-db }
- secretRef: { name: pedidos-rabbitmq }
resources:
requests: # lo que el scheduler reserva para colocar el pod
cpu: 100m # 0,1 núcleos
memory: 128Mi
limits: # techo: si la memoria se supera, el contenedor muere (OOMKilled)
cpu: 500m
memory: 256Mi
readinessProbe: # ¿puede atender? Si falla, el pod sale de los Endpoints del Service (03-05)
httpGet: { path: /health/ready, port: http }
initialDelaySeconds: 5 # el servicio tarda ~2 s en arrancar y conectar; 5 s de margen
periodSeconds: 5
failureThreshold: 3 # tres fallos seguidos (15 s) → no listo; un éxito → listo otra vez
livenessProbe: # ¿está vivo? Si falla, el kubelet REINICIA el contenedor
httpGet: { path: /health/live, port: http }
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true # la imagen es de solo lectura; Node no escribe en discoLos puntos que más dudas generan:
selectorylabels: elDeployment(a través de suReplicaSet) "posee" los pods cuya etiquetaapp: servicio-pedidoscoincide; elServicedel apartado 8 usa el mismo selector. Cambiarselectoren unDeploymentexistente no está permitido: se elige bien desde el principio.readinessProbefrente alivenessProbe(03-05): readiness mira dependencias propias (PostgreSQL y RabbitMQ en el/health/readyde 04-04) y su efecto es dejar de recibir tráfico; liveness solo mira que el proceso responde y su efecto es reiniciar. Poner la comprobación de la base de datos en liveness es el error clásico: si PostgreSQL cae, Kubernetes reinicia todos los pods de Pedidos en bucle sin arreglar nada.terminationGracePeriodSeconds: 30cierra el círculo del apagado ordenado: al borrar un pod, Kubernetes lo quita de los Endpoints y envía SIGTERM al PID 1 (elnodede 05-01); el servicio pone/health/readyen 503, termina las peticiones en curso y sale en menos de 10 s; si no lo hiciera, a los 30 s llegaría SIGKILL. Con dos réplicas y este contrato, un despliegue no pierde una petición (05-04 lo muestra paso a paso).securityContext:runAsNonRootverifica lo que la imagen ya hace (USER node);readOnlyRootFilesystemobliga a que cualquier escritura vaya a unemptyDirexplícito. El resto de endurecimiento (capabilities, seccomp, políticas de admisión) es de 07-04.
- El
Service y el nombre DNS servicio-pedidos:3002
Service y el nombre DNS servicio-pedidos:3002# k8s/servicio-pedidos/base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: servicio-pedidos # → DNS servicio-pedidos.techcorp.svc.cluster.local, o simplemente servicio-pedidos
namespace: techcorp
spec:
type: ClusterIP # solo accesible dentro del clúster (el gateway y otros servicios)
selector:
app: servicio-pedidos # los pods listos con esta etiqueta son los Endpoints
ports:
- name: http
port: 3002 # puerto del Service (el que usan los llamantes: PEDIDOS_URL=http://servicio-pedidos:3002)
targetPort: http # puerto del contenedor (por nombre, definido en el Deployment)Con esto, el PEDIDOS_URL=http://servicio-pedidos:3002 del gateway y el CATALOGO_URL=http://servicio-catalogo:3001 del ConfigMap de Pedidos resuelven exactamente como en Compose (05-01) y como prometía 03-05, sin que ningún servicio sepa cuántas réplicas hay ni en qué nodo están.
Aplicar y comprobar todo lo anterior:
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/servicio-pedidos/base/ # configmap, job, deployment, service (los secretos ya existen)
kubectl rollout status deploy/servicio-pedidos # deployment "servicio-pedidos" successfully rolled out
kubectl get pods -l app=servicio-pedidos # servicio-pedidos-7d9f6c4b8-abcde 1/1 Running, ...-fghij 1/1 Running
kubectl get endpoints servicio-pedidos # 10.244.1.7:3002,10.244.2.4:3002 → los dos pods listos
kubectl port-forward svc/servicio-pedidos 3002:3002 &
curl -s localhost:3002/health/ready # {"estado":"ok","dependencias":{"postgres":"ok","rabbitmq":"ok"}}Si un pod se queda en CrashLoopBackOff, kubectl logs --previous enseñará casi siempre el mensaje de config.js (04-03) diciendo qué variable falta: la validación fail-fast está pensada para este momento.
Ingress: exponer el gateway en api.techcorp.example
Ingress: exponer el gateway en api.techcorp.exampleEl gateway (03-04) se despliega igual que cualquier servicio (Deployment + Service gateway:8080, con su ConfigMap de URLs). Lo único que sale del clúster es un Ingress que el ingress controller (NGINX en kind y en producción; Traefik sería equivalente con ingressClassName: traefik) convierte en reglas de proxy:
# k8s/gateway/base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gateway
namespace: techcorp
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 2m
spec:
ingressClassName: nginx
rules:
- host: api.techcorp.example # en local: añadir "127.0.0.1 api.techcorp.example" a /etc/hosts
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway
port: { number: 8080 }
# tls: [{ hosts: [api.techcorp.example], secretName: api-techcorp-tls }] # certificados: 07-02kubectl apply -f k8s/gateway/base/
curl -s http://api.techcorp.example/api/v1/productos?ids=p-501 | jq .datos[0].nombre # "Auriculares BT X200"El ingress controller balancea en capa 7 hacia el Service del gateway, y el gateway hacia los servicios internos (03-05). El único punto de entrada sigue siendo el 8080 del gateway; ningún servicio-* tiene Ingress.
- Escalado manual y estado del despliegue
kubectl scale deploy/servicio-catalogo --replicas=6 # pico de catálogo (×20 en campañas, 01-05): más pods, mismo Service
kubectl get pods -l app=servicio-catalogo -w # los nuevos pasan por ContainerCreating → Running → Ready
kubectl scale deploy/servicio-catalogo --replicas=2 # vuelta a la normalidad
kubectl rollout history deploy/servicio-pedidos # revisiones (cada cambio de template crea una)kubectl scale es manual: alguien decide el número. El escalado automático por CPU o por métricas (HorizontalPodAutoscaler) es de 06-04, cuando tengamos métricas con las que decidir. Aquí lo importante es que escalar es cambiar un número y que el Service reparte solo entre las réplicas listas.
- RabbitMQ y PostgreSQL: dentro del clúster o gestionados
| Criterio | Dentro del clúster (StatefulSet, normalmente vía Helm o un operador) | Servicio gestionado (RDS/Cloud SQL, Amazon MQ/CloudAMQP) |
|---|---|---|
| Operación (copias, parches, failover, discos) | La hace el equipo de Plataforma | La hace el proveedor |
| Coste | Solo cómputo/almacenamiento del clúster | Más caro por unidad, sin horas de operación |
| Rendimiento y control | Total; también toda la responsabilidad | Menos ajustes finos; garantías de disponibilidad por contrato |
| Entorno local/CI | Imprescindible (no hay nube en kind) | No aplica |
| Riesgo | Perder datos por un error de operación de un equipo de 4 personas | Acoplamiento al proveedor |
Decisión de TechCorp: en dev y en el clúster kind de CI, PostgreSQL y RabbitMQ dentro del clúster con Helm (dos comandos, datos desechables); en staging y producción, gestionados, con la URL en los Secret (pedidos-db, pedidos-rabbitmq) y el nombre rabbitmq resuelto por un Service de tipo ExternalName si hace falta mantener RABBITMQ_URL estable. El equipo de Plataforma tiene cuatro personas y su prioridad es el gateway, el clúster y el pipeline, no ser DBA.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq -n techcorp --set auth.username=pedidos --set auth.password=dev-rabbit
helm install pg-pedidos bitnami/postgresql -n techcorp --set auth.username=svc_pedidos --set auth.password=dev-pedidos --set auth.database=pedidosHelm instala un chart (paquete de manifiestos parametrizados) que crea el StatefulSet, el Service (rabbitmq, pg-pedidos-postgresql) y los volúmenes; RABBITMQ_URL=amqp://pedidos:dev-rabbit@rabbitmq:5672 funciona en kind sin tocar los manifiestos de los servicios.
- Helm y Kustomize para no duplicar YAML entre seis servicios
Los cuatro ficheros de Pedidos se repetirían casi idénticos para Catálogo, Inventario, Pagos, Notificaciones, Clientes y el gateway, y además por entorno (dev, staging, prod). Dos herramientas evitan copiar y pegar:
| Kustomize | Helm | |
|---|---|---|
| Idea | YAML base + parches por entorno; sin plantillas | Plantillas Go con values.yaml; paquetes versionados (charts) |
Integrado en kubectl |
Sí (kubectl apply -k) |
No (binario helm) |
| Curva | Baja: es YAML normal | Media: sintaxis de plantillas |
| Encaje | Manifiestos propios de TechCorp | Instalar software de terceros (RabbitMQ, ingress-nginx, Prometheus en 06-01) |
TechCorp usa Kustomize para sus servicios y Helm para terceros. Estructura en techcorp/plataforma/k8s/:
k8s/ ├── namespace.yaml ├── servicio-pedidos/ │ ├── base/ │ │ ├── kustomization.yaml │ │ ├── configmap.yaml deployment.yaml service.yaml job-migraciones.yaml │ └── overlays/ │ ├── dev/kustomization.yaml │ └── prod/kustomization.yaml ├── servicio-catalogo/ ... (misma forma) └── gateway/ ...
# k8s/servicio-pedidos/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: techcorp
resources: [configmap.yaml, deployment.yaml, service.yaml, job-migraciones.yaml]
commonLabels:
app.kubernetes.io/part-of: techcorp-shop
---
# k8s/servicio-pedidos/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: servicio-pedidos, count: 1 }] # en dev, una réplica
images: # aquí es donde el pipeline de 05-03 cambiará la etiqueta
- { name: ghcr.io/techcorp/servicio-pedidos, newTag: sha-9f3c2ab }
patches:
- patch: |- # sobrescribe solo estas claves del ConfigMap base
apiVersion: v1
kind: ConfigMap
metadata: { name: servicio-pedidos-config }
data: { LOG_NIVEL: debug, OUTBOX_INTERVALO_MS: "500" }
---
# k8s/servicio-pedidos/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: servicio-pedidos, count: 3 }]
images:
- { name: ghcr.io/techcorp/servicio-pedidos, newTag: 1.0.0 }kubectl kustomize k8s/servicio-pedidos/overlays/dev # muestra el YAML final sin aplicarlo
kubectl apply -k k8s/servicio-pedidos/overlays/dev # aplica el overlay de dev
kubectl apply -k k8s/servicio-pedidos/overlays/prod # ...o el de prod, con la misma baseLa base es una por servicio, y los overlays solo dicen en qué se diferencia cada entorno: réplicas, etiqueta de imagen, dos claves de configuración. Cuando el equipo de Inventario cree su servicio, copiará servicio-pedidos/ y cambiará nombres, puerto (3006) y ConfigMap: es la parte de "plantilla" de la regla de Luis; la parte de "automatizar" es de 05-03.
Errores Comunes y Consejos
- Comprobar la base de datos en
livenessProbe. PostgreSQL cae 30 s y Kubernetes reinicia todos los pods de Pedidos en bucle. Dependencias en readiness; en liveness, solo el proceso. - Sin
resources. Sinrequestsel scheduler apila pods en un nodo; sinlimitsel primero que se dispara en memoria tumba a los demás. Los valores de partida se afinan midiendo (06-04). - Etiquetas del
selectorque no coinciden contemplate.metadata.labels. Elapplyfalla con un mensaje claro; en elService, en cambio, un selector mal escrito simplemente dejaEndpointsvacío y el servicio "no responde".kubectl get endpointses la primera comprobación. - Secretos en YAML dentro del repositorio "porque están en base64". No es cifrado.
kubectl create secreto herramientas de 07-04 (Sealed Secrets, External Secrets). - Reaplicar un
Jobcon el mismo nombre y cambios: Kubernetes lo rechaza ("field is immutable"). Nombre con versión okubectl delete jobantes. - Consejo:
kubectl describe pody su secciónEventsresponden al 90 % de los "no arranca";kubectl get events --sort-by=.lastTimestampda la vista del namespace.
Ejercicios
Ejercicio 1. Escribe el Deployment y el Service de servicio-catalogo (imagen ghcr.io/techcorp/servicio-catalogo:1.4.2, puerto 3001, ConfigMap servicio-catalogo-config con PUERTO, MONGO_BD, LOG_NIVEL, NODE_ENV; Secret catalogo-mongo con MONGO_URL) indicando solo las líneas que difieren de los de Pedidos. ¿Cuántas réplicas pondrías en el overlay de prod y por qué?
Ejercicio 2. Un pod de servicio-pedidos muestra READY 0/1 durante minutos pero STATUS Running y sin reinicios. kubectl logs enseña "escuchando en 3002" y ninguna traza de error. Enumera, en orden, los tres comandos que ejecutarías para diagnosticarlo y las dos causas más probables.
Ejercicio 3. Marta pregunta por qué el Job de migraciones lleva la versión en el nombre y qué pasa si el pipeline despliega la 1.0.1 sin haber cambiado ninguna migración. Responde y propón cómo evitar que el Job falle en ese caso.
Soluciones
Solución 1. Difieren: metadata.name, labels/selector (app: servicio-catalogo), image: ghcr.io/techcorp/servicio-catalogo:1.4.2, containerPort: 3001, envFrom con configMapRef: servicio-catalogo-config y secretRef: catalogo-mongo, y en el Service port: 3001 (el CATALOGO_URL=http://servicio-catalogo:3001 de todos). Las probes apuntan a los mismos /health/live y /health/ready (contrato de 03-05), terminationGracePeriodSeconds, securityContext y resources pueden ser iguales. No hay Job de migraciones (MongoDB sin esquema; la semilla es solo de dev). Réplicas en prod: más que Pedidos (por ejemplo 4), porque Catálogo recibe los picos ×20 de campañas (01-05) y es de solo lectura, barato de replicar; el número definitivo lo dará el HPA de 06-04.
Solución 2. (1) kubectl describe pod <nombre>: en Events aparecerá "Readiness probe failed: HTTP probe failed with statuscode: 503" (o timeout). (2) kubectl port-forward pod/<nombre> 3002:3002 y curl localhost:3002/health/ready: el cuerpo dice qué dependencia está mal (postgres o rabbitmq). (3) kubectl get secret pedidos-db -o jsonpath='{.data.PEDIDOS_DB_URL}' | base64 -d (y lo mismo para RabbitMQ) para ver a dónde apunta. Causas más probables: la URL del Secret apunta a un host incorrecto o con credenciales malas (el servicio arranca porque config.js solo valida el formato, pero /health/ready no puede hacer SELECT 1), o la dependencia no es alcanzable desde el namespace (RabbitMQ aún no instalado, Service rabbitmq inexistente). Es el comportamiento deseado: no listo, sin tráfico, sin reinicios en bucle.
Solución 3. Un Job es inmutable y su nombre único: si se reaplicara servicio-pedidos-migraciones con otra image, el API server lo rechazaría. Con la versión en el nombre, cada despliegue crea un Job nuevo, queda registro (kubectl get jobs) y ttlSecondsAfterFinished lo limpia. Si la 1.0.1 no trae migraciones nuevas, el Job arranca, migrar.js consulta migraciones_aplicadas, ve que 001-004 ya están y termina con código 0 sin hacer nada: es idempotente por diseño (04-04), así que no falla; simplemente es un Job de dos segundos. La alternativa de omitir el Job cuando "no hay cambios" exige que alguien lo decida, y la regla de Luis prefiere que sea siempre el mismo pipeline.
Conclusión
Kubernetes ejecuta las imágenes de 05-01 de forma declarativa y continua: un plano de control (API server, etcd, scheduler, controller manager) que reconcilia el estado deseado, y nodos con kubelet, kube-proxy y containerd que lo materializan. Para servicio-pedidos hemos escrito y aplicado en el namespace techcorp el ConfigMap servicio-pedidos-config, los Secret pedidos-db y pedidos-rabbitmq creados con kubectl create secret (base64, no cifrado), un Job versionado que ejecuta scripts/migrar.js con la misma imagen, un Deployment de dos réplicas con envFrom, resources, readinessProbe a /health/ready, livenessProbe a /health/live, terminationGracePeriodSeconds: 30 y runAsNonRoot, y el Service que da el nombre http://servicio-pedidos:3002; el gateway sale al exterior por un Ingress NGINX en api.techcorp.example; RabbitMQ y PostgreSQL van con Helm en dev/kind y gestionados en producción; y Kustomize (base + overlays dev/prod, con images: como el punto que cambiará el pipeline) evita duplicar YAML entre los seis servicios. Todo se ha aplicado a mano con kubectl apply, y eso es precisamente lo que la siguiente lección elimina: un pipeline de CI/CD por servicio que prueba, verifica pactos, construye la imagen y actualiza estos manifiestos sin que nadie escriba un comando.
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
