Lesson  in  Kubernetes 101

CoreDNS y el DNS interno

El listín telefónico del clúster: cómo se forma el nombre de un Service, qué es el ndots que causa la mitad de las latencias raras, cómo se resuelve entre namespaces y qué hace un Service de tipo ExternalName.

Llevas todo el curso escribiendo http://web y funcionando. Detrás de esa comodidad hay un servidor DNS que corre dentro del propio clúster (CoreDNS), un fichero /etc/resolv.conf que el kubelet inyecta en cada Pod y un par de detalles que causan latencias inexplicables cuando no se conocen.

Trabaja desde la pestaña dev-machine. Empieza saludando al servidor:

kubectl get deployment coredns -n kube-system
kubectl get service kube-dns -n kube-system
kubectl get configmap coredns -n kube-system -o yaml

Tres piezas que ya sabes leer: CoreDNS es un Deployment normal y corriente (con sus réplicas, su rollout y su rollback), expuesto por un Service llamado históricamente kube-dns, y configurado por un ConfigMap con un fichero llamado Corefile. Ese Corefile es una cadena de plugins; el que hace la magia se llama kubernetes y es quien traduce los objetos Service y Pod a registros DNS. Y fíjate en el plugin forward: es la puerta hacia el DNS de fuera, la que hace que un Pod pueda resolver también example.com.

Paso 1: El escenario

Levanta un Service en tienda (el namespace en el que ya te deja el contexto) y un cliente desde el que preguntar. Ya lo haces con los ojos cerrados:

kubectl create deployment web --image=ghcr.io/iximiuz/labs/nginx:alpine --port=80
kubectl expose deployment web --port=80 --target-port=80
kubectl run cliente --image=ghcr.io/iximiuz/labs/nginx:alpine --command -- sleep 100000

El playground trae además un namespace otro con otro Service que también se llama web. Esa colisión deliberada es el corazón de la lección.

Paso 2: La anatomía de un nombre

Todo Service tiene un nombre completo con esta forma:

<servicio>.<namespace>.svc.<dominio-del-cluster>

Es decir, web.tienda.svc.cluster.local. Los cuatro trozos, de derecha a izquierda: el dominio del clúster (configurable, casi siempre cluster.local), svc (que lo distingue de los registros de Pod), el namespace y el nombre del Service.

Pregunta por él desde el cliente, con el nombre entero y con el nombre corto:

kubectl exec cliente -- nslookup web.tienda.svc.cluster.local
kubectl exec cliente -- nslookup web

Las dos devuelven la misma IP. ¿Por qué funciona la corta? Por el fichero que el kubelet le ha puesto dentro:

kubectl exec cliente -- cat /etc/resolv.conf

Tres líneas que merece la pena entender:

  • nameserver: la ClusterIP del Service kube-dns. Ahí es donde el Pod pregunta.
  • search: la lista de sufijos que el resolver va probando cuando le das un nombre incompleto: tienda.svc.cluster.local, svc.cluster.local, cluster.local. Por eso web acaba encontrando a web.tienda.svc.cluster.local y no al del namespace otro: el primer sufijo de la lista es el namespace del propio Pod.
  • options ndots:5: y aquí está el clásico de DNS en Kubernetes.

¿Qué significa ndots:5? Que si el nombre que pides tiene menos de 5 puntos, el resolver lo considera "incompleto" y prueba primero todos los sufijos de search antes de probarlo tal cual. Con web es lo que queremos. Pero mira lo que pasa con un nombre externo:

example.com tiene 1 punto, o sea, menos de 5. Así que el Pod pregunta, por este orden:

  1. example.com.tienda.svc.cluster.local → no existe
  2. example.com.svc.cluster.local → no existe
  3. example.com.cluster.local → no existe
  4. example.com → por fin

Tres consultas fallidas antes de cada acierto, en cada resolución, multiplicadas por todas las peticiones de tu aplicación. Es una de las causas clásicas de latencia inexplicable y de sobrecarga de CoreDNS en clústeres grandes. El truco para evitarlo es tan simple como feo: poner un punto al final del nombre (example.com.), que lo declara absoluto y salta el search entero. Pruébalo:

kubectl exec cliente -- nslookup example.com
kubectl exec cliente -- nslookup example.com.

Las dos responden lo mismo, y esa es justo la trampa: las tres consultas fallidas no se ven. Ocurren por debajo, dentro del resolver del contenedor, y nslookup solo enseña la que acertó. Para verlas hay que mirar desde el otro lado, en los logs de CoreDNS o capturando el tráfico del Pod. Lo que cambia entre las dos formas no es el resultado: es el trabajo que ha costado llegar a él.

Paso 3: Entre namespaces

Ahora la colisión. Desde el cliente, que vive en tienda, pregunta por los dos:

kubectl exec cliente -- nslookup web
kubectl exec cliente -- nslookup web.otro
kubectl exec cliente -- nslookup web.otro.svc.cluster.local

El nombre corto se queda en casa; el nombre con namespace cruza la frontera. Este es el mecanismo que hace que los namespaces sean de verdad espacios de nombres: dos equipos pueden llamar web a sus servicios sin pisarse, y cada uno resuelve el suyo por defecto.

Note

💡 Y no solo los Services tienen nombre. Los Pods de un StatefulSet reciben el suyo a través del Service headless (db-0.db.tienda.svc.cluster.local, como viste), y cada puerto con nombre de un Service genera además un registro SRV (_<puerto>._<protocolo>.<servicio>.<namespace>.svc.cluster.local) que los clientes capaces de descubrimiento por SRV consultan para obtener puerto e IP de una vez. Tu web no sirve para probarlo, porque kubectl expose deja el puerto sin nombre y sin nombre no hay SRV. El propio CoreDNS sí lo tiene, así que pregúntale por él mismo: kubectl exec cliente -- nslookup -type=srv _dns._udp.kube-dns.kube-system.svc.cluster.local.

Paso 4: ExternalName, el Service que no tiene Pods

Última pieza, y la más rara de la familia Service: uno que no selecciona nada y no reparte tráfico. Crea externo.yaml:

cat << 'EOF' > externo.yaml
apiVersion: v1
kind: Service
metadata:
  name: externo
spec:
  type: ExternalName
  externalName: example.com
EOF

El YAML, en tres preguntas

¿Dónde está el selector?

No hay, y tampoco hay ClusterIP, ni endpoints, ni kube-proxy metido de por medio. Un ExternalName no es un proxy: es una entrada en el DNS. Cuando alguien pregunta por externo.tienda.svc.cluster.local, CoreDNS responde con un CNAME hacia example.com y se lava las manos. La conexión la hace el cliente directamente contra el destino.

¿Y para qué sirve entonces?

Para dar un nombre estable dentro del clúster a algo que vive fuera: la base de datos gestionada del proveedor de cloud, una API de terceros, un sistema heredado. Tu aplicación habla siempre con db.tienda.svc.cluster.local, y el día que esa base de datos se migre dentro del clúster, cambias el Service de ExternalName a uno normal con selector y la aplicación no se entera. Es una capa de indirección gratis.

¿Alguna pega?

Dos. Al ser un CNAME, no funciona bien con protocolos que no sean por nombre (por ejemplo, un cliente que valide un certificado TLS verá el nombre externo, no el interno). Y como no pasa por kube-proxy, las NetworkPolicies de salida no lo filtran por Service: el tráfico sale hacia una IP externa cualquiera.

kubectl apply -f externo.yaml
kubectl get service externo
kubectl exec cliente -- nslookup externo.tienda.svc.cluster.local

Fíjate en la columna CLUSTER-IP: vacía. Y en la respuesta del nslookup: un CNAME.

Resumen

  • CoreDNS es un Deployment más, con su ConfigMap (el Corefile) y su Service kube-dns. Se puede escalar, actualizar y romper como cualquier otro.
  • El nombre canónico es <servicio>.<namespace>.svc.cluster.local; el nombre corto funciona gracias a la lista search del /etc/resolv.conf.
  • ndots:5 hace que los nombres externos generen consultas fallidas de más. El punto final (example.com.) las evita.
  • Los namespaces son espacios de nombres de verdad: dos Services pueden llamarse igual.
  • ExternalName es un CNAME con nombre de Service: la indirección para lo que vive fuera del clúster.
Previous lesson
NetworkPolicy