Le DNS du cluster
Pourquoi
Presque tout appel dans un cluster commence par une résolution de nom : Signalements trouve PostgreSQL par postgresql, un client appelle signalements.signalements.svc, une tâche contacte une API externe. Le DNS est donc sur le chemin de chaque connexion, et quand il ralentit ou échoue, tout semble tomber en panne à la fois, avec des symptômes trompeurs : des délais de 5 secondes, des erreurs Temporary failure in name resolution, des pods qui « marchent » une fois sur dix.
La leçon Services et DNS a présenté les noms de Services et l'effet de ndots:5. Ici, on va jusqu'au serveur : comment CoreDNS est configuré, quels enregistrements il publie, ce que le résolveur d'un pod envoie vraiment, d'où viennent les fameux 5 secondes, et comment dimensionner et surveiller le service. Les notions générales du DNS (zones, enregistrements, TTL) relèvent du chapitre Fondations ; on s'attache à ce que Kubernetes y ajoute.
Les concepts
CoreDNS et son Corefile
Le DNS d'un cluster est assuré par CoreDNS (glossaire), un serveur DNS écrit en Go dont chaque fonction est un plugin assemblé dans un fichier de configuration, le Corefile. Dans un cluster, c'est un Deployment ordinaire du namespace kube-system (par défaut deux répliques sur un cluster kubeadm, et sur kind), exposé par un Service dont l'adresse est écrite dans le resolv.conf de chaque pod. Pour des raisons historiques, ce Service s'appelle kube-dns et ses pods portent l'étiquette k8s-app=kube-dns, même si le serveur est CoreDNS. Le Corefile est dans la ConfigMap coredns de kube-system. Voici le Corefile par défaut, tel que le donne la documentation de Kubernetes :
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}.:53 : un bloc de serveur pour toutes les zones (.), sur le port 53. Les plugins s'exécutent dans un ordre défini à la compilation de CoreDNS, pas dans l'ordre d'écriture, ce qui compte parce que cache doit s'exécuter avant kubernetes et forward pour mettre leurs réponses en cache. Leur rôle :
| Plugin | Rôle |
|---|---|
errors | Écrit les erreurs sur la sortie standard |
health | Expose /health (port 8080) ; lameduck 5s : à l'arrêt, le serveur se déclare d'abord non sain et attend 5 s avant de s'éteindre, pour laisser les clients changer de serveur |
ready | Expose /ready (port 8181) : 200 quand tous les plugins sont prêts. C'est la sonde de disponibilité du pod |
kubernetes | Répond pour les zones du cluster (cluster.local, et les zones inverses) à partir des Services, EndpointSlices et pods de l'API. pods insecure publie des enregistrements de pods sans vérifier qu'ils existent ; fallthrough laisse passer au plugin suivant les requêtes inverses sans réponse ; ttl 30 fixe la durée de vie des réponses (30 s ici ; 5 s par défaut du plugin) |
prometheus | Expose les métriques sur le port 9153 |
forward . /etc/resolv.conf | Transmet tout ce qui n'est pas du cluster aux serveurs listés dans le resolv.conf du nœud (les résolveurs du fournisseur ou de l'entreprise) |
cache 30 | Met en cache les réponses, au plus 30 s |
loop | Détecte les boucles de transmission simples et arrête CoreDNS en cas de détection |
reload | Recharge le Corefile quand la ConfigMap change (délai d'environ 2 minutes) |
loadbalance | Mélange l'ordre des enregistrements A, AAAA et MX d'une réponse (tourniquet DNS) |
Le plugin loop mérite un mot : si le resolv.conf du nœud désigne le serveur local (127.0.0.53, le cas des machines Ubuntu avec systemd-resolved), CoreDNS se transmettrait ses propres requêtes à l'infini. loop le détecte au démarrage et arrête le processus, avec un message explicite (Loop ... detected) ; la correction est de pointer forward vers les vrais résolveurs, ou de configurer le kubelet (resolvConf) pour qu'il fournisse le bon fichier aux pods.
Les enregistrements publiés
Le plugin kubernetes fabrique les réponses à partir de l'état de l'API.
Service ordinaire (ClusterIP). Le nom <service>.<namespace>.svc.cluster.local donne un enregistrement A (l'adresse du Service) et, sur un cluster double pile, un AAAA. Pour chaque port nommé, un enregistrement SRV _<port>._<protocole>.<service>.<namespace>.svc.cluster.local donne le numéro de port et le nom du Service. Pour Signalements : _http._tcp.signalements.signalements.svc.cluster.local.
Service headless (clusterIP: None). Le nom renvoie plusieurs enregistrements A : les adresses de tous les pods prêts. Les SRV donnent un enregistrement par pod. Pour un StatefulSet, chaque pod a de plus un nom stable <pod>.<service>.<namespace>.svc.cluster.local.
Pods. Avec pods insecure, un pod a un enregistrement A de la forme <adresse-avec-tirets>.<namespace>.pod.cluster.local (par exemple 10-244-1-5.signalements.pod.cluster.local) : le nom est dérivé de l'adresse, il ne prouve pas qu'un pod existe, et personne ne devrait s'en servir pour joindre une application (il change à chaque recréation du pod).
Service ExternalName. Un enregistrement CNAME vers le nom externe.
Les requêtes inverses (adresse vers nom, zones in-addr.arpa et ip6.arpa) sont traitées pour les adresses de Services et de pods du cluster, puis passées au plugin suivant sinon (le fallthrough du Corefile).
Le resolv.conf d'un pod
Le kubelet écrit le /etc/resolv.conf de chaque pod d'après sa dnsPolicy. Pour la politique par défaut, ClusterFirst, il y met :
nameserver 10.96.0.10
search signalements.svc.cluster.local svc.cluster.local cluster.local
options ndots:5L'adresse du nameserver est celle du Service kube-dns (ici l'adresse de la plage de Services de kind, d'illustration). Remarquez que le résolveur ne parle pas à un pod CoreDNS : il parle à une adresse virtuelle, traduite par kube-proxy ou le plugin (leçons 3 et 4) vers l'un des pods CoreDNS. Le DNS dépend donc du mécanisme des Services : ce point est la source de plusieurs pannes, vues plus loin.
La documentation limite la liste de recherche : six domaines au plus, 256 caractères au total, et 32 options. Ajouter des domaines de recherche par dnsConfig peut dépasser ces limites, et le pod est alors refusé avec un événement.
ndots:5 et la cascade de requêtes
La règle du résolveur : si le nom demandé contient moins de ndots points, il est essayé d'abord avec chaque domaine de recherche, dans l'ordre, puis tel quel ; s'il en contient au moins ndots, il est essayé tel quel d'abord. Avec ndots:5, presque tous les noms externes comptent moins de cinq points : api.example.com (2 points) déclenche la cascade.
Pour un pod du namespace signalements qui résout api.example.com, le résolveur envoie, en demandant à chaque fois un A et un AAAA :
api.example.com.signalements.svc.cluster.local:NXDOMAINapi.example.com.svc.cluster.local:NXDOMAINapi.example.com.cluster.local:NXDOMAINapi.example.com: la bonne réponse
Soit huit requêtes dont six inutiles. Les NXDOMAIN sont rapides (CoreDNS répond depuis sa connaissance du cluster), mais la multiplication pèse sur CoreDNS, sur le suivi de connexion du nœud, et ajoute de la latence à chaque nouvelle connexion sortante. À l'inverse, un nom du cluster comme signalements n'a aucun point : la recherche trouve dès le premier domaine.
Trois remèdes, du plus local au plus global :
- Le FQDN avec point final :
api.example.com.est un nom absolu : le résolveur ne le complète pas. C'est le réglage idéal pour une configuration d'application, mais tous les clients ne l'acceptent pas (certaines bibliothèques HTTP et la validation TLS du nom d'hôte supportent mal le point final) : testez. dnsConfig.optionsavecndotsabaissé dans le pod. Avecndots:2,api.example.com(2 points) est tenté en premier tel quel. Contrepartie : un nom du cluster à deux points ou plus (signalements.signalements.svc) est lui aussi d'abord tenté tel quel, donc unNXDOMAINde plus avant la recherche. On ajuste selon ce que le pod appelle le plus.- Un cache plus proche (NodeLocal DNSCache, ci-dessous) : on n'évite pas les requêtes, on les rend peu coûteuses.
dnsPolicy et dnsConfig
Le champ dnsPolicy du Pod choisit l'origine de la configuration DNS :
| Valeur | Effet |
|---|---|
ClusterFirst (défaut) | Le DNS du cluster d'abord ; les noms hors du domaine du cluster sont transmis plus loin par CoreDNS |
Default | Le pod hérite du resolv.conf du nœud : il ne résout pas les noms du cluster. (Le nom prête à confusion : ce n'est pas la valeur par défaut) |
ClusterFirstWithHostNet | Pour un pod en hostNetwork: true, qui sans cela aurait Default : lui redonne la politique ClusterFirst |
None | Aucune configuration automatique : tout vient de dnsConfig |
dnsConfig ajoute ou, avec None, définit les serveurs, domaines de recherche et options. Un pod de Signalements qui appelle beaucoup de noms externes (un service d'envoi d'e-mails, une API partenaire) peut abaisser ndots sans toucher au reste :
apiVersion: v1
kind: Pod
metadata:
name: signalements-ndots
namespace: signalements
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "2"
containers:
- name: app
image: registry.example.com/signalements:1.4.0Avec dnsPolicy: ClusterFirst, le champ dnsConfig est fusionné : les serveurs et domaines du cluster sont conservés, et l'option ndots est remplacée. Dans un Deployment, ce bloc va sous spec.template.spec. Pour un pod de diagnostic qui doit ignorer le cluster et interroger un serveur précis, on utilise dnsPolicy: None avec nameservers et searches explicites.
NodeLocal DNSCache
NodeLocal DNSCache (stable depuis Kubernetes 1.18) est un DaemonSet qui place un cache DNS sur chaque nœud, joignable par une adresse locale de lien (par convention 169.254.20.10). Les pods le contactent à la place du Service kube-dns ; en cas d'absence en cache, il interroge CoreDNS. D'après la documentation, il :
- évite la traduction d'adresse et le suivi de connexion pour les requêtes des pods vers le cache (l'adresse locale n'est pas un Service) ;
- interroge CoreDNS en TCP, ce qui évite les pertes de paquets UDP et nettoie proprement les connexions ;
- évite la saturation de la table conntrack par les requêtes UDP ;
- réactive la mise en cache des réponses négatives (
NXDOMAIN), ce qui amortit directement le coût dendots:5; - fournit des métriques par nœud.
Le mode de déploiement diffère selon kube-proxy : en iptables, le cache écoute sur l'adresse locale et sur l'adresse du Service kube-dns (le noyau détourne vers lui, sans changer les pods) ; en ipvs, il faut modifier l'option --cluster-dns du kubelet. Avec Cilium en remplacement de kube-proxy, la configuration d'un cache local passe par un mécanisme propre (Local Redirect Policy) décrit dans la documentation de Cilium, qu'il faut lire avant de déployer le manifeste générique.
En pratique
Sur le cluster kind formation ; les commandes sont décrites avec ce que vous devez observer.
Lire la configuration
$ kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}'
$ kubectl -n kube-system get deployment coredns
$ kubectl -n kube-system get service kube-dns
La première commande affiche le Corefile (celui de kubeadm, proche de celui de la documentation) ; la deuxième montre le nombre de répliques ; la troisième, l'adresse du Service, que vous retrouvez dans le resolv.conf de n'importe quel pod.
Interroger comme un pod
Lancez un pod outillé (l'image nicolaka/netshoot contient dig) :
$ kubectl run diag --rm -it --restart=Never --image=nicolaka/netshoot -n signalements -- bash
bash-5.2# cat /etc/resolv.conf
bash-5.2# dig +short signalements
bash-5.2# dig +search +short signalements
bash-5.2# dig +short signalements.signalements.svc.cluster.local
bash-5.2# dig +short SRV _http._tcp.signalements.signalements.svc.cluster.local
dig n'applique pas les domaines de recherche par défaut : le nom court signalements sans +search n'est pas complété et ne répond rien. +search les active ; +short limite l'affichage aux réponses. Les deux dernières commandes interrogent les noms complets : l'A donne l'adresse ClusterIP, le SRV donne le port et le nom du Service. Pour la section de réponse complète avec le TTL, retirez +short : le TTL doit être celui du plugin kubernetes (30 s dans le Corefile par défaut de la documentation, ou la valeur de votre configuration).
Voir la cascade de ndots
Pour constater les requêtes multiples, il faut regarder ce que reçoit CoreDNS. Activez temporairement le plugin log :
$ kubectl -n kube-system edit configmap coredns
Ajoutez la ligne log dans le bloc .:53, enregistrez : reload applique le changement sous environ 2 minutes. Dans un pod de diagnostic, résolvez un nom externe sans dig, avec le résolveur du système :
bash-5.2# getent hosts api.example.com
$ kubectl -n kube-system logs -l k8s-app=kube-dns --tail=30
getent hosts passe par le résolveur de la bibliothèque C, comme une application. Les journaux de CoreDNS montrent alors les requêtes pour api.example.com.signalements.svc.cluster.local, ...svc.cluster.local, ...cluster.local, puis api.example.com, en A et AAAA (selon l'implémentation du résolveur) : les trois premières avec le code NXDOMAIN. Comparez avec getent hosts api.example.com. (point final) : une seule requête, la bonne. Retirez ensuite log : il journalise chaque requête, ce qui est coûteux et expose les noms interrogés.
Changer ndots et mesurer
Appliquez le pod signalements-ndots de la section précédente (avec une image réelle), et lisez son fichier :
$ kubectl exec -n signalements signalements-ndots -- cat /etc/resolv.conf
La ligne options affiche ndots:2, et les lignes nameserver et search sont inchangées (fusion).
Interroger CoreDNS directement et lire ses métriques
$ kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
$ kubectl -n kube-system port-forward deploy/coredns 9153:9153 &
$ curl -s localhost:9153/metrics | grep -E '^coredns_dns_(requests|responses)_total'
Vous voyez des compteurs par type de requête (A, AAAA, SRV...) et par code de réponse (NOERROR, NXDOMAIN...). La proportion de NXDOMAIN d'un cluster dont les pods appellent des noms externes est typiquement élevée à cause de ndots:5.
Déployer NodeLocal DNSCache (principe)
La documentation fournit un manifeste de référence (nodelocaldns.yaml) dont on remplace trois variables : l'adresse locale (169.254.20.10), le domaine (cluster.local) et l'adresse du Service kube-dns. En mode kube-proxy iptables, les remplacements sont faits par sed, puis kubectl create -f nodelocaldns.yaml. Sur un cluster Cilium avec remplacement de kube-proxy, n'appliquez pas ce manifeste tel quel : suivez le guide Node-Local DNS Cache de la documentation de Cilium, qui utilise une CiliumLocalRedirectPolicy. Après déploiement, un DaemonSet node-local-dns tourne sur chaque nœud, et les métriques des caches apparaissent.
Sous le capot
Le trajet d'une requête. Un processus du pod appelle getaddrinfo, qui lit /etc/resolv.conf et envoie un paquet UDP (port 53) vers l'adresse du Service kube-dns. Ce paquet suit le chemin de la leçon 3 : DNAT vers un pod CoreDNS, conntrack mémorise la traduction. CoreDNS lit la requête ; si le nom appartient à cluster.local, le plugin kubernetes répond depuis un cache en mémoire alimenté par l'API (CoreDNS maintient des watches sur les Services, EndpointSlices et, avec pods insecure, les pods : il n'interroge pas l'API à chaque requête) ; sinon forward l'envoie aux résolveurs du nœud. Le pod CoreDNS lui-même a dnsPolicy: Default, pour éviter de se résoudre par lui-même.
La répartition entre pods CoreDNS. Comme toute connexion vers un Service, chaque requête UDP crée une entrée de conntrack qui fixe le pod CoreDNS choisi pour ce quintuplet. Un résolveur qui réutilise le même port source envoie toutes ses requêtes au même pod pendant la durée de l'entrée (300 s en UDP par défaut, leçon 4). On voit alors un pod CoreDNS chargé alors que les autres sont au repos.
Pourquoi 5 secondes. Les bibliothèques C (glibc, musl) envoient les requêtes A et AAAA en parallèle depuis le même socket : mêmes adresse et port source, même destination (l'adresse du Service). Si les deux paquets partent à quelques microsecondes d'écart, avant que la première traduction DNAT ne soit inscrite dans conntrack, les deux créent une entrée concurrente ; une seule insertion réussit, et le noyau jette l'autre paquet (compteur insert_failed). Le client ne reçoit jamais de réponse à cette requête et réessaie après le délai du résolveur, 5 secondes par défaut (timeout:5 dans resolv.conf). Le correctif du noyau a réduit la fréquence du problème sans le supprimer dans toutes les configurations : on reste prudent. Tout ce qui évite le DNAT et conntrack pour le DNS règle le problème à la racine, ce que fait NodeLocal DNSCache.
Pièges courants
Un timeout de 5 secondes, intermittent. Le symptôme canonique de la course conntrack. Diagnostic : conntrack -S sur le nœud (insert_failed croissant), requêtes DNS qui prennent précisément ~5 s. Remèdes, par ordre de préférence : NodeLocal DNSCache ; sinon options single-request-reopen (glibc) ou use-vc dans dnsConfig (requêtes en TCP). Notez que single-request-reopen n'a aucun effet avec musl (Alpine).
Temporary failure in name resolution sur un nom externe seulement. CoreDNS ne parvient pas à joindre les résolveurs en amont (le forward pointe vers le resolv.conf du nœud) : pare-feu, résolveurs inaccessibles depuis les pods, ou boucle. Test : depuis un pod, dig @<résolveur amont> example.com, et dans les journaux de CoreDNS, les erreurs i/o timeout du forward.
CoreDNS en CrashLoopBackOff avec Loop ... detected. Le plugin loop a détecté que le resolv.conf du nœud pointe sur un serveur local (127.0.0.53). Corrigez le forward pour viser les vrais résolveurs, ou la configuration resolvConf du kubelet.
Un CoreDNS sous-dimensionné. Deux répliques pour des centaines de pods très actifs : latence en hausse, requêtes perdues, coredns_dns_request_duration_seconds qui s'étire. Voir la section En production.
Un nom court qui marche ici et pas là. signalements ne se résout que dans le namespace signalements (domaines de recherche) ; depuis un autre namespace, signalements.signalements est le minimum.
Le pod en hostNetwork qui ne résout pas les Services. Sans dnsPolicy: ClusterFirstWithHostNet, il utilise le DNS du nœud.
Un Corefile modifié sans effet. reload prend environ deux minutes ; si CoreDNS refuse la nouvelle configuration (erreur de syntaxe), il garde l'ancienne et le signale dans ses journaux. Consultez-les après toute modification.
Dépasser la limite de domaines de recherche. Plus de six domaines : le kubelet émet un événement d'avertissement (Search Line limits were exceeded) et tronque la liste. Un domaine de recherche manquant fait échouer silencieusement certains noms courts.
Sécurité
- Le DNS révèle la topologie. Tout pod peut énumérer les noms de Services (par requêtes SRV, par balayage de noms ou de zones inverses). Ce n'est pas un secret, mais ne mettez pas d'information sensible dans un nom de Service, de namespace ou de port.
- Les NetworkPolicies et le DNS. Un pod en refus de sortie par défaut doit pouvoir joindre CoreDNS (UDP et TCP 53, vers
kube-system, podk8s-app=kube-dns), sinon plus rien ne résout : c'est l'erreur la plus fréquente de la leçon 8. Autorisez le DNS explicitement et rien de plus. - L'exfiltration par DNS. Un pod compromis peut faire sortir des données dans des noms de domaine (
donnees-encodees.attaquant.example), que CoreDNS transmet au résolveur amont : un canal qui échappe aux filtres de sortie ordinaires. Remèdes : journaliser et analyser les requêtes (volume, noms anormalement longs), et des politiques DNS de sortie par nom (par exemple les politiquestoFQDNsde Cilium) pour restreindre les noms autorisés. - L'empoisonnement de cache : un cache DNS qui accepte de fausses réponses redirige des clients. DNSSEC (voir DNSSEC) protège l'intégrité des réponses de la zone, mais le DNS interne d'un cluster n'est pas signé ; la protection est le contrôle du réseau (qui peut envoyer des paquets à CoreDNS) et des pods.
pods insecuren'est pas un secret : les noms de pods sont déductibles des adresses. Ne fondez jamais une décision d'accès sur eux (utilisez les identités de pod, par exemple le compte de service ou le TLS mutuel).- Qui modifie le Corefile contrôle la résolution de tous les pods. Restreignez l'écriture dans la ConfigMap
corednsdekube-systempar le RBAC, et suivez ses modifications dans Git.
En production
- Dimensionner CoreDNS : deux répliques sont un minimum de haute disponibilité, pas un dimensionnement. Le projet cluster-proportional-autoscaler ajuste le nombre de répliques selon le nombre de nœuds et de cœurs. Mettez des
requestsde ressources réalistes, unPodDisruptionBudget, et une répartition sur plusieurs nœuds (topologySpreadConstraints) : perdre l'unique nœud qui porte les deux répliques coupe tout le DNS. - Déployer NodeLocal DNSCache dès que les pods sont nombreux, que les appels sortants sont fréquents, ou que les timeouts de 5 secondes apparaissent : c'est le correctif le plus rentable. Sur Cilium, via sa politique de redirection locale.
- Régler
ndotspar charge de travail plutôt que globalement : abaissez-le pour les pods qui parlent surtout à l'extérieur, gardez 5 ailleurs. Dans les configurations d'application, écrivez les noms externes en FQDN quand le client le permet. - Surveiller CoreDNS. Le plugin
prometheusexpose, sur le port 9153, notammentcoredns_dns_requests_total(volume, par type),coredns_dns_responses_total(par code de réponse, pour suivreNXDOMAINetSERVFAIL),coredns_dns_request_duration_seconds(histogramme de latence), et pour le cachecoredns_cache_hits_totaletcoredns_cache_misses_total. Alertes utiles : un taux deSERVFAILnon nul, un percentile 99 de latence supérieur à quelques dizaines de millisecondes, une proportion deNXDOMAINen hausse subite. Une requête PromQL pour le taux de requêtes en échec :sum(rate(coredns_dns_responses_total{rcode="SERVFAIL"}[5m])). - Zones privées d'un client : pour que les pods résolvent les noms d'un réseau d'entreprise (l'accès VPN d'un client), ajoutez un bloc au Corefile qui transmet cette zone à son serveur DNS (
client.example:53 { forward . <adresse> }), plutôt que de changer leforwardglobal. Testez avant et après : une erreur de Corefile touche tous les pods.
Exercices
1. Compter les requêtes (niveau 200). Un pod du namespace signalements (ndots:5, résolveur envoyant A et AAAA) appelle smtp.relais.example.com, puis smtp.relais.example.com.. Combien de requêtes émet-il pour chacun, au plus, et lesquelles sont utiles ?
Solution
Premier nom : 3 points, moins que 5, donc recherche d'abord. Quatre noms : smtp.relais.example.com.signalements.svc.cluster.local, ...svc.cluster.local, ...cluster.local, puis smtp.relais.example.com : 4 x 2 = 8 requêtes, dont 2 utiles. Second nom, avec point final : nom absolu, pas de recherche : 2 requêtes (A et AAAA), toutes utiles. D'où l'intérêt de FQDN avec point final dans la configuration, quand le client les accepte.
2. Choisir ndots (niveau 200). Un pod de Signalements appelle surtout postgresql (même namespace) et l'API api.partenaire.example (2 points). Un collègue propose ndots:1. Quelles conséquences pour les deux noms ?
Solution
postgresql : aucun point, moins que ndots, donc la recherche s'applique : trouvé au premier domaine, comme avant. api.partenaire.example : 2 points, au moins ndots, donc tenté tel quel d'abord : une seule requête utile, plus de cascade. Contrepartie : un nom du cluster de la forme signalements.signalements (1 point, soit au moins ndots:1) est lui aussi tenté tel quel d'abord, ce qui produit un NXDOMAIN avant la recherche. Le résultat reste correct, au prix d'une requête de plus pour ces noms. ndots:2 aurait le même effet pour les noms à deux points comme signalements.signalements.svc, et un compromis se choisit selon ce que le pod appelle le plus.
3. Le timeout de 5 secondes (niveau 300). Des appels sortants depuis Signalements attendent parfois exactement 5 secondes avant de se connecter. conntrack -S montre insert_failed qui augmente. Expliquez, proposez le correctif de fond et un correctif rapide.
Solution
Le résolveur envoie A et AAAA en parallèle depuis le même socket vers l'adresse du Service kube-dns : le DNAT est inscrit dans conntrack au premier paquet, et deux insertions concurrentes pour le même quintuplet entrent en conflit, un paquet est jeté (insert_failed), et le client attend le délai de 5 secondes avant de réessayer. Correctif de fond : NodeLocal DNSCache (le pod parle à une adresse locale de nœud sans DNAT ni conntrack, et le cache parle à CoreDNS en TCP). Correctif rapide, à porter dans dnsConfig.options du pod : single-request-reopen (avec glibc : A et AAAA sur des sockets différents) ou use-vc (TCP). Aucun des deux n'est utile avec musl (Alpine) pour le premier.
4. Le DNS après un refus par défaut (niveau 200). Après l'application d'une NetworkPolicy qui refuse toute sortie du namespace signalements, l'application échoue avec Temporary failure in name resolution, mais l'adresse IP de la base répond au ping. Que s'est-il passé et que faut-il autoriser ?
Solution
La politique a coupé le trafic vers CoreDNS : les requêtes DNS (UDP et TCP 53) vers les pods k8s-app=kube-dns de kube-system sont refusées, donc plus aucun nom ne se résout, alors que les adresses IP restent joignables si une règle les autorise. Il faut ajouter une règle de sortie vers le namespace kube-system, pods portant k8s-app: kube-dns, ports 53 UDP et 53 TCP (avec NodeLocal DNSCache, vers l'adresse locale du cache en plus). Puis ajouter des règles précises pour la base et le reste (leçon 8).
Récapitulatif
- Le DNS du cluster est CoreDNS, un Deployment de
kube-systemderrière le Servicekube-dns, configuré par le Corefile (ConfigMapcoredns) :kubernetesrépond pour le cluster,forwardpour le reste,cacheallège,loopdétecte les boucles,healthetreadyservent aux sondes,prometheusaux métriques. - Un Service a un A (et AAAA) et des SRV pour ses ports nommés ; un Service headless renvoie les adresses des pods ; un pod a un nom dérivé de son adresse.
- Le resolv.conf d'un pod contient l'adresse du Service
kube-dns, trois domaines de recherche etndots:5; sousndots:5, un nom externe déclenche jusqu'à huit requêtes dont six inutiles. Remèdes : FQDN avec point final,dnsConfigavecndotsréduit, cache local. dnsPolicy:ClusterFirst(défaut),Default(DNS du nœud, sans les Services),ClusterFirstWithHostNet,NoneavecdnsConfig.- Les timeouts de 5 secondes viennent de la course conntrack entre les requêtes A et AAAA en UDP ; NodeLocal DNSCache est le remède de fond.
- Dimensionnez et surveillez CoreDNS (
coredns_dns_requests_total,coredns_dns_responses_total,coredns_dns_request_duration_seconds) ; autorisez le DNS dans les NetworkPolicies de sortie.
Pour aller plus loin
- La page DNS for Services and Pods de la documentation de Kubernetes, qui détaille les enregistrements,
dnsPolicyetdnsConfig. - La documentation du plugin
kubernetesde CoreDNS, pour les options (ttl,pods,fallthrough, zones et noms de domaine). - La page Using NodeLocal DNSCache de la documentation de Kubernetes.
- La leçon suivante, Gateway API en production, qui déplace le regard du dedans vers l'extérieur du cluster : le routage des requêtes entrantes.