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
- El problema: IPs efímeres
- Què és un Service i què et dona
- Endpoints i EndpointSlices: qui manté la llista
- Anatomia del manifest:
port,targetPort,protocol - Per què
ClusterIPés el tipus per defecte - Els Serveis de Rutas Norte
- Verificació des de dins del clúster
- El nom DNS i els serveis headless
- La fallada més comuna: el selector que no encaixa
- El problema: IPs efímeres
Fem visible el problema amb un experiment de trenta segons. Apunta les IPs actuals d'api-reserves:
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-norteAra 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 wideNAME 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-norteNoms 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:
- Una adreça que no canviï mai, encara que els pods de darrere canviïn tots.
- Repartiment automàtic del trànsit entre les rèpliques disponibles.
- Un nom, no una IP, per no haver de configurar adreces enlloc.
- 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-reservesdes 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".
- 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:
- Llegeix el
selectordel Service. - Busca els pods del namespace que encaixen amb aquelles etiquetes.
- D'aquests, selecciona els que estan llestos (
Ready). - 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.
- Anatomia del manifest:
port, targetPort, protocol
port, targetPort, protocolPrimer 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: TCPCamp per camp:
apiVersion: v1: el Service pertany al grup core, com el Pod. No portaapps/.metadata.name: api-reserves: importantíssim, perquè el nom del Service és el nom DNS.http://api-reserves:3000funcionarà 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 hamatchLabels: 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, ambspec.template.metadata.labelsdel 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é admetUDPiSCTP.
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 3000Així 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:
La segona és clarament superior, i és la que adopta el projecte. Avantatges:
- Desacobla el Service del contenidor. Si demà
api-reservespassa a escoltar al 8080, n'hi ha prou amb canviar elcontainerPortal 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
httpen números diferents i el mateix Service els serveix tots. - Es documenta sol.
targetPort: metricsdiu més quetargetPort: 9090.
Requisit per poder-la fer servir: el contenidor ha de declarar el port amb nom. El nostre Deployment ja ho fa:
Compte amb una asimetria que confon: port no admet nom, només número. El nom només val per a targetPort.
- Per què
ClusterIP és el tipus per defecte
ClusterIP és el tipus per defecteKubernetes 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.
- Els Serveis de Rutas Norte
Donarem adreça estable a la plataforma. Comencem pel que ja tenim escrit:
service/api-reserves created
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reserves ClusterIP 10.96.184.22 <none> 3000/TCP 4sAquí 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: TCPpostgres-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: TCPI 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: TCPAplica-ho tot i contempla el resultat:
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 8sQuatre adreces estables per a la plataforma Rutas Norte. A partir d'ara, cap component no torna a conèixer una IP de pod.
- Verificació des de dins del clúster
Comprovar els endpoints
Abans de provar trànsit, verifica sempre que el Service ha trobat pods:
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 2mAquella 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:
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/disponibilitatHem 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-n4jvtVuit 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-reservesNAME 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:3000La 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 pingkubectl 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;"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:
- El nom DNS i els serveis headless
El nom complet
Cada Service obté automàticament un registre DNS amb aquesta forma:
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:
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" deletedEl 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.
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 | Sí | 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.
- 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: 3000service/api-reserves-trencat created
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
api-reserves-trencat ClusterIP 10.96.155.71 <none> 3000/TCP 3sAspecte 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 serverEl procediment de diagnòstic
Quatre passos, sempre en aquest ordre.
Pas 1: té endpoints? És la pregunta que resol el 80 % dels casos.
<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?
Pas 3: quines etiquetes tenen els pods?
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=7f1d3e942Pas 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à.
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 PODSUn 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
portambtargetPort.portés on escolta el Service;targetPort, on escolta el contenidor. - Fer servir un nom a
port. NoméstargetPortadmet 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 a0.0.0.0per 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
pinga unaClusterIP. No respon: és una regla de reenviament, no una interfície. Fes servircurloncal 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
targetPortpel 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 yamlgenera un Service correcte que pots revisar i desar.
Exercicis
Exercici 1: Crear i verificar el Service de botiga-web
- Aplica el Service de
botiga-webd'aquesta lliçó i comprova que té 3 endpoints. - Des d'un pod efímer, fes 6 peticions a
http://botiga-web/i verifica que respon nginx. - Apunta la
ClusterIP, escala el Deployment a 5 rèpliques, torna a desplegar-lo ambrollout restarti demostra amb dues ordres que laClusterIPno ha canviat i que els endpoints sí. - Explica per què el Service fa servir
port: 80itargetPort: httpen lloc detargetPort: 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: httpExercici 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.
- Comprova que els tres Serveis tenen endpoints.
- Des d'un pod efímer de
redis:7.2-alpine, escriu a la memòria cau la disponibilitat d'una expedició: la clauplaces:BIL-SAN:2026-08-14amb valor37. Recupera-la després des d'un altre pod efímer diferent. - Des d'un pod efímer de
postgres:16, crea la taulareservesamb columnesid,clientiexpedicio, insereix dues reserves fictícies i consulta-les. - Escriu el fragment de variables d'entorn que portaria el Deployment d'
api-reservesper connectar-se a tots dos pel nom, i explica per què aquella configuració és idèntica arutas-norte-dev,rutas-norte-preirutas-norte-pro.
Solucions
Solució 1
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 5skubectl 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'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-webNAME 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:80La ClusterIP continua sent 10.96.12.209; els cinc endpoints són IPs noves.
targetPort: httpreferencia el nom del port declarat al contenidor (ports[].name: http) en lloc del seu número. Així, si demàbotiga-webcanvia el seu nginx per escoltar al 8080, n'hi ha prou amb actualitzar elcontainerPortal 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.
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 del B: no hi ha endpoints perque el port es un nom inexistent
kubectl describe svc api-reserves-b | grep -E "TargetPort|Endpoints"# Comprovacio del C
kubectl get endpoints botiga-web-c -n default
kubectl get pods -n default -l app=botiga-webSolució 3
# 1. Els tres Serveis amb endpoints
kubectl get endpoints api-reserves redis-cache postgres-reservesNAME 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 37kubectl run redis-lec --rm -it --image=redis:7.2-alpine --restart=Never -- \
redis-cli -h redis-cache GET places:BIL-SAN:2026-08-14Dos 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- Configuració d'
api-reservespel 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-02Aquella 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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
