Docker Compose aixeca el sistema de TechCorp en un portàtil, però es queda curt tan bon punt hi ha més d'una màquina: si el servidor que executa servei-comandes cau, ningú no l'arrenca en un altre lloc; si el Black Friday exigeix deu rèpliques de servei-cataleg, algú ha de decidir on posar-les; si cal desplegar la versió 1.0.1 sense tallar el servei, cal coordinar a mà l'arrencada de la nova i l'aturada de la vella. Un orquestrador fa tot això de manera declarativa: li dius què vols ("dues rèpliques d'aquesta imatge, sanes, accessibles amb aquest nom") i ell s'ocupa del com, de manera contínua. Kubernetes és l'orquestrador de la llista curta de 04-01. En aquesta lliçó n'entenem l'arquitectura i els objectes, muntem un clúster local, i escrivim i apliquem els manifestos complets de servei-comandes amb els noms que 03-05 (Service, /health/*) i 04-03 (servei-comandes-config, comandes-db, comandes-rabbitmq) van deixar fixats. El pipeline que aplicarà aquests manifestos automàticament (05-03) i les estratègies per canviar de versió sense talls (05-04) venen després.

Contingut

  1. Per què cal un orquestrador
  2. Arquitectura de Kubernetes: pla de control i nodes
  3. Els objectes bàsics
  4. Entorn local amb kind i kubectl imprescindible
  5. Namespace, ConfigMap i Secret de servei-comandes
  6. Un Job per a les migracions
  7. El Deployment de servei-comandes, línia a línia
  8. El Service i el nom DNS servei-comandes:3002
  9. Ingress: exposar el gateway a api.techcorp.example
  10. Escalat manual i estat del desplegament
  11. RabbitMQ i PostgreSQL: dins del clúster o gestionats
  12. Helm i Kustomize per no duplicar YAML entre sis serveis

  1. Per què cal un orquestrador

El que un orquestrador resol, comparat amb "contenidors en màquines":

Necessitat Sense orquestrador Amb Kubernetes
Rèpliques Scripts que executen docker run en N màquines replicas: 2 en un Deployment; Kubernetes en manté sempre dues
Reinicis restart: always per màquina; si la màquina mor, res Si un pod o un node cau, es recrea en un altre node
Distribució Algú decideix a quina màquina va cada servei L'scheduler tria node segons la CPU/memòria demanades i les regles
Xarxa i descobriment Ports i IP a mà, o Consul (03-05) Service + DNS intern: http://servei-comandes:3002
Configuració i secrets Fitxers .env copiats a cada màquina ConfigMap/Secret injectats com a variables (04-03)
Desplegaments Aturar la vella, arrencar la nova, creuar els dits RollingUpdate amb readiness: sense talls (05-04)
Salut HEALTHCHECK de Docker, sense conseqüències Probes que reinicien pods i els treuen del balanceig

Tot es declara en YAML i es desa a git; el clúster reconcilia contínuament l'estat real amb el desitjat. Aquesta és la idea central: no executes ordres imperatives, descrius el resultat.

  1. Arquitectura de Kubernetes: pla de control i nodes

flowchart TB
    subgraph CP["Pla de control"]
        API[API server<br/>única porta: kubectl, controladors, kubelets]
        ETCD[(etcd<br/>estat desitjat i real)]
        SCH[Scheduler<br/>assigna pods a nodes]
        CM[Controller manager<br/>Deployment, ReplicaSet, Job, Endpoints...]
        API <--> ETCD
        SCH --> API
        CM --> API
    end
    subgraph N1["Node 1"]
        K1[kubelet] --> R1[containerd]
        R1 --> P1[pod servei-comandes-7d9f-abc]
        R1 --> P2[pod servei-cataleg-5c1b-xyz]
        KP1[kube-proxy]
    end
    subgraph N2["Node 2"]
        K2[kubelet] --> R2[containerd]
        R2 --> P3[pod servei-comandes-7d9f-def]
        KP2[kube-proxy]
    end
    API --> K1
    API --> K2
    kubectl -->|kubectl apply -f| API
  • API server: rep totes les peticions (de kubectl, dels controladors, dels kubelets), les valida i les persisteix a etcd, la base de dades clau-valor del clúster.
  • Scheduler: observa pods sense node assignat i els en tria un segons els recursos sol·licitats, les afinitats i les restriccions.
  • Controller manager: executa els bucles de control. El controlador de Deployment crea ReplicaSet; el de ReplicaSet crea o esborra pods fins a quadrar replicas; el d'Endpoints manté la llista de pods llestos de cada Service (el "registre" de 03-05).
  • kubelet (a cada node): agent que parla amb l'API server, arrenca els contenidors dels pods assignats al seu node a través del runtime (containerd, la mateixa tecnologia de contenidors de 05-01) i executa les probes.
  • kube-proxy: programa les regles de xarxa perquè la ClusterIP d'un Service reparteixi entre els seus pods.

Un desenvolupador de TechCorp només parla amb l'API server (kubectl), i ni tan sols això en producció: ho farà el pipeline o Argo CD (05-03).

  1. Els objectes bàsics

Objecte Què és Ús a TechCorp
Pod Unitat mínima: un o més contenidors que comparteixen xarxa (mateixa IP) i emmagatzematge; efímer Un contenidor servei-comandes per pod (més un sidecar si hi hagués mesh, 05-05)
ReplicaSet Manté N pods idèntics vius No s'escriu mai a mà: el crea el Deployment
Deployment Descriu la plantilla de pod, les rèpliques i com actualitzar-les Un per servei stateless: els sis serveis i el gateway
Service Nom i IP estables davant d'un conjunt de pods (03-05). Tipus: ClusterIP (intern, per defecte), NodePort (port a cada node), LoadBalancer (balancejador del núvol) ClusterIP per a tots els serveis interns; el gateway s'exposa per Ingress
Ingress Regla HTTP (host/ruta → Service) executada per un ingress controller (NGINX, Traefik) api.techcorp.examplegateway:8080
ConfigMap Parells clau-valor no sensibles servei-comandes-config (04-03)
Secret Parells clau-valor sensibles (base64, no xifrat per defecte) comandes-db, comandes-rabbitmq (04-03)
Namespace Partició lògica del clúster per a noms, quotes i permisos techcorp per a tots els serveis; plataforma per a ingress, observabilitat
StatefulSet Pods amb identitat i disc estables PostgreSQL/RabbitMQ en dev (apartat 11); cap servei de TechCorp
Job / CronJob Tasca que acaba / tasca programada Migracions (Job); una neteja nocturna de claus_idempotencia seria un CronJob

  1. Entorn local amb kind i kubectl imprescindible

kind (Kubernetes in Docker) crea un clúster complet dins de contenidors; minikube és l'alternativa equivalent. TechCorp fa servir kind perquè és el mateix que corre a CI. El fitxer de configuració publica els ports 80/443 del node al portàtil perquè l'Ingress funcioni:

# techcorp/plataforma/local/kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:                       # 80/443 del node → portàtil, perquè l'Ingress (apartat 9) respongui a 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                       # comprova 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/servei-comandes:1.0.0 --name techcorp   # imatge local (05-01) sense passar per ghcr.io

Les ordres de kubectl que es fan servir cada dia (kubectl config set-context --current --namespace=techcorp evita repetir -n techcorp; en el que segueix l'ometem):

kubectl apply -f fitxer.yaml             # crear o actualitzar (declaratiu, idempotent); -k per a un directori Kustomize
kubectl get pods -n techcorp -w          # llistar (i -w: seguir canvis); també get deploy/svc/cm/secret/ingress/job
kubectl describe pod servei-comandes-7d9f-abc -n techcorp    # detall i, sobretot, la secció Events (per què no arrenca)
kubectl logs -f deploy/servei-comandes -n techcorp           # logs (d'un pod del Deployment); --previous si s'ha reiniciat
kubectl port-forward svc/servei-comandes 3002:3002 -n techcorp    # túnel local per provar amb curl sense Ingress
kubectl rollout status deploy/servei-comandes -n techcorp     # espera que el desplegament acabi (o falli)
kubectl exec -it deploy/servei-comandes -n techcorp -- sh     # shell en un pod; kubectl delete -f: esborrar el que s'ha declarat

  1. Namespace, ConfigMap i Secret de servei-comandes

Els manifestos viuen a techcorp/plataforma/k8s/servei-comandes/base/ (l'apartat 12 n'explica l'estructura). Primer el namespace i la configuració no sensible, que és exactament la columna "producció" de la taula de 04-03:

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: techcorp
---
# k8s/servei-comandes/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: servei-comandes-config           # el nom acordat a 04-03
  namespace: techcorp
data:                                    # tot són cadenes: els números van entre cometes
  NODE_ENV: production
  PORT: "3002"
  LOG_NIVELL: info
  CATALEG_URL: http://servei-cataleg:3001       # noms DNS dels Service (03-05)
  CLIENTS_URL: http://servei-clients:3004
  TIMEOUT_HTTP_MS: "2000"
  OUTBOX_INTERVAL_MS: "250"
  CATALEG_REMOT: "true"

Els secrets no van en YAML dins del repositori. Es creen amb kubectl (o els injecta un gestor extern, 07-04):

kubectl create secret generic comandes-db \
  --from-literal=COMANDES_DB_URL='postgres://svc_comandes:[email protected]:5432/comandes'
kubectl create secret generic comandes-rabbitmq \
  --from-literal=RABBITMQ_URL='amqp://comandes:Pr0d-Rq7t...@rabbitmq:5672'

kubectl get secret comandes-db -o yaml
# data:
#   COMANDES_DB_URL: cG9zdGdyZXM6Ly9zdmNfY29tYW5kZXM6UHIwZC1YazN2Li4uQHBnLWNvbWFuZGVzLi4u

Aquest valor és base64, no xifrat: echo cG9z... | base64 -d retorna la contrasenya. Un Secret només és "secret" perquè l'accés s'hi restringeix amb permisos (RBAC) i perquè etcd es pot xifrar en repòs; totes dues coses són de 07-04. Per això els secrets no es versionen en text pla i per això el .dockerignore de 05-01 excloïa .env. Les claus del Secret es diuen com les variables d'entorn (COMANDES_DB_URL) per poder-les injectar amb envFrom.

  1. Un Job per a les migracions

scripts/migrar.js (04-04) s'ha d'executar abans que arrenqui la nova versió de Comandes, una vegada, i fallar sorollosament si no pot. És un Job:

# k8s/servei-comandes/base/job-migracions.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: servei-comandes-migracions-1-0-0       # el nom porta la versió: un Job és immutable, cada versió crea el seu
  namespace: techcorp
spec:
  backoffLimit: 3                              # reintents si el pod falla (p. ex. PostgreSQL encara no accepta connexions)
  ttlSecondsAfterFinished: 3600                # s'esborra sol una hora després d'acabar
  template:
    spec:
      restartPolicy: Never                     # un Job no reinicia el contenidor: crea un altre pod si cal
      securityContext:
        runAsNonRoot: true
      containers:
        - name: migracions
          image: ghcr.io/techcorp/servei-comandes:1.0.0      # la MATEIXA imatge del servei: migracions/ i scripts/ van a dins (05-01)
          command: ["node", "scripts/migrar.js"]
          envFrom:
            - secretRef: { name: comandes-db } # només necessita COMANDES_DB_URL
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 256Mi }
kubectl apply -f k8s/servei-comandes/base/job-migracions.yaml
kubectl wait --for=condition=complete job/servei-comandes-migracions-1-0-0 --timeout=120s
kubectl logs job/servei-comandes-migracions-1-0-0     # "aplicada 001-esquema-inicial.sql" ... o l'error SQL

L'alternativa és un init container dins del pod del servei; funciona, però executa les migracions a cada rèplica en arrencar (amb dues rèpliques, dues vegades, i migrar.js ha de suportar la concurrència). El Job les executa una vegada per versió i es veu a kubectl get jobs. A 05-03 el pipeline el llança i espera abans d'aplicar el Deployment.

  1. El Deployment de servei-comandes, línia a línia

# k8s/servei-comandes/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: servei-comandes
  namespace: techcorp
  labels:
    app: servei-comandes
spec:
  replicas: 2                                  # dos pods sempre; l'scheduler els reparteix entre nodes
  selector:
    matchLabels:
      app: servei-comandes                     # quins pods gestiona aquest Deployment (ha de coincidir amb template.metadata.labels)
  # strategy: RollingUpdate és el valor per defecte; els seus paràmetres s'afinen a 05-04
  template:                                    # plantilla del pod
    metadata:
      labels:
        app: servei-comandes
        version: v1                            # etiqueta extra que 05-04 i 05-05 faran servir per encaminar entre versions
    spec:
      terminationGracePeriodSeconds: 30        # després de SIGTERM, quant espera el kubelet abans de SIGKILL (l'aturada de 04-02 triga < 10 s)
      securityContext:
        runAsNonRoot: true                     # el kubelet rebutja el pod si la imatge intentés córrer com a root (USER node a 05-01)
        runAsUser: 1000                        # uid de l'usuari 'node' de la imatge base
      containers:
        - name: servei-comandes
          image: ghcr.io/techcorp/servei-comandes:1.0.0      # etiqueta concreta, mai latest (05-01)
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 3002              # el PORT del ConfigMap; documenta i dona nom al port
          envFrom:                             # totes les claus d'aquests objectes passen a ser variables d'entorn (04-03)
            - configMapRef: { name: servei-comandes-config }
            - secretRef: { name: comandes-db }
            - secretRef: { name: comandes-rabbitmq }
          resources:
            requests:                          # el que l'scheduler reserva per col·locar el pod
              cpu: 100m                        # 0,1 nuclis
              memory: 128Mi
            limits:                            # sostre: si la memòria se supera, el contenidor mor (OOMKilled)
              cpu: 500m
              memory: 256Mi
          readinessProbe:                      # pot atendre? Si falla, el pod surt dels Endpoints del Service (03-05)
            httpGet: { path: /health/ready, port: http }
            initialDelaySeconds: 5             # el servei triga ~2 s a arrencar i connectar; 5 s de marge
            periodSeconds: 5
            failureThreshold: 3                # tres fallades seguides (15 s) → no llest; un èxit → llest una altra vegada
          livenessProbe:                       # és viu? Si falla, el kubelet REINICIA el contenidor
            httpGet: { path: /health/live, port: http }
            initialDelaySeconds: 15
            periodSeconds: 10
            failureThreshold: 3
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true       # la imatge és només de lectura; Node no escriu a disc

Els punts que generen més dubtes:

  • selector i labels: el Deployment (a través del seu ReplicaSet) "posseeix" els pods l'etiqueta dels quals app: servei-comandes coincideix; el Service de l'apartat 8 fa servir el mateix selector. Canviar selector en un Deployment existent no està permès: es tria bé des del començament.
  • readinessProbe davant de livenessProbe (03-05): readiness mira dependències pròpies (PostgreSQL i RabbitMQ al /health/ready de 04-04) i el seu efecte és deixar de rebre trànsit; liveness només mira que el procés respon i el seu efecte és reiniciar. Posar la comprovació de la base de dades a liveness és l'error clàssic: si PostgreSQL cau, Kubernetes reinicia tots els pods de Comandes en bucle sense arreglar res.
  • terminationGracePeriodSeconds: 30 tanca el cercle de l'aturada ordenada: en esborrar un pod, Kubernetes el treu dels Endpoints i envia SIGTERM al PID 1 (el node de 05-01); el servei posa /health/ready a 503, acaba les peticions en curs i surt en menys de 10 s; si no ho fes, als 30 s arribaria SIGKILL. Amb dues rèpliques i aquest contracte, un desplegament no perd cap petició (05-04 ho mostra pas a pas).
  • securityContext: runAsNonRoot verifica el que la imatge ja fa (USER node); readOnlyRootFilesystem obliga que qualsevol escriptura vagi a un emptyDir explícit. La resta d'enduriment (capabilities, seccomp, polítiques d'admissió) és de 07-04.

  1. El Service i el nom DNS servei-comandes:3002

# k8s/servei-comandes/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: servei-comandes                        # → DNS servei-comandes.techcorp.svc.cluster.local, o simplement servei-comandes
  namespace: techcorp
spec:
  type: ClusterIP                              # només accessible dins del clúster (el gateway i altres serveis)
  selector:
    app: servei-comandes                       # els pods llestos amb aquesta etiqueta són els Endpoints
  ports:
    - name: http
      port: 3002                               # port del Service (el que fan servir els que criden: COMANDES_URL=http://servei-comandes:3002)
      targetPort: http                         # port del contenidor (pel nom, definit al Deployment)

Amb això, el COMANDES_URL=http://servei-comandes:3002 del gateway i el CATALEG_URL=http://servei-cataleg:3001 del ConfigMap de Comandes resolen exactament com a Compose (05-01) i com prometia 03-05, sense que cap servei sàpiga quantes rèpliques hi ha ni a quin node són.

Aplicar i comprovar tot l'anterior:

kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/servei-comandes/base/      # configmap, job, deployment, service (els secrets ja existeixen)
kubectl rollout status deploy/servei-comandes   # deployment "servei-comandes" successfully rolled out
kubectl get pods -l app=servei-comandes         # servei-comandes-7d9f6c4b8-abcde 1/1 Running, ...-fghij 1/1 Running
kubectl get endpoints servei-comandes           # 10.244.1.7:3002,10.244.2.4:3002 → els dos pods llestos
kubectl port-forward svc/servei-comandes 3002:3002 &
curl -s localhost:3002/health/ready             # {"estat":"ok","dependencies":{"postgres":"ok","rabbitmq":"ok"}}

Si un pod es queda en CrashLoopBackOff, kubectl logs --previous ensenyarà gairebé sempre el missatge de config.js (04-03) dient quina variable falta: la validació fail-fast està pensada per a aquest moment.

  1. Ingress: exposar el gateway a api.techcorp.example

El gateway (03-04) es desplega igual que qualsevol servei (Deployment + Service gateway:8080, amb el seu ConfigMap d'URL). L'única cosa que surt del clúster és un Ingress que l'ingress controller (NGINX a kind i en producció; Traefik seria equivalent amb ingressClassName: traefik) converteix en regles 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: afegir "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 }]   # certificats: 07-02
kubectl apply -f k8s/gateway/base/
curl -s http://api.techcorp.example/api/v1/productes?ids=p-501 | jq .dades[0].nom    # "Auriculars BT X200"

L'ingress controller balanceja en capa 7 cap al Service del gateway, i el gateway cap als serveis interns (03-05). L'únic punt d'entrada continua sent el 8080 del gateway; cap servei-* no té Ingress.

  1. Escalat manual i estat del desplegament

kubectl scale deploy/servei-cataleg --replicas=6         # pic de catàleg (×20 en campanyes, 01-05): més pods, mateix Service
kubectl get pods -l app=servei-cataleg -w                # els nous passen per ContainerCreating → Running → Ready
kubectl scale deploy/servei-cataleg --replicas=2         # tornada a la normalitat
kubectl rollout history deploy/servei-comandes           # revisions (cada canvi de template en crea una)

kubectl scale és manual: algú decideix el nombre. L'escalat automàtic per CPU o per mètriques (HorizontalPodAutoscaler) és de 06-04, quan tinguem mètriques amb què decidir. Aquí l'important és que escalar és canviar un número i que el Service reparteix només entre les rèpliques llestes.

  1. RabbitMQ i PostgreSQL: dins del clúster o gestionats

Criteri Dins del clúster (StatefulSet, normalment via Helm o un operador) Servei gestionat (RDS/Cloud SQL, Amazon MQ/CloudAMQP)
Operació (còpies, pedaços, failover, discos) La fa l'equip de Plataforma La fa el proveïdor
Cost Només còmput/emmagatzematge del clúster Més car per unitat, sense hores d'operació
Rendiment i control Total; també tota la responsabilitat Menys ajustaments fins; garanties de disponibilitat per contracte
Entorn local/CI Imprescindible (no hi ha núvol a kind) No aplica
Risc Perdre dades per un error d'operació d'un equip de 4 persones Acoblament al proveïdor

Decisió de TechCorp: en dev i al clúster kind de CI, PostgreSQL i RabbitMQ dins del clúster amb Helm (dues ordres, dades d'un sol ús); a staging i producció, gestionats, amb l'URL als Secret (comandes-db, comandes-rabbitmq) i el nom rabbitmq resolt per un Service de tipus ExternalName si cal mantenir RABBITMQ_URL estable. L'equip de Plataforma té quatre persones i la seva prioritat és el gateway, el clúster i el pipeline, no fer de DBA.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq -n techcorp --set auth.username=comandes --set auth.password=dev-rabbit
helm install pg-comandes bitnami/postgresql -n techcorp --set auth.username=svc_comandes --set auth.password=dev-comandes --set auth.database=comandes

Helm instal·la un chart (paquet de manifestos parametritzats) que crea l'StatefulSet, el Service (rabbitmq, pg-comandes-postgresql) i els volums; RABBITMQ_URL=amqp://comandes:dev-rabbit@rabbitmq:5672 funciona a kind sense tocar els manifestos dels serveis.

  1. Helm i Kustomize per no duplicar YAML entre sis serveis

Els quatre fitxers de Comandes es repetirien gairebé idèntics per a Catàleg, Inventari, Pagaments, Notificacions, Clients i el gateway, i a més per entorn (dev, staging, prod). Dues eines eviten copiar i enganxar:

Kustomize Helm
Idea YAML base + pedaços per entorn; sense plantilles Plantilles Go amb values.yaml; paquets versionats (charts)
Integrat a kubectl Sí (kubectl apply -k) No (binari helm)
Corba Baixa: és YAML normal Mitjana: sintaxi de plantilles
Encaix Manifestos propis de TechCorp Instal·lar programari de tercers (RabbitMQ, ingress-nginx, Prometheus a 06-01)

TechCorp fa servir Kustomize per als seus serveis i Helm per a tercers. Estructura a techcorp/plataforma/k8s/:

k8s/
├── namespace.yaml
├── servei-comandes/
│   ├── base/
│   │   ├── kustomization.yaml
│   │   ├── configmap.yaml  deployment.yaml  service.yaml  job-migracions.yaml
│   └── overlays/
│       ├── dev/kustomization.yaml
│       └── prod/kustomization.yaml
├── servei-cataleg/ ...   (mateixa forma)
└── gateway/ ...
# k8s/servei-comandes/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: techcorp
resources: [configmap.yaml, deployment.yaml, service.yaml, job-migracions.yaml]
commonLabels:
  app.kubernetes.io/part-of: techcorp-shop
---
# k8s/servei-comandes/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: servei-comandes, count: 1 }]             # en dev, una rèplica
images:                                                     # aquí és on el pipeline de 05-03 canviarà l'etiqueta
  - { name: ghcr.io/techcorp/servei-comandes, newTag: sha-9f3c2ab }
patches:
  - patch: |-                                               # sobreescriu només aquestes claus del ConfigMap base
      apiVersion: v1
      kind: ConfigMap
      metadata: { name: servei-comandes-config }
      data: { LOG_NIVELL: debug, OUTBOX_INTERVAL_MS: "500" }
---
# k8s/servei-comandes/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: servei-comandes, count: 3 }]
images:
  - { name: ghcr.io/techcorp/servei-comandes, newTag: 1.0.0 }
kubectl kustomize k8s/servei-comandes/overlays/dev       # mostra el YAML final sense aplicar-lo
kubectl apply -k k8s/servei-comandes/overlays/dev        # aplica l'overlay de dev
kubectl apply -k k8s/servei-comandes/overlays/prod       # ...o el de prod, amb la mateixa base

La base és una per servei, i els overlays només diuen en què es diferencia cada entorn: rèpliques, etiqueta d'imatge, dues claus de configuració. Quan l'equip d'Inventari creï el seu servei, copiarà servei-comandes/ i canviarà noms, port (3006) i ConfigMap: és la part de "plantilla" de la regla del Luis; la part d'"automatitzar" és de 05-03.

Errors Comuns i Consells

  • Comprovar la base de dades al livenessProbe. PostgreSQL cau 30 s i Kubernetes reinicia tots els pods de Comandes en bucle. Dependències a readiness; a liveness, només el procés.
  • Sense resources. Sense requests l'scheduler apila pods en un node; sense limits el primer que es dispara en memòria tomba els altres. Els valors de partida s'afinen mesurant (06-04).
  • Etiquetes del selector que no coincideixen amb template.metadata.labels. L'apply falla amb un missatge clar; al Service, en canvi, un selector mal escrit simplement deixa els Endpoints buits i el servei "no respon". kubectl get endpoints és la primera comprovació.
  • Secrets en YAML dins del repositori "perquè són en base64". No és xifratge. kubectl create secret o eines de 07-04 (Sealed Secrets, External Secrets).
  • Reaplicar un Job amb el mateix nom i canvis: Kubernetes el rebutja ("field is immutable"). Nom amb versió o kubectl delete job abans.
  • Consell: kubectl describe pod i la seva secció Events responen el 90 % dels "no arrenca"; kubectl get events --sort-by=.lastTimestamp dona la vista del namespace.

Exercicis

Exercici 1. Escriu el Deployment i el Service de servei-cataleg (imatge ghcr.io/techcorp/servei-cataleg:1.4.2, port 3001, ConfigMap servei-cataleg-config amb PORT, MONGO_BD, LOG_NIVELL, NODE_ENV; Secret cataleg-mongo amb MONGO_URL) indicant només les línies que difereixen dels de Comandes. Quantes rèpliques posaries a l'overlay de prod i per què?

Exercici 2. Un pod de servei-comandes mostra READY 0/1 durant minuts però STATUS Running i sense reinicis. kubectl logs ensenya "escoltant al 3002" i cap traça d'error. Enumera, en ordre, les tres ordres que executaries per diagnosticar-ho i les dues causes més probables.

Exercici 3. La Marta pregunta per què el Job de migracions porta la versió al nom i què passa si el pipeline desplega la 1.0.1 sense haver canviat cap migració. Respon i proposa com evitar que el Job falli en aquest cas.

Solucions

Solució 1. Difereixen: metadata.name, labels/selector (app: servei-cataleg), image: ghcr.io/techcorp/servei-cataleg:1.4.2, containerPort: 3001, envFrom amb configMapRef: servei-cataleg-config i secretRef: cataleg-mongo, i al Service port: 3001 (el CATALEG_URL=http://servei-cataleg:3001 de tots). Les probes apunten als mateixos /health/live i /health/ready (contracte de 03-05); terminationGracePeriodSeconds, securityContext i resources poden ser iguals. No hi ha Job de migracions (MongoDB sense esquema; la llavor és només de dev). Rèpliques a prod: més que Comandes (per exemple 4), perquè Catàleg rep els pics ×20 de campanyes (01-05) i és només de lectura, barat de replicar; el nombre definitiu el donarà l'HPA de 06-04.

Solució 2. (1) kubectl describe pod <nom>: a Events apareixerà "Readiness probe failed: HTTP probe failed with statuscode: 503" (o timeout). (2) kubectl port-forward pod/<nom> 3002:3002 i curl localhost:3002/health/ready: el cos diu quina dependència està malament (postgres o rabbitmq). (3) kubectl get secret comandes-db -o jsonpath='{.data.COMANDES_DB_URL}' | base64 -d (i el mateix per a RabbitMQ) per veure on apunta. Causes més probables: l'URL del Secret apunta a un host incorrecte o amb credencials dolentes (el servei arrenca perquè config.js només valida el format, però /health/ready no pot fer SELECT 1), o la dependència no és accessible des del namespace (RabbitMQ encara no instal·lat, Service rabbitmq inexistent). És el comportament desitjat: no llest, sense trànsit, sense reinicis en bucle.

Solució 3. Un Job és immutable i el seu nom únic: si es reapliqués servei-comandes-migracions amb una altra image, l'API server el rebutjaria. Amb la versió al nom, cada desplegament crea un Job nou, en queda registre (kubectl get jobs) i ttlSecondsAfterFinished el neteja. Si la 1.0.1 no porta migracions noves, el Job arrenca, migrar.js consulta migracions_aplicades, veu que 001-004 ja hi són i acaba amb codi 0 sense fer res: és idempotent per disseny (04-04), així que no falla; simplement és un Job de dos segons. L'alternativa d'ometre el Job quan "no hi ha canvis" exigeix que algú ho decideixi, i la regla del Luis prefereix que sigui sempre el mateix pipeline.

Conclusió

Kubernetes executa les imatges de 05-01 de manera declarativa i contínua: un pla de control (API server, etcd, scheduler, controller manager) que reconcilia l'estat desitjat, i nodes amb kubelet, kube-proxy i containerd que el materialitzen. Per a servei-comandes hem escrit i aplicat al namespace techcorp el ConfigMap servei-comandes-config, els Secret comandes-db i comandes-rabbitmq creats amb kubectl create secret (base64, no xifrat), un Job versionat que executa scripts/migrar.js amb la mateixa imatge, un Deployment de dues rèpliques amb envFrom, resources, readinessProbe a /health/ready, livenessProbe a /health/live, terminationGracePeriodSeconds: 30 i runAsNonRoot, i el Service que dona el nom http://servei-comandes:3002; el gateway surt a l'exterior per un Ingress NGINX a api.techcorp.example; RabbitMQ i PostgreSQL van amb Helm en dev/kind i gestionats en producció; i Kustomize (base + overlays dev/prod, amb images: com el punt que canviarà el pipeline) evita duplicar YAML entre els sis serveis. Tot s'ha aplicat a mà amb kubectl apply, i això és precisament el que la lliçó següent elimina: un pipeline de CI/CD per servei que prova, verifica pactes, construeix la imatge i actualitza aquests manifestos sense que ningú escrigui cap ordre.

Curs de Microserveis

Mòdul 1: Introducció als Microserveis

Mòdul 2: Disseny de Microserveis

Mòdul 3: Comunicació entre Microserveis

Mòdul 4: Implementació de Microserveis

Mòdul 5: Desplegament i Orquestració

Mòdul 6: Monitoratge i Manteniment

Mòdul 7: Seguretat en Microserveis

Mòdul 8: Casos d'Estudi i Exemples Pràctics

© Copyright 2026. Tots els drets reservats