Comment répartir le DNS entre Kubernetes et un sidecar OpenVPN
Un pod sidecar a besoin de deux mondes de noms. Pointez un résolveur loopback vers le cluster et le tunnel, et cessez de dépendre d’un resolv.conf modifié.
Les pods qui exécutent un sidecar OpenVPN échouent la résolution de noms selon un schéma reconnaissable : les noms du cluster se bloquent, les noms distants répondent jusqu’au redémarrage du pod, et le fichier de résolveur que vous avez modifié revient en arrière. Rien dans cette séquence n’est un défaut du tunnel. Un pod a un fichier de résolveur et deux mondes de noms, et le fichier peut lister des serveurs dans l’ordre mais ne peut pas exprimer des zones. La correction durable est un résolveur qui s’exécute dans le pod et possède la répartition, de sorte que le fichier reste trivial et qu’aucune politique n’a de raison de le réécrire.
Pourquoi la modification que vous avez faite ne cesse de disparaître
Le fichier appartient au kubelet, pas à vous. Sous dnsPolicy: ClusterFirst, le kubelet le génère pour pointer vers le DNS du cluster et le régénère à chaque démarrage de conteneur. Tout ce que vous modifiez via kubectl exec vit dans la couche inscriptible du conteneur, donc un redémarrage, une éviction ou un déploiement le jettent. La spécification du pod porte toujours la même politique, ce qui explique le retour du même fichier.
Définir dnsPolicy: None arrête la régénération et supprime aussi la résolution du cluster. Vous gagnez un fichier que vous contrôlez et perdez les noms dont vous aviez besoin. Cet échange est tout le problème : resolv.conf n’a pas la notion de zones, seulement de serveurs à essayer dans l’ordre, il ne peut donc pas envoyer cluster.local vers une adresse et une zone privée vers une autre.
Exécutez un résolveur dans le pod, sur loopback
Pointez le pod vers un seul serveur de noms sur loopback, puis laissez un conteneur CoreDNS qui partage l’espace de noms réseau du pod répondre sur le port 53. Réglez la politique sur None, donnez au pod cette adresse unique, et ajoutez le conteneur DNS à côté du VPN et de l’application :
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-corefileLes trois conteneurs partagent un même espace de noms réseau, donc 127.0.0.1:53 depuis le conteneur applicatif atteint CoreDNS sans service, sans port-forward et sans détour par le réseau du cluster. Monter le Corefile depuis une ConfigMap le garde comme une entrée versionnée au lieu d’un fichier modifié sur place. Le resolv.conf généré ne contient alors qu’un seul serveur de noms et rien d’autre, un état sur lequel vous pouvez raisonner.
Deux détails mordent ici. L’image officielle lie le port 53 en tant que root, et certaines politiques de cluster vous obligent à l’écrire explicitement. Chaque conteneur du pod dépend désormais de l’état de ce seul conteneur, alors ajoutez une sonde de disponibilité sur un nom inoffensif et laissez l’application tolérer les échecs de démarrage.
Donnez à chaque zone privée son propre upstream
Un Corefile est une liste de zones, chacune nommant le serveur qui peut y répondre. Un bloc couvre la zone du tunnel, et le bloc attrape-tout vient en dernier et prend tout ce qu’aucun bloc explicite n’a revendiqué. Le bloc attrape-tout doit viser un véritable upstream, jamais renvoyer vers le pod : .:53 { forward . 10.100.0.10 } réutilise le résolveur du cluster parce que ce résolveur transfère déjà les noms publics aux résolveurs du nœud lui-même. Rien d’autre dans le pod n’a besoin de savoir à quel réseau appartient un nom.
.:53 {
errors
cache 30
forward . 10.100.0.10
}
svc.acme.internal:53 {
errors
cache 30
forward . 192.168.240.1
}Lisez l’adresse du cluster sur le service kube-dns dans kube-system au lieu d’en copier une depuis un article, car les CIDR de service diffèrent. L’adresse du tunnel vient de la valeur dhcp-option DNS que pousse le serveur OpenVPN, ou du serveur autoritatif de cette zone. Si votre résolveur de cluster ne répond pas aux noms publics, mettez l’adresse du résolveur du nœud dans le bloc attrape-tout, lue sur le nœud et non dans le pod.
Ne pointez pas le bloc attrape-tout vers /etc/resolv.conf, qui est la ligne unique tentante et un piège dans cette configuration précise. Sous dnsPolicy: None, le fichier du pod lui-même contient l’unique serveur de noms 127.0.0.1, donc cette cible est CoreDNS lui-même et chaque requête non appariée récurse jusqu’au délai d’expiration. Un bloc nommé svc.acme.internal correspond à ce nom et à chaque nom en dessous, ce qui couvre db.svc.acme.internal et exclut svc.acme.internal.example.net. Le point final est cosmétique, car les noms de zone sont normalisés en forme pleinement qualifiée ; la frontière du suffixe décide où une requête atterrit.
Décidez si le transfert appartient au résolveur du cluster
Si chaque charge de travail de l’espace de noms a besoin de la zone du tunnel, un bloc de transfert dans la ConfigMap CoreDNS sous kube-system coûte moins cher qu’un sidecar par pod. Ce changement est à l’échelle du cluster : il exige des droits d’administrateur et un redémarrage d’un déploiement dont tout le monde dépend. Limitez-le à cette seule zone, jamais à la racine. L’adresse du tunnel d’une seule charge de travail devient alors le résolveur de chaque pod pour la zone, et le jour où le tunnel tombe, tout le cluster perd le nom.
Le sidecar garde le rayon d’explosion dans un seul pod, ce qui est le meilleur choix par défaut quand seuls quelques services ont besoin du réseau distant. Quel que soit votre choix, notez quel résolveur possède la zone, car la prochaine personne qui déboggera un délai d’expiration le demandera.
Des noms qui se résolvent alors que les connexions échouent encore
La résolution est l’une des deux choses que le tunnel doit transporter ; l’autre est le chemin de retour, et le réseau du pod le cache bien. Si le serveur OpenVPN lui-même tourne comme pod, les règles que vous ajoutez dans ce conteneur ne régissent pas le trafic qui sort par le nœud. Cette défaillance est étroite : seuls les pods du nœud qui exécutait aussi le serveur obtenaient des réponses, et tout le reste expirait.
Exécuter le serveur avec hostNetwork: true le remet dans l’espace de noms du nœud, où une règle FORWARD et la traduction de source prennent effet. Traduisez le pool de clients à la sortie, sinon aucun nœud distant n’a de route de retour vers une adresse de tunnel comme 10.8.0.6. Vérifiez les deux sens avant de toucher à nouveau le Corefile. Un tunnel qui transporte la requête et pas la réponse produit exactement le même délai d’expiration qu’un résolveur qui n’a jamais reçu la requête.
Vérifiez depuis un nouveau pod, pas depuis la session que vous avez réparée
Interrogez explicitement les deux zones, puis répétez la requête après le remplacement du pod. Une configuration qui n’existe que dans un conteneur en cours d’exécution n’est pas une configuration :
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 la première paire répond et que la paire après le remplacement ne répond pas, le Corefile est correct et la spécification du pod ne l’est pas. Si les deux répondent et que l’application ne peut toujours pas se connecter, cessez de déboguer le DNS et testez le chemin TCP avec nc -vz db.svc.acme.internal 5432 depuis l’intérieur du conteneur. Conservez la sortie de dig dans le ticket, car c’est l’artefact qui sépare un problème de noms d’un problème de routage.