A la lliçó anterior vam actualitzar api-reserves sense tallar el servei, però per comprovar que responia vam haver de buscar a mà la IP d'un pod concret i confiar que continués existint cinc segons després. Aquest és el forat que ens queda: cada desplegament, cada autoreparació i cada escalat canvien les IPs dels pods, així que cap component no pot conèixer un altre per la seva adreça. El Service és la resposta de Kubernetes: una adreça virtual estable, amb nom propi, que reparteix el trànsit entre totes les rèpliques sanes d'un component i sobreviu al fet que els pods de darrere vagin i vinguin. En aquesta lliçó entendràs el problema en profunditat, veuràs què és exactament un Service i com el controlador d'endpoints manté actualitzada la llista de destins, llegiràs el manifest camp per camp distingint port de targetPort, crearàs els quatre Serveis de Rutas Norte —botiga-web, api-reserves, postgres-reserves i redis-cache—, els verificaràs des de dins del clúster amb pods efímers i aprendràs a diagnosticar la fallada més freqüent de tot Kubernetes: un selector que no encaixa amb les etiquetes dels pods.

Contingut

  1. El problema: IPs efímeres
  2. Què és un Service i què et dona
  3. Endpoints i EndpointSlices: qui manté la llista
  4. Anatomia del manifest: port, targetPort, protocol
  5. Per què ClusterIP és el tipus per defecte
  6. Els Serveis de Rutas Norte
  7. Verificació des de dins del clúster
  8. El nom DNS i els serveis headless
  9. La fallada més comuna: el selector que no encaixa

  1. El problema: IPs efímeres

Fem visible el problema amb un experiment de trenta segons. Apunta les IPs actuals d'api-reserves:

kubectl get pods -l app=api-reserves -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE
api-reserves-c5a9b47d8-h7kdn   1/1   Running   0    22m   10.244.0.48   rutas-norte
api-reserves-c5a9b47d8-w3pqx   1/1   Running   0    22m   10.244.0.49   rutas-norte

Ara provoca un desplegament qualsevol i torna a mirar:

kubectl rollout restart deployment/api-reserves
kubectl rollout status deployment/api-reserves
kubectl get pods -l app=api-reserves -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE
api-reserves-7f1d3e942-b8mrs   1/1   Running   0    18s   10.244.0.52   rutas-norte
api-reserves-7f1d3e942-n4jvt   1/1   Running   0    12s   10.244.0.53   rutas-norte

Noms nous, IPs noves. Si botiga-web tingués escrita a la seva configuració l'adreça 10.244.0.48, acabaria de perdre la connexió amb l'API. I els moments en què això passa són constants:

Situació Efecte sobre les IPs
Desplegament d'una versió nova Tots els pods se substitueixen: totes les IPs canvien
Autoreparació després d'una caiguda El pod substitut té IP nova
Escalat cap amunt Apareixen IPs que ningú no coneixia
Escalat cap avall Desapareixen IPs que algú tenia apuntades
Desallotjament per manca de memòria El pod reneix en un altre node amb una altra IP

I hi ha un segon problema, tan important com el primer: el repartiment de càrrega. Encara que congeléssim les IPs, botiga-web hauria de conèixer les dues, tres o set rèpliques d'api-reserves i repartir el trànsit entre elles pel seu compte, comprovant quines estan sanes. Això és reinventar un balancejador dins de cada client.

Rutas Norte necessita tres coses de cop:

  1. Una adreça que no canviï mai, encara que els pods de darrere canviïn tots.
  2. Repartiment automàtic del trànsit entre les rèpliques disponibles.
  3. Un nom, no una IP, per no haver de configurar adreces enlloc.

  1. Què és un Service i què et dona

Un Service és un objecte de Kubernetes que defineix un punt d'accés lògic i estable a un conjunt de pods, seleccionats per etiquetes.

flowchart LR
    TW["Pods de botiga-web"]
    SVC["<b>Service api-reserves</b><br/>ClusterIP 10.96.184.22<br/>nom DNS: api-reserves"]
    P1["Pod api-reserves<br/>10.244.0.52"]
    P2["Pod api-reserves<br/>10.244.0.53"]
    P3["Pod api-reserves<br/>10.244.0.61<br/>(creat després d'un escalat)"]
    TW -->|"http://api-reserves:3000"| SVC
    SVC --> P1
    SVC --> P2
    SVC --> P3

El que aporta, punt per punt:

  • Una IP virtual estable, la ClusterIP. S'assigna en crear el Service i no canvia mentre el Service existeixi, independentment de quants pods hi hagi al darrere o de si n'hi ha zero.
  • Un nom DNS, que és el que realment faràs servir: api-reserves des del mateix namespace.
  • Balanceig de càrrega entre tots els pods sans que encaixin amb el selector, per defecte aleatori.
  • Actualització automàtica de destins: quan un pod neix, hi entra; quan mor o deixa d'estar llest, en surt. Sense intervenció humana.
  • Un port lògic que pot ser diferent del port real del contenidor.

Un detall que descol·loca al principi: la ClusterIP no pertany a cap màquina. No hi ha cap interfície de xarxa configurada amb aquella adreça, no respon a ping i no li pots fer ssh. És una adreça virtual que existeix únicament com a regla de reenviament instal·lada a cada node del clúster. Quan un pod envia un paquet a 10.96.184.22, aquella regla el reescriu al vol cap a la IP real d'un dels pods de destí.

Qui instal·la aquestes regles i com (iptables, IPVS, eBPF) és matèria de Xarxes de Clúster. Per a aquesta lliçó n'hi ha prou amb el model mental: el Service és una regla de reenviament amb nom, no una màquina.

Un advertiment sobre el balanceig, perquè genera confusió: el repartiment es fa per connexió, no per petició. Si un client obre una connexió HTTP persistent (keep-alive) o gRPC contra la ClusterIP, totes les peticions d'aquella connexió acaben al mateix pod. És un comportament normal i previsible, però convé conèixer-lo abans de sorprendre's perquè el trànsit "no es reparteix".

  1. Endpoints i EndpointSlices: qui manté la llista

El Service, per si sol, no sap res de pods. Només declara un selector. Qui fa la feina bruta és un altre controlador del kube-controller-manager: el controlador d'endpoints.

El seu bucle de reconciliació, que a hores d'ara ja et resulta familiar:

  1. Llegeix el selector del Service.
  2. Busca els pods del namespace que encaixen amb aquelles etiquetes.
  3. D'aquests, selecciona els que estan llestos (Ready).
  4. Escriu la llista de les seves IPs i ports en un objecte EndpointSlice associat al Service.
flowchart TD
    SVC["Service api-reserves<br/>selector: app=api-reserves, entorn=dev"]
    EC["Controlador d'endpoints<br/>(kube-controller-manager)"]
    ES["EndpointSlice api-reserves-xk4p2<br/>10.244.0.52:3000 · ready<br/>10.244.0.53:3000 · ready"]
    KP["kube-proxy a cada node<br/>tradueix a regles de reenviament"]
    SVC --> EC
    EC -->|"observa pods amb aquestes etiquetes"| EC
    EC --> ES
    ES --> KP

El punt crucial, i la raó que existeixin les sondes: només entren a la llista els pods llestos. Un pod que arrenca, un que està acabant o un la readinessProbe del qual falla queda fora i no rep trànsit. Aquest és el mecanisme exacte que fa possibles els desplegaments sense tall de la lliçó anterior, i també la peça que encara ens falta fins a Verificacions de Salut i Sondes.

Històricament aquesta llista vivia en un objecte Endpoints (un per Service, amb totes les adreces a dins). En clústers grans, un Service amb milers de pods generava un objecte enorme que es reenviava sencer a tots els nodes cada cop que canviava una sola IP. Per això des de Kubernetes 1.21 el mecanisme real són els EndpointSlice: fragments de fins a 100 adreces cadascun.

Aspecte Endpoints EndpointSlice
Objectes per Service 1 Tants com calguin (100 adreces per llesca)
Escalabilitat Dolenta amb milers de pods Dissenyat per escalar
Estat Es manté per compatibilitat El mecanisme real des de la 1.21
Ordre kubectl get endpoints kubectl get endpointslices

A la pràctica continuaràs fent servir kubectl get endpoints per diagnosticar perquè la seva sortida és més compacta i llegible, i Kubernetes el manté sincronitzat. Només recorda que per sota són EndpointSlices.

  1. Anatomia del manifest: port, targetPort, protocol

Primer Service del projecte, el d'api-reserves:

# k8s/base/api-reserves-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-reserves
  namespace: rutas-norte-dev
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  type: ClusterIP
  selector:
    app: api-reserves
    entorn: dev
  ports:
    - name: http
      port: 3000
      targetPort: http
      protocol: TCP

Camp per camp:

  • apiVersion: v1: el Service pertany al grup core, com el Pod. No porta apps/.
  • metadata.name: api-reserves: importantíssim, perquè el nom del Service és el nom DNS. http://api-reserves:3000 funcionarà gràcies a aquesta línia.
  • spec.type: ClusterIP: és el valor per defecte; l'escrivim explícit per claredat. La resta de tipus, a Tipus de Serveis.
  • spec.selector: quins pods hi ha al darrere. Aquí no hi ha matchLabels: el selector d'un Service és un mapa pla d'igualtat, més simple que el d'un Deployment. I un advertiment que desenvoluparem a l'apartat 9: aquestes etiquetes han de coincidir amb les dels pods, és a dir, amb spec.template.metadata.labels del Deployment, no amb les del Deployment en si.
  • ports[].name: http: obligatori si hi ha més d'un port, recomanable sempre.
  • ports[].port: 3000: el port del Service. És al qual es connecten els clients: api-reserves:3000.
  • ports[].targetPort: http: el port del pod al qual es reenvia. Aquí fem servir el nom del port declarat al contenidor.
  • ports[].protocol: TCP: per defecte TCP. També admet UDP i SCTP.

port enfront de targetPort

És la confusió número u amb els Serveis. La regla és simple:

port és per on entra el trànsit al Service. targetPort és per on surt cap al pod.

flowchart LR
    C["Client<br/>pod de botiga-web"] -->|"http://api-reserves:3000"| S["Service api-reserves<br/>port: 3000"]
    S -->|"reenvia a targetPort"| P["Pod api-reserves<br/>containerPort: 3000"]

No han de ser iguals per força, i sovint no ho són. Un cas molt habitual a Rutas Norte:

ports:
  - name: http
    port: 80          # els clients fan servir el port estandard
    targetPort: 3000  # en canvi, l'aplicacio Node escolta al 3000

Així botiga-web pot cridar http://api-reserves sense especificar port, mentre l'aplicació continua escoltant on li convé.

targetPort amb nom

Compara les dues maneres d'apuntar al pod:

# Per numero
targetPort: 3000
# Per nom, referenciant ports[].name del contenidor
targetPort: http

La segona és clarament superior, i és la que adopta el projecte. Avantatges:

  • Desacobla el Service del contenidor. Si demà api-reserves passa a escoltar al 8080, n'hi ha prou amb canviar el containerPort al Deployment; el Service continua sent vàlid sense tocar-hi ni una línia.
  • Permet un Service comú a pods heterogenis. Pods diferents poden exposar el port http en números diferents i el mateix Service els serveix tots.
  • Es documenta sol. targetPort: metrics diu més que targetPort: 9090.

Requisit per poder-la fer servir: el contenidor ha de declarar el port amb nom. El nostre Deployment ja ho fa:

ports:
  - name: http
    containerPort: 3000

Compte amb una asimetria que confon: port no admet nom, només número. El nom només val per a targetPort.

  1. Per què ClusterIP és el tipus per defecte

Kubernetes ofereix quatre tipus de Service. Aquesta lliçó se centra en ClusterIP; la resta es tracten en detall a Tipus de Serveis.

Tipus Abast Ús típic
ClusterIP (per defecte) Només dins del clúster Comunicació entre components: la immensa majoria dels Serveis
NodePort Port a cada node Proves i entorns sense balancejador
LoadBalancer Balancejador extern del proveïdor cloud Exposició pública, un per servei
ExternalName Àlies DNS a un host extern Apuntar a un servei de fora del clúster

Que ClusterIP sigui el valor per defecte no és casualitat: és una decisió de disseny alineada amb el principi de mínim privilegi. Un component no hauria de ser accessible des de fora tret que algú ho decideixi explícitament.

Aplicat als sis components de Rutas Norte:

Component Tipus de Service Per què
botiga-web ClusterIP S'exposarà a l'exterior mitjançant Ingress, no amb un Service públic
api-reserves ClusterIP Igual: l'Ingress enrutarà api.rutasnorte.example cap a ell
postgres-reserves ClusterIP Mai no ha de ser accessible des d'internet: conté dades personals
redis-cache ClusterIP Només el consumeix api-reserves
worker-notificacions Cap Ningú no li parla; ell surt a parlar amb els altres
informes-ocupacio Cap Tasca programada sense trànsit entrant

Fixa't que fins i tot els components públics fan servir ClusterIP. L'exposició a l'exterior la farà un únic Controlador d'Ingress que enruta per domini cap a aquests Serveis interns: una sola porta d'entrada, un sol lloc on acabar el TLS i aplicar regles.

I fixa't també que dos components no porten Service en absolut. Un Service només és necessari si algú ha d'iniciar connexions cap al component. worker-notificacions consumeix una cua i parla amb PostgreSQL, però ningú no el crida a ell.

  1. Els Serveis de Rutas Norte

Donarem adreça estable a la plataforma. Comencem pel que ja tenim escrit:

kubectl apply -f k8s/base/api-reserves-service.yaml
kubectl get svc api-reserves
service/api-reserves created

NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
api-reserves   ClusterIP   10.96.184.22    <none>        3000/TCP   4s

Aquí hi ha la IP virtual: 10.96.184.22. No canviarà mai mentre existeixi el Service.

botiga-web

# k8s/base/botiga-web-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: botiga-web
  namespace: rutas-norte-dev
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  type: ClusterIP
  selector:
    app: botiga-web
    entorn: dev
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP

postgres-reserves

Encara no hem desplegat PostgreSQL —necessita emmagatzematge persistent i arribarà al mòdul 5—, però podem deixar preparat un Deployment provisional d'una rèplica per practicar la connectivitat. El Service és el que ens interessa:

# k8s/base/postgres-reserves-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-reserves
  namespace: rutas-norte-dev
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  type: ClusterIP
  selector:
    app: postgres-reserves
    entorn: dev
  ports:
    - name: postgres
      port: 5432
      targetPort: postgres
      protocol: TCP

I el Deployment provisional, amb una contrasenya que de moment va en clar. És una mala pràctica deliberada i temporal: es corregeix a Secrets.

# k8s/base/postgres-reserves-deployment.yaml (PROVISIONAL: sense persistencia)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-reserves
  namespace: rutas-norte-dev
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: postgres-reserves
      entorn: dev
  template:
    metadata:
      labels:
        app: postgres-reserves
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - name: postgres
              containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: "reserves"
            - name: POSTGRES_USER
              value: "rutasnorte"
            - name: POSTGRES_PASSWORD
              value: "canviar-al-modul-3"   # provisional: veure llico 03-02
          resources:
            requests:
              cpu: "100m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

redis-cache

# k8s/base/redis-cache-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-cache
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  type: ClusterIP
  selector:
    app: redis-cache
    entorn: dev
  ports:
    - name: redis
      port: 6379
      targetPort: redis
      protocol: TCP

Aplica-ho tot i contempla el resultat:

kubectl apply -f k8s/base/
kubectl get svc
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
api-reserves        ClusterIP   10.96.184.22    <none>        3000/TCP   3m
botiga-web          ClusterIP   10.96.12.209    <none>        80/TCP     8s
postgres-reserves   ClusterIP   10.96.201.140   <none>        5432/TCP   8s
redis-cache         ClusterIP   10.96.77.33     <none>        6379/TCP   8s

Quatre adreces estables per a la plataforma Rutas Norte. A partir d'ara, cap component no torna a conèixer una IP de pod.

  1. Verificació des de dins del clúster

Comprovar els endpoints

Abans de provar trànsit, verifica sempre que el Service ha trobat pods:

kubectl get endpoints
NAME                ENDPOINTS                                       AGE
api-reserves        10.244.0.52:3000,10.244.0.53:3000              5m
botiga-web          10.244.0.44:80,10.244.0.45:80,10.244.0.46:80    2m
postgres-reserves   10.244.0.58:5432                                2m
redis-cache         10.244.0.55:6379                                2m

Aquella columna ENDPOINTS és la dada més valuosa del diagnòstic de Serveis: són les IPs reals a les quals es reenviarà el trànsit. Tres pods de botiga-web, dos d'api-reserves, un de cada base de dades. Exactament el que esperàvem.

La vista moderna, en EndpointSlices:

kubectl get endpointslices -l kubernetes.io/service-name=api-reserves
NAME                 ADDRESSTYPE   PORTS   ENDPOINTS                 AGE
api-reserves-4x7km   IPv4          3000    10.244.0.52,10.244.0.53   5m

Provar el trànsit amb un pod efímer

Fem servir la tècnica de la lliçó de Pods. Fixa't que ara no necessitem esbrinar cap IP:

kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s http://api-reserves:3000/disponibilitat
{"servei":"api-reserves","version":"2.4.0","pod":"api-reserves-7f1d3e942-b8mrs"}
pod "test" deleted

Hem cridat l'API pel seu nom. Aquell http://api-reserves:3000 és exactament la cadena de connexió que anirà a la configuració de botiga-web, i no canviarà mai.

Demostrar el balanceig de càrrega

Aquí és on ens serveix haver inclòs el nom del pod a la resposta de l'API:

kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  sh -c 'for i in $(seq 1 8); do curl -s http://api-reserves:3000/ | grep -o "api-reserves-[a-z0-9-]*"; done'
api-reserves-7f1d3e942-b8mrs
api-reserves-7f1d3e942-n4jvt
api-reserves-7f1d3e942-n4jvt
api-reserves-7f1d3e942-b8mrs
api-reserves-7f1d3e942-b8mrs
api-reserves-7f1d3e942-n4jvt
api-reserves-7f1d3e942-b8mrs
api-reserves-7f1d3e942-n4jvt

Vuit peticions repartides entre les dues rèpliques. El repartiment és aleatori, no estrictament altern: no esperis una distribució perfecta en poques peticions.

Comprovar la persistència de l'adreça

La prova definitiva. Escala, torna a desplegar i torna a cridar:

kubectl scale deployment api-reserves --replicas=4
kubectl rollout restart deployment/api-reserves
kubectl rollout status deployment/api-reserves
kubectl get svc api-reserves
kubectl get endpoints api-reserves
NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
api-reserves   ClusterIP   10.96.184.22   <none>        3000/TCP   12m

NAME           ENDPOINTS
api-reserves   10.244.0.64:3000,10.244.0.65:3000,10.244.0.66:3000,10.244.0.67:3000

La ClusterIP continua sent 10.96.184.22. Els quatre endpoints són IPs completament noves. Aquest és el contracte del Service en una línia: l'adreça de fora no canvia mai, la llista de dins s'actualitza sola.

Provar les bases de dades

kubectl run test-redis --rm -it --image=redis:7.2-alpine --restart=Never -- \
  redis-cli -h redis-cache -p 6379 ping
PONG
pod "test-redis" deleted
kubectl run test-pg --rm -it --image=postgres:16 --restart=Never --env="PGPASSWORD=canviar-al-modul-3" -- \
  psql -h postgres-reserves -U rutasnorte -d reserves -c "SELECT 'connexio correcta' AS estat;"
       estat
-------------------
 connexio correcta
(1 row)

pod "test-pg" deleted

Els quatre components són assolibles pel nom des de qualsevol punt del clúster. La plataforma ja té sistema circulatori.

Torna a 2 rèpliques abans de continuar:

kubectl scale deployment api-reserves --replicas=2

  1. El nom DNS i els serveis headless

El nom complet

Cada Service obté automàticament un registre DNS amb aquesta forma:

<servei>.<namespace>.svc.cluster.local

Per a la nostra API: api-reserves.rutas-norte-dev.svc.cluster.local. I com que el DNS del clúster configura sufixos de cerca a cada pod, des de rutas-norte-dev pots fer servir qualsevol d'aquestes formes:

Forma Funciona des de Quan fer-la servir
api-reserves El mateix namespace L'habitual: configuració de components de Rutas Norte
api-reserves.rutas-norte-dev Qualsevol namespace Quan cal creuar entorns
api-reserves.rutas-norte-dev.svc.cluster.local Qualsevol namespace Nom complet, sense ambigüitat

Comprova-ho:

kubectl run test-dns --rm -it --image=busybox:1.36 --restart=Never -- \
  nslookup api-reserves
Server:    10.96.0.10
Address:   10.96.0.10:53

Name:      api-reserves.rutas-norte-dev.svc.cluster.local
Address:   10.96.184.22

pod "test-dns" deleted

El nom resol a la ClusterIP, no a les IPs dels pods. El funcionament intern de CoreDNS, els sufixos de cerca, els registres SRV i la resolució entre namespaces s'estudien a fons a DNS Intern i Descobriment de Serveis.

Serveis headless

Existeix una variant que convé conèixer encara que no la fem servir de moment: un Service amb clusterIP: None, anomenat headless.

spec:
  clusterIP: None       # servei headless
  selector:
    app: postgres-reserves

El seu comportament és diferent: no té IP virtual i no balanceja res. En comptes d'això, el DNS retorna directament les IPs de tots els pods que encaixen amb el selector.

Aspecte Service normal Service headless
ClusterIP Sí, una IP virtual None
Què retorna el DNS La ClusterIP Totes les IPs de pod
Balanceig No: tria el client
Ús típic Càrregues sense estat Càrregues amb estat, clients que necessiten parlar amb una rèplica concreta

Per a què serveix: quan un client necessita adreçar-se a una rèplica concreta en lloc de a "qualsevol". És el cas d'un clúster de PostgreSQL on cal escriure al primari i llegir de les rèpliques, o de Kafka, o de qualsevol sistema amb quòrum. Per això els headless van gairebé sempre de la mà dels StatefulSets: la combinació dona a cada rèplica un nom DNS propi i estable com ara postgres-reserves-0.postgres-reserves.rutas-norte-dev.svc.cluster.local.

Quan al mòdul 6 convertim postgres-reserves en un StatefulSet, el seu Service passarà a ser headless. Els detalls, a DNS Intern i StatefulSets.

  1. La fallada més comuna: el selector que no encaixa

Si haguessis de memoritzar una sola cosa d'aquesta lliçó, que sigui aquesta. L'avaria número u amb Serveis a Kubernetes és un selector que no coincideix amb les etiquetes dels pods. I és especialment traïdora perquè no produeix cap error: el Service es crea sense protestar, obté la seva ClusterIP, apareix a kubectl get svc amb un aspecte perfectament sa... i no reenvia trànsit enlloc.

Provocar l'avaria

# /tmp/servei-trencat.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-reserves-trencat
  namespace: rutas-norte-dev
spec:
  type: ClusterIP
  selector:
    app: api-reserve        # falta la "s" final
    entorn: dev
  ports:
    - name: http
      port: 3000
      targetPort: 3000
kubectl apply -f /tmp/servei-trencat.yaml
kubectl get svc api-reserves-trencat
service/api-reserves-trencat created

NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
api-reserves-trencat   ClusterIP   10.96.155.71   <none>        3000/TCP   3s

Aspecte impecable. Però:

kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s --max-time 5 http://api-reserves-trencat:3000/
curl: (7) Failed to connect to api-reserves-trencat port 3000 after 2 ms: Could not connect to server

El procediment de diagnòstic

Quatre passos, sempre en aquest ordre.

Pas 1: té endpoints? És la pregunta que resol el 80 % dels casos.

kubectl get endpoints api-reserves-trencat
NAME                   ENDPOINTS   AGE
api-reserves-trencat   <none>      2m

<none>. El Service no ha trobat ni un sol pod. Confirmat: el problema és al selector o a les etiquetes, no a la xarxa ni a l'aplicació.

Pas 2: quin selector té el Service?

kubectl describe svc api-reserves-trencat | grep -i selector
Selector:  app=api-reserve,entorn=dev

Pas 3: quines etiquetes tenen els pods?

kubectl get pods -l app=api-reserves --show-labels
NAME                           READY   STATUS    LABELS
api-reserves-7f1d3e942-b8mrs   1/1     Running   app=api-reserves,app.kubernetes.io/part-of=rutas-norte,entorn=dev,pod-template-hash=7f1d3e942

Pas 4: comparar. app=api-reserve enfront de app=api-reserves. Aquí hi ha l'essa que falta.

La prova definitiva és fer servir el selector del Service com a filtre de kubectl: si no retorna pods, el Service tampoc no els trobarà.

kubectl get pods -l app=api-reserve,entorn=dev
No resources found in rutas-norte-dev namespace.
kubectl delete -f /tmp/servei-trencat.yaml

El catàleg complet de causes

Quan ENDPOINTS és buit o incomplet, la causa és sempre en aquesta taula:

Causa Com es detecta Solució
Etiqueta mal escrita al selector kubectl get pods -l <selector> no retorna res Corregir el selector
El selector apunta a les etiquetes del Deployment, no del pod Les etiquetes de metadata i de template.metadata difereixen El selector ha d'encaixar amb spec.template.metadata.labels
Service i pods en namespaces diferents kubectl get pods -n <ns> -l <selector> al namespace del Service Un Service només selecciona pods del seu propi namespace
Els pods existeixen però no estan Ready kubectl get pods mostra 0/1 Arreglar el pod: mirar describe i logs
targetPort amb un nom que el contenidor no declara Hi ha endpoints, però amb port incorrecte o sense port Declarar ports[].name al contenidor
El contenidor no escolta realment en aquell port Hi ha endpoints, però la connexió es rebutja kubectl exec i comprovar el port real

De totes elles, la segona és la més subtil i la que fa perdre més temps. Mira aquest Deployment:

metadata:
  name: api-reserves
  labels:
    app: api-reserves-deploy     # etiqueta del DEPLOYMENT
spec:
  template:
    metadata:
      labels:
        app: api-reserves        # etiqueta dels PODS

Un Service amb selector: {app: api-reserves-deploy} no trobarà res, perquè aquella etiqueta la porta el Deployment, que no és un pod. El Service selecciona pods, sempre. La convenció de Rutas Norte —fer servir les mateixes tres etiquetes al Deployment i al template— evita aquest error per construcció.

Quan sí que hi ha endpoints i tot i així falla

Si ENDPOINTS té adreces però la connexió no funciona, el problema és més avall:

# Respon el pod directament, saltant-se el Service?
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  curl -s --max-time 5 http://10.244.0.52:3000/

# Escolta el proces al port que diu?
kubectl exec deploy/api-reserves -- netstat -tlnp 2>/dev/null || \
kubectl exec deploy/api-reserves -- wget -qO- http://localhost:3000/

Si el pod respon per IP directa però no pel Service, mira el targetPort. Si no respon ni per IP directa, el problema és l'aplicació, no Kubernetes.

I un últim advertiment que estalvia hores: si la teva aplicació escolta a 127.0.0.1 en lloc de a 0.0.0.0, només es respon a si mateixa. El Service tindrà endpoints correctes i tot i així totes les connexions fallaran. És una fallada de configuració de l'aplicació que sembla una fallada de Kubernetes.

Errors Comuns i Consells

  • Selector que no encaixa amb les etiquetes dels pods. La fallada número u. El Service es crea sense error i no serveix res. Diagnòstic: kubectl get endpoints.
  • Apuntar el selector a les etiquetes del Deployment. Els Services seleccionen pods: les etiquetes rellevants són les de spec.template.metadata.labels.
  • Confondre port amb targetPort. port és on escolta el Service; targetPort, on escolta el contenidor.
  • Fer servir un nom a port. Només targetPort admet noms. port és sempre numèric.
  • Esperar que un Service arribi a pods d'un altre namespace. No ho fa: només selecciona al seu. Per creuar, es fa servir el nom DNS complet, com veurem a Namespaces.
  • Que l'aplicació escolti a 127.0.0.1. Ha d'escoltar a 0.0.0.0 per ser assolible des de fora del contenidor.
  • Crear un Service per a worker-notificacions. No rep connexions entrants; un Service sense clients és soroll i superfície d'atac innecessària.
  • Esperar un repartiment perfectament altern. El balanceig és aleatori per connexió, no per petició. Amb keep-alive, totes les peticions d'una connexió van al mateix pod.
  • Fer ping a una ClusterIP. No respon: és una regla de reenviament, no una interfície. Fes servir curl o nc al port del Service.
  • Consell: el diagnòstic de Serveis comença sempre per kubectl get endpoints <nom>. Buit significa problema de selector o d'etiquetes; amb adreces, problema de ports o d'aplicació.
  • Consell: fes servir sempre targetPort pel nom. Desacobla el Service del contenidor i t'estalvia un canvi cada cop que l'aplicació mogui el seu port.
  • Consell: per a una prova ràpida sense escriure manifests, kubectl expose deployment api-reserves --port=3000 --target-port=http --dry-run=client -o yaml genera un Service correcte que pots revisar i desar.

Exercicis

Exercici 1: Crear i verificar el Service de botiga-web

  1. Aplica el Service de botiga-web d'aquesta lliçó i comprova que té 3 endpoints.
  2. Des d'un pod efímer, fes 6 peticions a http://botiga-web/ i verifica que respon nginx.
  3. Apunta la ClusterIP, escala el Deployment a 5 rèpliques, torna a desplegar-lo amb rollout restart i demostra amb dues ordres que la ClusterIP no ha canviat i que els endpoints sí.
  4. Explica per què el Service fa servir port: 80 i targetPort: http en lloc de targetPort: 80.

Exercici 2: Diagnosticar tres Serveis trencats

Un company ha aplicat aquests tres Serveis a rutas-norte-dev i cap no funciona. Per a cadascun, identifica la causa exacta, indica l'ordre que la revela i escriu la correcció.

# Servei A
apiVersion: v1
kind: Service
metadata:
  name: redis-cache-a
  namespace: rutas-norte-dev
spec:
  selector:
    app: redis
    entorn: dev
  ports:
    - port: 6379
      targetPort: redis
# Servei B
apiVersion: v1
kind: Service
metadata:
  name: api-reserves-b
  namespace: rutas-norte-dev
spec:
  selector:
    app: api-reserves
    entorn: dev
  ports:
    - port: 3000
      targetPort: api      # el contenidor declara el port com a "http"
# Servei C
apiVersion: v1
kind: Service
metadata:
  name: botiga-web-c
  namespace: default        # compte
spec:
  selector:
    app: botiga-web
    entorn: dev
  ports:
    - port: 80
      targetPort: http

Exercici 3: Connectar la plataforma d'extrem a extrem

Objectiu: que api-reserves parli amb redis-cache i amb postgres-reserves només pel nom, sense cap IP.

  1. Comprova que els tres Serveis tenen endpoints.
  2. Des d'un pod efímer de redis:7.2-alpine, escriu a la memòria cau la disponibilitat d'una expedició: la clau places:BIL-SAN:2026-08-14 amb valor 37. Recupera-la després des d'un altre pod efímer diferent.
  3. Des d'un pod efímer de postgres:16, crea la taula reserves amb columnes id, client i expedicio, insereix dues reserves fictícies i consulta-les.
  4. Escriu el fragment de variables d'entorn que portaria el Deployment d'api-reserves per connectar-se a tots dos pel nom, i explica per què aquella configuració és idèntica a rutas-norte-dev, rutas-norte-pre i rutas-norte-pro.

Solucions

Solució 1

kubectl apply -f k8s/base/botiga-web-service.yaml
kubectl get svc,endpoints botiga-web
service/botiga-web created

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/botiga-web   ClusterIP   10.96.12.209   <none>        80/TCP    5s

NAME                   ENDPOINTS                                      AGE
endpoints/botiga-web   10.244.0.44:80,10.244.0.45:80,10.244.0.46:80   5s
kubectl run test --rm -it --image=curlimages/curl:8.8.0 --restart=Never -- \
  sh -c 'for i in $(seq 1 6); do curl -s -o /dev/null -w "%{http_code} " http://botiga-web/; done; echo'
200 200 200 200 200 200
pod "test" deleted
kubectl scale deployment botiga-web --replicas=5
kubectl rollout restart deployment/botiga-web
kubectl rollout status deployment/botiga-web
kubectl get svc botiga-web
kubectl get endpoints botiga-web
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
botiga-web   ClusterIP   10.96.12.209   <none>        80/TCP    4m

NAME         ENDPOINTS
botiga-web   10.244.0.71:80,10.244.0.72:80,10.244.0.73:80,10.244.0.74:80,10.244.0.75:80

La ClusterIP continua sent 10.96.12.209; els cinc endpoints són IPs noves.

  1. targetPort: http referencia el nom del port declarat al contenidor (ports[].name: http) en lloc del seu número. Així, si demà botiga-web canvia el seu nginx per escoltar al 8080, n'hi ha prou amb actualitzar el containerPort al Deployment: el Service continua sent correcte sense tocar-lo. És un desacoblament entre la definició del servei i el detall d'implementació del contenidor, i a més fa el manifest autoexplicatiu.
kubectl scale deployment botiga-web --replicas=3

Solució 2

Servei Causa Ordre que la revela Correcció
A El selector fa servir app: redis, però els pods porten app: redis-cache kubectl get endpoints redis-cache-a<none>, i kubectl get pods -l app=redis no retorna res app: redis-cache
B targetPort: api no existeix: el contenidor declara el port com a http Hi ha endpoints, però la connexió falla; kubectl describe svc api-reserves-b mostra TargetPort: api/TCP i kubectl get pod -o jsonpath='{.spec.containers[0].ports}' mostra name: http targetPort: http
C El Service és al namespace default i els pods a rutas-norte-dev. Un Service només selecciona pods del seu propi namespace kubectl get endpoints botiga-web-c -n default<none>, mentre que kubectl get pods -n default -l app=botiga-web no retorna res namespace: rutas-norte-dev
# Comprovacio de l'A
kubectl get endpoints redis-cache-a
kubectl get pods -l app=redis
NAME            ENDPOINTS   AGE
redis-cache-a   <none>      1m

No resources found in rutas-norte-dev namespace.
# Comprovacio del B: no hi ha endpoints perque el port es un nom inexistent
kubectl describe svc api-reserves-b | grep -E "TargetPort|Endpoints"
TargetPort:  api/TCP
Endpoints:   <none>
# Comprovacio del C
kubectl get endpoints botiga-web-c -n default
kubectl get pods -n default -l app=botiga-web
NAME           ENDPOINTS   AGE
botiga-web-c   <none>      1m

No resources found in default namespace.

Solució 3

# 1. Els tres Serveis amb endpoints
kubectl get endpoints api-reserves redis-cache postgres-reserves
NAME                ENDPOINTS                            AGE
api-reserves        10.244.0.52:3000,10.244.0.53:3000    25m
redis-cache         10.244.0.55:6379                     20m
postgres-reserves   10.244.0.58:5432                     20m
# 2. Escriure a la cache des d'un pod i llegir des d'un altre
kubectl run redis-esc --rm -it --image=redis:7.2-alpine --restart=Never -- \
  redis-cli -h redis-cache SET places:BIL-SAN:2026-08-14 37
OK
pod "redis-esc" deleted
kubectl run redis-lec --rm -it --image=redis:7.2-alpine --restart=Never -- \
  redis-cli -h redis-cache GET places:BIL-SAN:2026-08-14
"37"
pod "redis-lec" deleted

Dos pods efímers diferents, creats i destruïts, han compartit estat a través de redis-cache sense conèixer cap IP.

# 3. PostgreSQL
kubectl run pg-cli --rm -it --image=postgres:16 --restart=Never \
  --env="PGPASSWORD=canviar-al-modul-3" -- \
  psql -h postgres-reserves -U rutasnorte -d reserves -c "
    CREATE TABLE IF NOT EXISTS reserves (
      id SERIAL PRIMARY KEY,
      client TEXT NOT NULL,
      expedicio TEXT NOT NULL
    );
    INSERT INTO reserves (client, expedicio) VALUES
      ('Marta Ibarra', 'BIL-SAN 2026-08-14 08:30'),
      ('Xabier Aroca', 'BIL-SAN 2026-08-14 08:30');
    SELECT * FROM reserves;"
 id |    client    |        expedicio
----+--------------+--------------------------
  1 | Marta Ibarra | BIL-SAN 2026-08-14 08:30
  2 | Xabier Aroca | BIL-SAN 2026-08-14 08:30
(2 rows)

pod "pg-cli" deleted
  1. Configuració d'api-reserves pel nom:
env:
  - name: REDIS_HOST
    value: "redis-cache"
  - name: REDIS_PORT
    value: "6379"
  - name: DATABASE_HOST
    value: "postgres-reserves"
  - name: DATABASE_PORT
    value: "5432"
  - name: DATABASE_NAME
    value: "reserves"
  - name: DATABASE_USER
    value: "rutasnorte"
  # DATABASE_PASSWORD arribara des d'un Secret a la llico 03-02

Aquella configuració és idèntica als tres entorns perquè el nom curt redis-cache es resol dins del namespace on corre el pod. Un pod d'api-reserves a rutas-norte-pro resoldrà redis-cache com a redis-cache.rutas-norte-pro.svc.cluster.local, mentre que el mateix pod a rutas-norte-dev el resoldrà com a redis-cache.rutas-norte-dev.svc.cluster.local. El mateix manifest serveix per als tres entorns sense canviar ni una sola línia, i cadascun parla amb la seva pròpia base de dades i la seva pròpia memòria cau. És exactament el mecanisme que explota la lliçó següent, Namespaces.

Conclusió

La plataforma Rutas Norte ja té sistema circulatori. Has vist en primera persona per què les IPs de pod són inservibles com a punt de connexió —canvien a cada desplegament, cada autoreparació i cada escalat— i com el Service resol el problema amb una ClusterIP virtual que no canvia mai, un nom DNS i balanceig automàtic entre les rèpliques sanes. Saps que aquella IP no és una màquina sinó una regla de reenviament, i que la llista de destins reals la manté al dia el controlador d'endpoints, escrivint als EndpointSlices únicament les adreces dels pods llestos.

Llegeixes un manifest de Service sense dubtar: port és per on entra el trànsit i targetPort per on surt cap al contenidor, preferiblement pel nom per desacoblar el Service de la implementació. Entens per què ClusterIP és el tipus per defecte i per què a Rutas Norte ho són tots els Serveis, inclosos els dels components públics, que s'exposaran mitjançant una única porta d'entrada. I has donat adreça estable a quatre components: botiga-web, api-reserves, postgres-reserves i redis-cache, mentre que worker-notificacions i informes-ocupacio no porten Service perquè ningú no inicia connexions cap a ells. Ho has verificat sense trampes: pods efímers cridant pel nom, vuit peticions repartides entre dues rèpliques, i la prova definitiva d'escalar i tornar a desplegar veient com la ClusterIP aguanta mentre tots els endpoints es renoven.

Finalment, t'emportes el procediment de diagnòstic més rendible de tot Kubernetes: quan un Service no respon, el primer és kubectl get endpoints. Buit significa que el selector no encaixa amb les etiquetes dels pods, i d'aquí a comparar describe svc amb get pods --show-labels hi ha un pas. Amb adreces, el problema és als ports o a l'aplicació.

Has notat que en tota la lliçó hem escrit rutas-norte-dev una vegada i una altra, i que la configuració d'api-reserves no menciona l'entorn enlloc perquè el DNS ho resol sol. Això no és casualitat: és la propietat que fa possible desplegar la mateixa plataforma tres vegades sense tocar els manifests. A la lliçó següent, Namespaces, muntarem els tres entorns de Rutas Norte —dev, pre i pro—, desplegarem el mateix manifest en diversos d'ells, veurem què aïlla realment un namespace i, el més important, què no aïlla en absolut.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats