Los pods nacen, mueren y cambian de IP. El Service es la dirección fija que no cambia: un nombre DNS y una IP virtual que enrutan a los pods vivos que matcheen un selector. Este módulo abre el dominio más grande después de Troubleshooting.
expose, create service, edición de puertos.# Las IPs de los pods son efímeras: mirá y volvé a mirar
k create deploy web --image=nginx --replicas=3
k get pods -o wide # anotá una IP
k delete pod -l app=web
k get pods -o wide # IPs nuevas, otro pod
# expose: crea el Service tomando el selector del deploy
k expose deploy web --port=80 --target-port=80
k get svc web
# NAME TYPE CLUSTER-IP PORT(S)
# web ClusterIP 10.96.140.22 80/TCP
# La lista real de pods detrás del Service
k get endpointslices -l kubernetes.io/service-name=web
k describe svc web # campo Endpoints
# Probarlo desde dentro del cluster
k run tmp --rm -it --image=nginx -- curl -s web:80
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector: # A QUIÉN apunta (labels de POD)
app: web
ports:
- port: 80 # puerto DEL SERVICE
targetPort: 8080 # puerto DEL CONTAINER
protocol: TCP
selector matchea labels de pods, no del Deployment. Si le pasás el label del Deployment y los pods tienen otro, el Service queda sin endpoints y todo falla en silencio: la IP responde, pero nadie contesta.Tres reglas que el CNI (Calico, en tu cluster) garantiza:
Es una red plana. Por eso no hay "port mapping" tipo Docker: cada pod tiene sus puertos propios.
La red plana resuelve conectividad, no descubrimiento. El Service agrega tres cosas que la red no da: una IP estable, un nombre DNS y balanceo entre réplicas.
| Campo | Es el puerto de… |
|---|---|
| port | el Service (por donde le hablás) |
| targetPort | el container (a donde reenvía) |
| nodePort | el nodo (solo tipo NodePort/LB) |
Si omitís targetPort, asume el mismo valor que port. Fuente clásica de "conecta pero da error".
El endpoint incluye únicamente pods que pasan su readinessProbe. Es la conexión directa con el M4: una probe mal configurada saca al pod del balanceo aunque esté Running.
k describe svc.k expose deploy web --port=80
# Accesible solo desde dentro: web, web.default.svc.cluster.local
k expose deploy web --port=80 --type=NodePort
k get svc web
# PORT(S): 80:31234/TCP <- 31234 es el nodePort
# Desde tu máquina, contra la IP de CUALQUIER nodo
k get nodes -o wide
curl http://<IP-DE-CUALQUIER-NODO>:31234
Rango por defecto: 30000-32767. Podés fijarlo con nodePort: 30080 en el YAML.
k expose deploy web --port=80 --type=LoadBalancer
k get svc web
# EXTERNAL-IP: <pending> <- en kubeadm bare-metal queda así PARA SIEMPRE
spec:
type: ExternalName
externalName: db.ejemplo.com
# Sin selector, sin ClusterIP: DNS devuelve un CNAME y listo
spec:
clusterIP: None # esto lo vuelve headless
selector:
app: db
# nslookup devuelve las IPs de los pods, no una VIP
<pending> sin un controller como MetalLB. Leé bien qué te piden.| Tipo | Alcance | Uso típico |
|---|---|---|
| ClusterIP | Interno | Comunicación entre servicios |
| NodePort | IP nodo + puerto | Acceso externo básico / labs |
| LoadBalancer | IP pública | Producción en la nube |
| ExternalName | Solo DNS | Apuntar a algo de afuera |
NodePort incluye ClusterIP. LoadBalancer incluye NodePort, que incluye ClusterIP. No son alternativas excluyentes: son capas. Un Service NodePort sigue teniendo su ClusterIP y su nombre DNS interno funcionando igual.
Con clusterIP: None no hay VIP ni balanceo: el DNS devuelve todas las IPs de los pods. Sirve cuando el cliente necesita elegir instancia — bases de datos con réplicas, y sobre todo StatefulSets, donde cada pod tiene identidad propia (M4).
Cluster (default): el tráfico que entra por un nodo puede reenviarse a un pod en otro nodo. Balancea mejor, pierde la IP de origen.Local: solo a pods del mismo nodo. Preserva la IP de origen, pero si ese nodo no tiene pods, se cae.# Toma el selector del recurso automáticamente
k expose deploy web --port=80 --target-port=8080
k expose pod mypod --port=80 --name=mypod-svc
k expose deploy web --port=80 --type=NodePort
# Generar el YAML sin crear (para editar el nodePort)
k expose deploy web --port=80 --type=NodePort $do > svc.yaml
# Formato de puertos: --tcp=PORT:TARGETPORT
k create service clusterip mysvc --tcp=80:8080
k create service nodeport mysvc --tcp=80:8080 --node-port=30080
k create service externalname ext --external-name=db.ejemplo.com
# run + expose en un comando (crea pod Y ClusterIP)
k run web --image=nginx --port=80 --expose
k edit svc web # puertos y tipo: editables en vivo
k patch svc web -p '{"spec":{"type":"NodePort"}}'
create service arma su propio selector (app: mysvc) que probablemente no matchee tus pods. Si lo usás, verificá los endpoints o corregí el selector a mano. Con expose ese problema no existe.expose cuando el workload ya existe — hereda el selector correcto y es imposible equivocarse. create service solo cuando necesitás un Service sin workload detrás (o un ExternalName).
Si un Service expone más de un puerto, cada entrada debe tener name. Con un solo puerto es opcional. La API rechaza el objeto si te lo olvidás.
targetPort: http resuelve al containerPort llamado http en el pod. Más robusto: el Service no se rompe si cambia el número.
$do del M0 (--dry-run=client -o yaml): para cualquier cosa que expose no soporte por flag (nodePort fijo, sessionAffinity, headless), generás el YAML, editás una línea y aplicás. Sigue siendo más rápido que escribirlo desde cero.k get pods -n kube-system -l k8s-app=kube-dns
k get svc -n kube-system kube-dns # normalmente 10.96.0.10
# Su config es un ConfigMap (el Corefile)
k -n kube-system get cm coredns -o yaml
# busybox:1.28 — las versiones nuevas tienen nslookup roto
k run dns --rm -it --image=busybox:1.28 -- sh
# dentro del pod:
nslookup web # mismo namespace
nslookup web.default # cross-namespace
nslookup web.default.svc.cluster.local
cat /etc/resolv.conf # nameserver + search domains
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Por eso web solo funciona: los search domains completan el FQDN.
# Cada pod tiene su nombre estable:
# POD-N.SERVICIO.NAMESPACE.svc.cluster.local
nslookup db-0.db.default.svc.cluster.local
kube-dns tiene endpoints? 3) ¿el nameserver del pod apunta a esa IP? En tu cluster, CoreDNS quedó Pending hasta que instalaste el CNI — sin red de pods, no hay DNS.| Parte | Qué es |
|---|---|
| web | nombre del Service |
| default | namespace |
| svc | tipo de recurso |
| cluster.local | dominio del cluster |
Completo: web.default.svc.cluster.local
web.web.otrons como mínimo.dev pidiendo web busca web.dev..., NO web.default....CoreDNS crea registros A para todos los Services. Los pods también tienen registro (10-244-1-5.default.pod.cluster.local, con guiones), pero casi nunca se usa: a los pods se les habla vía Service.
Un nombre con menos de 5 puntos se prueba primero contra los search domains. Es la razón por la que los nombres cortos funcionan, y también de latencia extra al resolver dominios externos.
k get ds -n kube-system kube-proxy
k logs -n kube-system -l k8s-app=kube-proxy --tail=20
# Su modo está en el ConfigMap
k -n kube-system get cm kube-proxy -o yaml | grep -i mode
# Lo moderno
k get endpointslices
k get endpointslices -l kubernetes.io/service-name=web -o yaml
# Lo viejo (deprecado, pero todavía responde)
k get endpoints web
# 1. ¿Existe el Service y qué selector tiene?
k get svc web -o wide
# 2. ¿Hay pods con ESE label?
k get pods -l app=web
# 3. ¿Están Ready? (solo los Ready entran)
k get pods -l app=web -o wide
# 4. ¿El Service los tomó?
k describe svc web # Endpoints: vacío = ahí está el problema
# 5. Saltear el Service: pegarle al pod directo
k run t --rm -it --image=nginx -- curl -s <IP-DEL-POD>:8080
No hay ninguna interfaz de red con esa IP, ni un proceso escuchando ahí. Es una entrada en las reglas de packet filtering del kernel de cada nodo: cuando un paquete sale hacia esa IP, el kernel le reescribe el destino (DNAT) a la IP de uno de los pods del endpoint. Por eso no podés hacer ping a una ClusterIP.
Corre en cada nodo (DaemonSet), observa Services y EndpointSlices en la API, y programa esas reglas. No procesa tráfico: lo configura y se sale del camino. Si muere, lo ya programado sigue funcionando; lo nuevo no se aplica.
| Modo | Estado |
|---|---|
| iptables | Default histórico, el más común |
| nftables | GA desde 1.33, mejor a gran escala |
| ipvs | Más algoritmos de balanceo |
Algunos CNI (Calico, Cilium) pueden reemplazar kube-proxy con eBPF. Para el examen: asumí modo iptables salvo que te digan otra cosa.
El objeto Endpoints original guardaba TODOS los backends en un solo objeto: con miles de pods, cada cambio reescribía todo. EndpointSlice los parte en trozos (100 por defecto). La API Endpoints está deprecada desde v1.33 — sigue respondiendo por compatibilidad, pero lo canónico es EndpointSlice.
k get endpoints. Funciona y no te lo van a penalizar, pero si el enunciado dice "EndpointSlice", el objeto que tenés que mirar es ese. Verificalo siempre en la doc oficial de la versión evaluada.# Crear (el 90% de las veces: expose)
k expose deploy web --port=80 --target-port=8080
k expose deploy web --port=80 --type=NodePort
k run web --image=nginx --port=80 --expose
k create service clusterip s --tcp=80:8080
# Verificar (SIEMPRE, después de crear)
k get svc -o wide # muestra SELECTOR
k describe svc web # muestra Endpoints
k get endpointslices -l kubernetes.io/service-name=web
# Probar desde dentro
k run tmp --rm -it --image=nginx -- curl -s web:80
k run dns --rm -it --image=busybox:1.28 -- nslookup web
# Puentes rápidos (debug, no producción)
k port-forward svc/web 8080:80
k port-forward pod/web-abc 8080:80
# DNS y proxy
k get pods -n kube-system -l k8s-app=kube-dns
k get ds -n kube-system kube-proxy
# FQDN: SERVICIO.NAMESPACE.svc.cluster.local
port es del Service, targetPort del container, nodePort del nodo. Tres cosas distintas.