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

  1. Por qué hace falta un orquestador
  2. Arquitectura de Kubernetes: plano de control y nodos
  3. Los objetos básicos
  4. Entorno local con kind y kubectl imprescindible
  5. Namespace, ConfigMap y Secret de servicio-pedidos
  6. Un Job para las migraciones
  7. El Deployment de servicio-pedidos, línea a línea
  8. El Service y el nombre DNS servicio-pedidos:3002
  9. Ingress: exponer el gateway en api.techcorp.example
  10. Escalado manual y estado del despliegue
  11. RabbitMQ y PostgreSQL: dentro del clúster o gestionados
  12. Helm y Kustomize para no duplicar YAML entre seis servicios

  1. 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.

  1. 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 Deployment crea ReplicaSet; el de ReplicaSet crea o borra pods hasta cuadrar replicas; el de Endpoints mantiene la lista de pods listos de cada Service (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 Service reparta 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).

  1. 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

  1. Entorno local con kind y kubectl imprescindible

kind (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: worker
kind 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.io

Los 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

  1. Namespace, ConfigMap y Secret de servicio-pedidos

Los 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.

  1. Un Job para las migraciones

scripts/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 SQL

La 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.

  1. El 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 disco

Los puntos que más dudas generan:

  • selector y labels: el Deployment (a través de su ReplicaSet) "posee" los pods cuya etiqueta app: servicio-pedidos coincide; el Service del apartado 8 usa el mismo selector. Cambiar selector en un Deployment existente no está permitido: se elige bien desde el principio.
  • readinessProbe frente a livenessProbe (03-05): readiness mira dependencias propias (PostgreSQL y RabbitMQ en el /health/ready de 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: 30 cierra el círculo del apagado ordenado: al borrar un pod, Kubernetes lo quita de los Endpoints y envía SIGTERM al PID 1 (el node de 05-01); el servicio pone /health/ready en 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: runAsNonRoot verifica lo que la imagen ya hace (USER node); readOnlyRootFilesystem obliga a que cualquier escritura vaya a un emptyDir explícito. El resto de endurecimiento (capabilities, seccomp, políticas de admisión) es de 07-04.

  1. El 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.

  1. Ingress: exponer el gateway en api.techcorp.example

El 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-02
kubectl 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.

  1. 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.

  1. 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=pedidos

Helm 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.

  1. 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 base

La 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. Sin requests el scheduler apila pods en un nodo; sin limits el primero que se dispara en memoria tumba a los demás. Los valores de partida se afinan midiendo (06-04).
  • Etiquetas del selector que no coinciden con template.metadata.labels. El apply falla con un mensaje claro; en el Service, en cambio, un selector mal escrito simplemente deja Endpoints vacío y el servicio "no responde". kubectl get endpoints es la primera comprobación.
  • Secretos en YAML dentro del repositorio "porque están en base64". No es cifrado. kubectl create secret o herramientas de 07-04 (Sealed Secrets, External Secrets).
  • Reaplicar un Job con el mismo nombre y cambios: Kubernetes lo rechaza ("field is immutable"). Nombre con versión o kubectl delete job antes.
  • Consejo: kubectl describe pod y su sección Events responden al 90 % de los "no arranca"; kubectl get events --sort-by=.lastTimestamp da 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

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados