Cómo dividir DNS entre Kubernetes y un sidecar de OpenVPN
Un pod sidecar necesita dos mundos de nombres. Apunte un resolutor de loopback al clúster y al túnel, y deje de depender de un resolv.conf editado.
Los pods que ejecutan un sidecar OpenVPN fallan la resolución de nombres con un patrón reconocible: los nombres del clúster se cuelgan, los nombres remotos responden hasta que el pod se reinicia, y el archivo de resolutor que editó se revierte. Nada en esa secuencia es un fallo del túnel. Un pod tiene un archivo de resolutor y dos mundos de nombres, y el archivo puede listar servidores en orden pero no puede expresar zonas. La solución duradera es un resolutor que se ejecuta dentro del pod y es dueño de la división, de modo que el archivo sigue siendo trivial y ninguna política tiene motivo para reescribirlo.
Por qué la edición que hizo sigue desapareciendo
El archivo pertenece al kubelet, no a usted. Con dnsPolicy: ClusterFirst el kubelet lo genera para apuntar al DNS del clúster y lo regenera cada vez que arranca un contenedor. Todo lo que cambie mediante kubectl exec vive en la capa de escritura del contenedor, así que un reinicio, un desalojo o un despliegue lo descarta. La especificación del pod sigue llevando la misma política, por eso vuelve el mismo archivo.
Poner dnsPolicy: None detiene la regeneración y también elimina la resolución del clúster. Gana un archivo que controla y pierde los nombres que necesitaba. Ese intercambio es todo el problema: resolv.conf no tiene noción de zonas, solo de servidores que probar en orden, así que no puede enviar cluster.local a una dirección y una zona privada a otra.
Ejecute un resolutor en el pod, en loopback
Apunte el pod a un único servidor de nombres en loopback y luego deje que un contenedor CoreDNS que comparte el espacio de nombres de red del pod responda en el puerto 53. Fije la política en None, entregue al pod esa única dirección y añada el contenedor DNS junto al VPN y a la aplicación:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- 127.0.0.1
options:
- name: ndots
value: "5"
containers:
- name: dns
image: coredns/coredns
args: ["-conf", "/etc/coredns/Corefile"]
volumeMounts:
- {name: corefile, mountPath: /etc/coredns, readOnly: true}
- name: vpn
image: your-openvpn-client-image
- name: app
image: your-app-image
volumes:
- name: corefile
configMap:
name: split-dns-corefileLos tres contenedores comparten un único espacio de nombres de red, así que 127.0.0.1:53 desde el contenedor de la aplicación llega a CoreDNS sin servicio, sin port-forward y sin rodeo por la red del clúster. Montar el Corefile desde un ConfigMap lo mantiene como una entrada versionada en lugar de un archivo editado en el sitio. El resolv.conf generado contiene entonces un solo servidor de nombres y nada más, que es un estado sobre el que puede razonar.
Dos detalles muerden aquí. La imagen oficial enlaza el puerto 53 como root, y algunas políticas del clúster le obligan a escribirlo explícitamente. Cada contenedor del pod ahora depende de que ese único contenedor esté en marcha, así que añada una sonda de disponibilidad contra un nombre inofensivo y deje que la aplicación tolere fallos de arranque.
Dé a cada zona privada su propio upstream
Un Corefile es una lista de zonas, cada una nombra el servidor que puede responderla. Un bloque cubre la zona del túnel, y el bloque que lo atrapa todo va al final y toma todo lo que ningún bloque explícito reclamó. El bloque que lo atrapa todo debe apuntar a un upstream real, nunca de vuelta al pod: .:53 { forward . 10.100.0.10 } reutiliza el resolutor del clúster porque ese resolutor ya reenvía los nombres públicos a los resolutores del propio nodo. Nada más en el pod necesita saber a qué red pertenece un nombre.
.:53 {
errors
cache 30
forward . 10.100.0.10
}
svc.acme.internal:53 {
errors
cache 30
forward . 192.168.240.1
}Lea la dirección del clúster del servicio kube-dns en kube-system en lugar de copiar una de un artículo, porque los CIDR de servicio difieren. La dirección del túnel viene del valor dhcp-option DNS que envía el servidor OpenVPN, o del servidor autoritativo de esa zona. Si su resolutor del clúster no responde nombres públicos, ponga la dirección del resolutor del nodo en el bloque que lo atrapa todo, leída en el nodo y no dentro del pod.
No apunte el bloque que lo atrapa todo a /etc/resolv.conf, que es la línea única tentadora y una trampa en esta configuración exacta. Con dnsPolicy: None el archivo del propio pod contiene el único servidor de nombres 127.0.0.1, así que ese objetivo es el propio CoreDNS y cada consulta no coincidente recurre hasta agotarse. Un bloque llamado svc.acme.internal coincide con ese nombre y con cada nombre debajo, lo que cubre db.svc.acme.internal y excluye svc.acme.internal.example.net. El punto final es cosmético, porque los nombres de zona se normalizan a la forma plenamente cualificada; el límite del sufijo es lo que decide dónde cae una consulta.
Decida si el reenvío pertenece al resolutor del clúster
Si cada carga de trabajo del espacio de nombres necesita la zona del túnel, un bloque de reenvío en el ConfigMap de CoreDNS bajo kube-system es más barato que un sidecar por pod. Ese cambio es de alcance de clúster: requiere derechos de administrador y un reinicio de un despliegue del que todos dependen. Limítelo a esa única zona, nunca a la raíz. La dirección del túnel de una sola carga de trabajo se convierte entonces en el resolutor de cada pod para la zona, y el día que cae el túnel, todo el clúster pierde el nombre.
El sidecar mantiene el radio de explosión dentro de un solo pod, que es el mejor valor por defecto cuando solo unos pocos servicios necesitan la red remota. Elija lo que elija, anote qué resolutor es dueño de la zona, porque la siguiente persona que depure un tiempo de espera lo preguntará.
Nombres que se resuelven mientras las conexiones aún fallan
La resolución es una de las dos cosas que el túnel debe llevar; la otra es el camino de vuelta, y la red del pod lo esconde bien. Si el propio servidor OpenVPN se ejecuta como pod, las reglas que añada dentro de ese contenedor no gobiernan el tráfico que sale por el nodo. Ese fallo es estrecho: solo los pods del nodo que también ejecutaba el servidor obtenían respuestas, y todo lo demás expiró.
Ejecutar el servidor con hostNetwork: true lo devuelve al espacio de nombres del nodo, donde una regla FORWARD y la traducción de origen surten efecto. Traduzca el pool de clientes a la salida, o ningún nodo remoto tendrá ruta de vuelta a una dirección de túnel como 10.8.0.6. Compruebe ambos sentidos antes de tocar de nuevo el Corefile. Un túnel que lleva la consulta y no la respuesta produce exactamente el mismo tiempo de espera que un resolutor que nunca recibió la consulta.
Verifique desde un pod nuevo, no desde la sesión que arregló
Pregunte explícitamente por ambas zonas y luego repita la consulta después de que el pod se reemplace. La configuración que solo existe en un contenedor en marcha no es configuración:
kubectl exec -it deploy/app -- dig @127.0.0.1 kubernetes.default.svc.cluster.local +short
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short
kubectl delete pod -l app=your-app
kubectl wait --for=condition=Ready pod -l app=your-app --timeout=90s
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +shortSi el primer par responde y el par tras el reemplazo no, el Corefile está bien y la especificación del pod no. Si ambos responden y la aplicación sigue sin poder conectar, deje de depurar DNS y pruebe la ruta TCP con nc -vz db.svc.acme.internal 5432 desde dentro del contenedor. Guarde la salida de dig en el tique, porque es el artefacto que separa un problema de nombres de un problema de enrutamiento.