Aller au contenu

Services et DNS

200 Pratiquer ⏱ 1 h 15 kuberneteskubectlkindcoredns

À la fin, vous saurez

  • Expliquer pourquoi un client ne doit jamais viser l'adresse d'un pod, et ce qu'apporte un Service
  • Écrire un Service ClusterIP pour Signalements avec un port nommé
  • Relier un Service, ses EndpointSlices et la sonde de disponibilité des pods
  • Décrire ce que kube-proxy installe dans chaque nœud et le rapprocher de la traduction d'adresses
  • Résoudre un service par son nom DNS, et expliquer les domaines de recherche et ndots
  • Choisir entre ClusterIP, NodePort, LoadBalancer, headless et ExternalName

Prérequis

Testé avec kubectl 1.36.0 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

Le Deployment de la leçon précédente fait tourner trois pods de Signalements. Chacun a une adresse IP, attribuée par le plugin réseau du cluster, que vous pouvez lire avec kubectl get pods -o wide. Tentant : il suffirait de donner ces adresses aux clients.

Ce serait une erreur, pour trois raisons. Les adresses changent : chaque pod remplacé (une mise à jour, une panne, une éviction) en reçoit une nouvelle. Elles sont plusieurs : un client devrait savoir répartir ses requêtes entre trois, puis cinq, puis deux pods. Et toutes ne sont pas bonnes à un instant donné : un pod qui démarre, ou qui a perdu sa base, ne doit pas recevoir de trafic.

Le Service répond à ces trois problèmes. C'est une adresse et un nom stables, devant un ensemble de pods choisis par sélecteur ; le cluster tient à jour la liste des pods prêts derrière, et répartit les connexions. Le client ne connaît que signalements, ou http://signalements.signalements.svc.cluster.local, et cela reste vrai pendant toute la vie de l'application.

Cette leçon décrit comment cela fonctionne réellement : une adresse qui n'appartient à aucune machine, des règles de traduction d'adresses installées dans chaque nœud, un serveur DNS interne. Comprendre ce mécanisme, c'est savoir quoi regarder le jour où « le service ne répond pas ».

Les concepts

Le Service ClusterIP

Le type de Service par défaut, ClusterIP, donne une adresse virtuelle, joignable uniquement depuis l'intérieur du cluster. Le Service de Signalements :

apiVersion: v1
kind: Service
metadata:
  name: signalements
  namespace: signalements
  labels:
    app.kubernetes.io/name: signalements
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: signalements
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
  • selector désigne les pods, exactement comme le sélecteur du Deployment (leçon 5). Le Service ne connaît pas le Deployment : il ne voit que des pods qui portent l'étiquette.
  • port: 80 est le port du Service : les clients appellent signalements:80.
  • targetPort: http est le port des pods, désigné ici par le nom donné au port du conteneur à la leçon 4. Si une version future de l'application écoute sur 8080 et nomme ce port http, le Service n'a pas à changer. La documentation le présente comme la principale raison de nommer ses ports.
  • Le nom du port (http) est obligatoire dès qu'un Service expose plusieurs ports ; il sert aussi aux enregistrements DNS de type SRV.

À la création, le plan de contrôle attribue au Service une adresse (clusterIP) dans une plage réservée aux services, fixée à la création du cluster et distincte de celle des pods : elle ne change plus tant que le Service existe.

kubectl sait générer un squelette en mode client, sans contacter le cluster. Sortie réelle avec kubectl 1.36.0 :

$ kubectl create service clusterip signalements --tcp=80:8000 --dry-run=client -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: signalements
  name: signalements
spec:
  ports:
  - name: 80-8000
    port: 80
    protocol: TCP
    targetPort: 8000
  selector:
    app: signalements
  type: ClusterIP
status:
  loadBalancer: {}

Remarquez le sélecteur app: signalements déduit du nom : il ne correspond pas aux étiquettes app.kubernetes.io/name du Deployment du cours. Un Service généré ainsi et appliqué tel quel ne trouverait aucun pod. Le générateur est un point de départ ; le sélecteur se vérifie toujours.

Les EndpointSlices

Le Service ne contient pas la liste de ses pods. Un contrôleur du plan de contrôle la calcule en permanence et l'écrit dans des objets séparés, les EndpointSlices (tranches de points d'accès). Chaque EndpointSlice, rattachée au Service par l'étiquette kubernetes.io/service-name, liste des adresses de pods avec leur port et trois conditions :

  • ready : le pod est prêt, il doit recevoir du trafic ;
  • serving : le pod répond, même s'il est en cours d'arrêt ;
  • terminating : le pod est en cours d'arrêt.

Par défaut, une tranche contient au plus 100 points d'accès ; au-delà, le contrôleur en crée d'autres. C'est la raison d'être des tranches : l'ancien objet Endpoints, qui mettait tous les points d'accès d'un Service dans un seul objet, devenait énorme et coûteux à diffuser dans les gros clusters. Il est déprécié depuis Kubernetes 1.33 ; les EndpointSlices sont stables depuis la 1.21.

Le lien avec la leçon 4 est direct : un pod dont la sonde de disponibilité échoue reste listé, mais avec ready: false, et ne reçoit plus de nouvelles connexions. Un pod en cours d'arrêt passe à terminating: true et ready: false. C'est par ce chemin que les sondes et l'arrêt gracieux agissent sur le trafic.

Ce que fait kube-proxy

L'adresse d'un Service n'est portée par aucune interface réseau : vous ne la trouverez dans ip addr d'aucun nœud. Alors qui répond ? Personne : elle est traduite avant d'atteindre quoi que ce soit.

Sur chaque nœud tourne kube-proxy (leçon 2). Il observe les Services et les EndpointSlices, et programme en conséquence le noyau Linux du nœud. Dans son mode par défaut, iptables, la documentation décrit une série de règles par Service : une règle qui reconnaît l'adresse et le port du Service, puis une règle par point d'accès prêt, et un choix aléatoire du point d'accès pour chaque nouvelle connexion. La règle finale fait une traduction d'adresse de destination (DNAT) : l'adresse du Service est remplacée par celle du pod choisi.

C'est exactement le mécanisme de la leçon La traduction d'adresses (NAT) du cours TCP/IP, avec le même suivi de connexion : la décision est prise au premier paquet d'une connexion TCP, inscrite dans la table de suivi (conntrack), et tous les paquets suivants de la connexion vont au même pod. Deux conséquences :

  • La répartition se fait par connexion, pas par requête. Un client qui garde une connexion HTTP ouverte (keep-alive) envoie toutes ses requêtes au même pod. Un client unique et bavard peut donc charger un seul pod.
  • Le paquet n'est pas relayé par un processus : il est réécrit dans le noyau, puis routé vers le pod, éventuellement sur un autre nœud. kube-proxy n'est sur le chemin d'aucun paquet ; il ne fait que programmer des règles.

kube-proxy a trois modes sous Linux. iptables est le défaut en 1.36. nftables, stable depuis Kubernetes 1.33, utilise l'interface plus récente du noyau ; la documentation annonce qu'une version future en fera le défaut, et conseille de fixer le mode explicitement pour ne pas en changer par surprise lors d'une mise à jour. ipvs est déprécié depuis la 1.35 : la documentation annonce sa désactivation par défaut à partir de la 1.40 et son retrait en 1.43. Certains plugins réseau (Cilium, par exemple) remplacent complètement kube-proxy par leur propre mise en œuvre ; le principe reste le même.

Le DNS des services

Personne ne veut écrire l'adresse virtuelle d'un Service dans une configuration. Chaque cluster a un serveur DNS interne, presque toujours CoreDNS, qui publie un nom pour chaque Service. La forme complète est :

<service>.<namespace>.svc.<domaine-du-cluster>
signalements.signalements.svc.cluster.local

cluster.local est le domaine par défaut. Ce nom donne un enregistrement A (et AAAA en IPv6) qui pointe vers l'adresse virtuelle du Service. Pour chaque port nommé, CoreDNS publie aussi un enregistrement SRV (_http._tcp.signalements.signalements.svc.cluster.local), qui donne le numéro de port.

Le kubelet configure le fichier /etc/resolv.conf de chaque pod pour viser ce DNS. La documentation en donne la forme, pour un pod du namespace test :

nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Les domaines de recherche (search) permettent les noms courts : depuis un pod du namespace signalements, signalements suffit, puisque le résolveur essaie signalements.signalements.svc.cluster.local. Depuis un autre namespace, il faut au moins signalements.signalements. Le nom complet fonctionne partout.

ndots:5 a un effet qui surprend. Le résolveur ne considère un nom comme complet que s'il contient au moins cinq points ; sinon, il essaie d'abord tous les domaines de recherche. Un pod qui appelle api.example.com (deux points) envoie donc d'abord des requêtes pour api.example.com.signalements.svc.cluster.local, puis api.example.com.svc.cluster.local, puis api.example.com.cluster.local, qui échouent toutes, avant la bonne. Chaque recherche vaut deux requêtes (A et AAAA) : jusqu'à huit requêtes pour un nom externe, et un DNS du cluster chargé pour rien. Deux remèdes : écrire les noms externes avec un point final (api.example.com.), qui les rend complets, ou abaisser ndots pour les pods qui appellent beaucoup de noms externes, par le champ dnsConfig du Pod.

Exposer hors du cluster : NodePort et LoadBalancer

Une adresse ClusterIP n'est pas joignable de l'extérieur. Deux types de Service étendent l'exposition, chacun en ajoutant une couche au précédent.

NodePort ouvre un port sur tous les nœuds du cluster, pris dans une plage réservée (30000 à 32767 par défaut). Une connexion vers adresse-de-n'importe-quel-nœud:port est traduite vers un pod du Service, même si ce nœud n'en héberge aucun. Le Service garde aussi son adresse ClusterIP. Utile en laboratoire, rarement en production : les ports sont exotiques, et il faut répartir soi-même entre les nœuds.

LoadBalancer demande, en plus, un répartiteur de charge externe au fournisseur. Ce n'est pas Kubernetes qui le crée, mais le cloud-controller-manager du fournisseur, un composant du plan de contrôle qui observe les Services de ce type et pilote l'API du fournisseur. Chez Scaleway, sur un cluster Kapsule, le Scaleway Cloud Controller Manager crée un Load Balancer Scaleway, dont les backends sont les nœuds du cluster, puis inscrit son adresse publique dans status.loadBalancer du Service.

Ce répartiteur se règle par des annotations sur le Service, toutes préfixées par service.beta.kubernetes.io/scw-loadbalancer-. La documentation du contrôleur en liste plusieurs dizaines ; quelques-unes, avec leur comportement documenté :

Annotation (après le préfixe)EffetDéfaut documenté
typeL'offre de Load Balancercelle de la configuration du contrôleur
zoneLa zone où créer le répartiteurla première zone de la région du cluster
health-check-typeLe type de vérification de santé (tcp, http...)tcp
health-check-http-uriLe chemin testé quand le type est http
proxy-protocol-v2Le protocole PROXY v2 vers les nœuds, pour transmettre l'adresse du client
privateUn répartiteur sans adresse publiquepublic
idL'identifiant du répartiteur, sous la forme <zone>/<id>rempli par le contrôleur

Le cours Scaleway en pratique détaille ces réglages côté répartiteur. Un point à retenir : chaque Service LoadBalancer crée son répartiteur, facturé. Pour exposer dix applications HTTP, on ne crée pas dix répartiteurs, mais un seul, devant un contrôleur d'entrée (Ingress) ou une passerelle (Gateway API) qui route par nom d'hôte : c'est l'objet de la leçon 11. Chez Lyneko, sur le cluster lyneko-apps, c'est le rôle de Traefik : un seul Service LoadBalancer, devant toutes les applications.

Les Services particuliers

Headless. Un Service avec clusterIP: None n'a pas d'adresse virtuelle, et kube-proxy l'ignore. Son nom DNS renvoie directement les adresses des pods prêts. On l'utilise quand le client veut choisir lui-même son pod : une base de données répliquée dont il faut joindre le primaire, ou les membres d'un StatefulSet, qui reçoivent chacun un nom DNS stable (leçon 10).

ExternalName. Un Service de type ExternalName n'a ni sélecteur ni adresse : son nom DNS est un alias (CNAME) vers un nom externe. Signalements pourrait appeler postgresql dans son namespace, alias de l'adresse de la base managée chez Scaleway, et le jour où la base change de nom, seul le Service change. La documentation prévient que l'alias pose problème pour HTTP et HTTPS : le client envoie le nom du Service dans l'en-tête Host et l'indication SNI de TLS, que le serveur externe ne connaît pas. Réservez-le aux protocoles qui ne transportent pas le nom, comme PostgreSQL, en sachant qu'une vérification TLS du nom du serveur (sslmode=verify-full) échouerait de la même façon : le certificat porte le nom externe, pas celui du Service.

Notez au passage une bizarrerie du générateur de kubectl, sortie réelle en mode client :

$ kubectl create service externalname base-ext --external-name=sig-db.example.com --dry-run=client -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: base-ext
  name: base-ext
spec:
  externalName: sig-db.example.com
  selector:
    app: base-ext
  type: ExternalName
status:
  loadBalancer: {}

Le générateur ajoute un selector, qui n'a aucun sens pour un ExternalName. Retirez-le du fichier versionné.

Affinité et répartition topologique

Par défaut, chaque nouvelle connexion part vers un pod choisi au hasard. Deux réglages modifient ce choix.

sessionAffinity: ClientIP envoie toutes les connexions d'une même adresse IP source vers le même pod, pendant sessionAffinityConfig.clientIP.timeoutSeconds (10800 secondes, soit trois heures, par défaut). C'est une béquille pour les applications qui gardent des sessions en mémoire ; derrière une traduction d'adresses ou un répartiteur, beaucoup de clients partagent la même adresse source, et l'affinité concentre la charge. Signalements est sans état : il n'en a pas besoin.

trafficDistribution exprime une préférence de proximité : PreferSameZone (privilégier les pods de la même zone que le client) ou PreferSameNode (du même nœud). Le champ est stable depuis la 1.33, la valeur PreferSameNode depuis la 1.35 ; l'ancienne valeur PreferClose, alias de PreferSameZone, est dépréciée. Sur un cluster réparti sur plusieurs zones, PreferSameZone réduit la latence et le trafic inter-zones, au prix d'une répartition moins égale.

En pratique

Sur le cluster kind formation, namespace signalements, avec le Deployment de la leçon 5. La leçon décrit ce que vous devez observer, sans reproduire de sortie, sauf les citations de documentation signalées.

Créer le Service et regarder ses points d'accès

Enregistrez le Service dans service.yaml, puis :

$ kubectl apply -f service.yaml
$ kubectl get service signalements
$ kubectl get endpointslices -l kubernetes.io/service-name=signalements -o wide

get service affiche le type, l'adresse CLUSTER-IP attribuée, et 80/TCP. get endpointslices liste une tranche, dont la colonne ENDPOINTS contient les trois adresses de pods. Comparez-les à celles de kubectl get pods -o wide : ce sont les mêmes.

Pour les conditions :

$ kubectl get endpointslices -l kubernetes.io/service-name=signalements \
    -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

Une ligne par pod, avec ready=true.

Joindre le Service depuis un pod

Lancez un pod de diagnostic éphémère, avec une image qui contient des outils réseau :

$ kubectl run diag --rm -it --restart=Never --image=busybox:1.37 -- sh
  • --rm supprime le pod à la sortie ;
  • -it attache un terminal interactif ;
  • --restart=Never crée un Pod simple, sans redémarrage.

Dans le pod :

/ # cat /etc/resolv.conf
/ # nslookup signalements
/ # wget -qO- http://signalements/sante
/ # wget -qO- http://signalements.signalements.svc.cluster.local/

resolv.conf contient l'adresse du DNS du cluster, les trois domaines de recherche de la forme vue plus haut, et options ndots:5. nslookup résout le nom court en l'adresse ClusterIP du Service, pas en l'adresse d'un pod. Les deux wget obtiennent la réponse de Signalements, sans que le pod de diagnostic connaisse aucune adresse de pod. Quittez avec exit.

Pour un diagnostic réseau plus riche (dig, curl, tcpdump), l'image nicolaka/netshoot regroupe la plupart des outils du cours TCP/IP.

Voir un pod sortir des points d'accès

Les conditions des points d'accès se voient le mieux pendant l'arrêt d'un pod. Dans un terminal :

$ kubectl get endpointslices -l kubernetes.io/service-name=signalements --watch -o wide

Dans un autre, supprimez un pod (kubectl delete pod <nom> --wait=false). Dans la tranche, l'adresse du pod supprimé reste quelques secondes avec les conditions terminating et serving (le preStop de 5 secondes, puis l'arrêt gracieux), avant de disparaître, pendant que l'adresse du remplaçant apparaît, d'abord non prête, puis prête quand sa sonde réussit. Le tutoriel Pods And Endpoints Termination Flow de la documentation montre la même séquence en JSON, avec "serving": true et "terminating": true pendant l'arrêt.

Joindre le Service depuis le poste

$ kubectl port-forward service/signalements 8080:80
$ curl -s http://localhost:8080/sante

port-forward vers un Service choisit un pod derrière lui et s'y connecte pour toute la durée de la commande : il ne répartit pas la charge. C'est un outil de diagnostic, pas une exposition.

Exposer par NodePort sur kind

Changez le type du Service en NodePort et réappliquez, ou générez un squelette pour comparer (sortie réelle en mode client) :

$ kubectl create service nodeport signalements --tcp=80:8000 --dry-run=client -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: signalements
  name: signalements
spec:
  ports:
  - name: 80-8000
    port: 80
    protocol: TCP
    targetPort: 8000
  selector:
    app: signalements
  type: NodePort
status:
  loadBalancer: {}

Le port côté nœud n'apparaît pas : il est attribué par le plan de contrôle à la création (nodePort), sauf si vous le fixez. Après application, kubectl get service signalements affiche 80:3xxxx/TCP. Sur kind, les « nœuds » sont des conteneurs Docker : leur port n'est joignable depuis le poste que si la configuration du cluster l'a publié (extraPortMappings dans la configuration de kind), ou depuis un autre conteneur du réseau kind. C'est une des raisons pour lesquelles la leçon 11 expose Signalements autrement.

Sur Kapsule : un Service LoadBalancer

Sur un cluster Kapsule, un Service de type LoadBalancer avec une vérification de santé HTTP :

apiVersion: v1
kind: Service
metadata:
  name: signalements-public
  namespace: signalements
  annotations:
    service.beta.kubernetes.io/scw-loadbalancer-health-check-type: "http"
    service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri: "/sante"
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: signalements
  ports:
    - name: http
      port: 80
      targetPort: http

Après application, kubectl get service signalements-public --watch montre EXTERNAL-IP à <pending> pendant la création du répartiteur, puis son adresse publique. La console Scaleway affiche le Load Balancer correspondant, et l'annotation scw-loadbalancer-id apparaît sur le Service. Supprimez ce Service après l'essai : le répartiteur est supprimé avec lui, et cesse d'être facturé.

Warning

Avec ces réglages, la vérification de santé du répartiteur vise les nœuds, sur le port NodePort du Service, et le trafic qui arrive sur un nœud peut repartir vers un pod d'un autre nœud. Le répartiteur voit des nœuds sains, pas des pods. Le réglage externalTrafficPolicy: Local change ce comportement (et conserve l'adresse source des clients) ; le cours Kubernetes : réseau et exposition le détaille.

Sous le capot

L'adresse virtuelle n'existe que dans des règles. Dans le mode iptables, la documentation décrit le chemin : une règle de la table nat reconnaît l'adresse et le port du Service et saute dans une chaîne propre au Service, qui choisit au hasard une chaîne par point d'accès, laquelle fait le DNAT. En mode nftables, la même logique s'exprime par des tables de correspondance, plus efficaces quand les Services se comptent en milliers. Dans les deux cas, conntrack retient la traduction pour la suite de la connexion, comme dans la leçon NAT du cours TCP/IP.

Pourquoi un ping vers un Service échoue. Les règles ne traduisent que les protocoles et ports déclarés (TCP 80 pour Signalements). Un ping (ICMP) vers l'adresse d'un Service ne correspond à aucune règle et n'obtient pas de réponse, alors que le Service fonctionne parfaitement. Testez un Service par son port, jamais par ping.

La propagation est asynchrone. Quand un pod devient non prêt, la chaîne est : le kubelet met à jour le statut du Pod dans l'API ; le contrôleur d'EndpointSlices met à jour la tranche ; chaque kube-proxy reçoit la modification et réécrit ses règles. Chaque étape prend un peu de temps, et chaque nœud va à son rythme. C'est la raison du preStop de la leçon 4 et de la leçon 5.

CoreDNS lit l'API. CoreDNS est lui-même un Deployment, dans le namespace kube-system, exposé par un Service (souvent nommé kube-dns pour des raisons historiques), dont l'adresse est celle que le kubelet écrit dans resolv.conf. Son plugin kubernetes observe les Services et les EndpointSlices et répond à partir de ce qu'il en sait. Une panne de CoreDNS rend tous les noms injoignables, alors que les adresses fonctionnent toujours : un symptôme à reconnaître.

Pièges courants

Un sélecteur qui ne trouve aucun pod. Le Service existe, son adresse aussi, mais toute connexion échoue ou expire. kubectl get endpointslices -l kubernetes.io/service-name=<nom> montre une tranche vide : comparez le sélecteur du Service aux étiquettes réelles des pods (kubectl get pods --show-labels). C'est le piège du générateur kubectl create service, qui met app: <nom>.

targetPort faux. Le Service vise le port 80 des pods, alors que gunicorn écoute sur 8000 : connexion refusée. Nommer les ports évite ce piège.

L'application écoute sur 127.0.0.1. Dans le pod, gunicorn répond à localhost, mais les connexions venues du Service arrivent sur l'adresse du pod et sont refusées. L'écoute doit se faire sur 0.0.0.0 (leçon Diagnostiquer un problème réseau du cours TCP/IP, qui montre le même piège avec un répartiteur).

Tester par ping. L'adresse d'un Service ne répond pas à ICMP ; seul le port déclaré est traduit.

Une sonde de disponibilité qui échoue partout. Tous les pods sont ready: false, la tranche ne contient aucun point d'accès prêt, et le Service ne répond plus, alors que tous les pods tournent. Regardez les événements des pods (Readiness probe failed).

Des noms courts d'un namespace à l'autre. signalements ne se résout que dans le namespace signalements ; ailleurs, il faut signalements.signalements ou le nom complet.

Une latence DNS sur les appels externes. Les recherches imposées par ndots:5 multiplient les requêtes. Un point final sur les noms externes, ou un dnsConfig adapté.

Un Service LoadBalancer par application. Chaque Service crée un répartiteur facturé ; on en met un seul, devant un contrôleur d'entrée.

Sécurité

  • Un Service n'est pas une barrière. Par défaut, tout pod du cluster peut joindre tout Service et tout pod, quel que soit le namespace. Le cloisonnement passe par les NetworkPolicies, appliquées par le plugin réseau (cours Kubernetes : réseau et exposition et Sécurité de Kubernetes).
  • LoadBalancer expose sur Internet. Un Service de ce type a, par défaut chez Scaleway, une adresse publique. Pour un service interne, l'annotation scw-loadbalancer-private ou, mieux, un Service ClusterIP derrière le contrôleur d'entrée. Vérifiez régulièrement la liste des Services de type LoadBalancer du cluster (kubectl get services -A -o wide, colonne TYPE).
  • NodePort ouvre un port sur tous les nœuds. Si les nœuds ont des adresses publiques, le port est joignable depuis Internet, sauf filtrage par le groupe de sécurité des nœuds.
  • externalIPs est déprécié depuis Kubernetes 1.36 : il permettait à quiconque pouvait créer un Service de capter le trafic destiné à une adresse arbitraire. La documentation demande de migrer vers un répartiteur ou une passerelle.
  • Le DNS du cluster révèle la topologie. Tout pod peut énumérer les noms des Services. Ce n'est pas un secret, mais ce n'est pas non plus une raison d'y mettre des informations sensibles.

En production

  • Un Service ClusterIP par application, et l'exposition externe concentrée dans un contrôleur d'entrée ou une Gateway (leçon 11), derrière un Service LoadBalancer.
  • Le mode de kube-proxy fixé dans la configuration, comme le recommande la documentation, pour ne pas changer de mode lors d'une mise à jour qui changerait le défaut. Sur Kapsule, c'est le fournisseur qui le gère, avec le plugin réseau choisi pour le cluster.
  • CoreDNS dimensionné et surveillé. Ses requêtes en échec (NXDOMAIN en rafale, signature de ndots) et sa latence sont des métriques à suivre ; un cache DNS local par nœud (NodeLocal DNSCache) soulage les gros clusters.
  • trafficDistribution: PreferSameZone sur les services appelés par d'autres services, dans un cluster à plusieurs zones : moins de latence, moins de trafic facturé entre zones.
  • Des connexions longues qui se rééquilibrent. Avec une répartition par connexion, un client qui garde ses connexions pendant des heures ne profite pas des nouveaux pods ; les clients HTTP de longue durée (pools de connexions) gagnent à renouveler leurs connexions périodiquement.

Exercices

1. Choisir le type (niveau 100). Pour chaque besoin, choisissez le type de Service : (a) Signalements appelé par un autre service du cluster ; (b) un tableau de bord interne à exposer sur Internet, avec une seule application dans le cluster ; (c) une base PostgreSQL managée hors du cluster, que l'on veut appeler par un nom interne ; (d) les trois membres d'un cluster PostgreSQL dans Kubernetes, dont le client doit joindre le primaire.

Solution

(a) ClusterIP. (b) LoadBalancer, acceptable quand il n'y a qu'une application ; dès qu'il y en a plusieurs, un contrôleur d'entrée derrière un seul LoadBalancer. Et pour un tableau de bord interne, la question préalable est : doit-il vraiment être sur Internet ? (c) ExternalName, qui fait un alias DNS ; PostgreSQL ne transporte pas le nom d'hôte comme HTTP, donc pas de problème d'en-tête Host, mais une vérification TLS stricte du nom du serveur demandera de viser le nom externe. (d) Un Service headless, qui donne les adresses des pods, éventuellement complété par un Service ClusterIP dont le sélecteur ne vise que le primaire (c'est ce que font les opérateurs PostgreSQL).

2. Compter les requêtes DNS (niveau 200). Un pod du namespace signalements résout smtp.example.com avec la configuration par défaut (ndots:5). Combien de noms le résolveur essaie-t-il, dans quel ordre, et combien de requêtes cela fait-il au plus si le résolveur demande à chaque fois un enregistrement A et un AAAA ? Comment l'éviter ?

Solution

Le nom a deux points, moins que 5 : le résolveur essaie les domaines de recherche d'abord. smtp.example.com.signalements.svc.cluster.local, smtp.example.com.svc.cluster.local, smtp.example.com.cluster.local, puis enfin smtp.example.com : quatre noms, soit jusqu'à huit requêtes, dont six inutiles. On l'évite en écrivant smtp.example.com. avec un point final dans la configuration, ou en réglant dnsConfig.options du Pod avec ndots à une valeur plus basse (par exemple 2).

3. Le Service muet (niveau 200). Un Service signalements existe, kubectl get service affiche une adresse, mais wget http://signalements/sante depuis un pod de diagnostic échoue. Décrivez les vérifications, dans l'ordre, et ce que chacune permet d'exclure.

Solution
  1. nslookup signalements depuis le pod : si le nom ne se résout pas, le problème est le DNS (CoreDNS en panne, mauvais namespace pour un nom court). 2. kubectl get endpointslices -l kubernetes.io/service-name=signalements : une tranche vide signale un sélecteur qui ne correspond à aucun pod (comparer avec kubectl get pods --show-labels) ; des points d'accès tous ready: false signalent des sondes de disponibilité en échec. 3. Si des points d'accès sont prêts, joindre directement l'adresse d'un pod sur son port (wget http://<ip-du-pod>:8000/sante) : un refus révèle un targetPort faux ou une application qui écoute sur 127.0.0.1. 4. Si l'adresse du pod répond mais pas le Service, regarder kube-proxy (ses journaux dans kube-system) ou une NetworkPolicy qui filtrerait.

4. Le répartiteur qui ne voit que des nœuds (niveau 200). Sur Kapsule, un Service LoadBalancer a une vérification de santé HTTP sur /sante. Un développeur s'étonne : un pod de Signalements a perdu sa base, sa sonde de disponibilité échoue, mais le répartiteur Scaleway affiche tous ses backends sains. Expliquez.

Solution

Les backends du répartiteur sont les nœuds, pas les pods. La vérification de santé du répartiteur interroge un nœud sur le NodePort du Service, et kube-proxy envoie cette requête à un pod prêt du Service, éventuellement sur un autre nœud. Le pod en échec est déjà retiré des EndpointSlices (sa sonde de disponibilité échoue), donc kube-proxy ne lui envoie plus rien : le répartiteur n'a aucune raison de voir un problème. La sonde de disponibilité fait déjà le travail ; la vérification de santé du répartiteur, elle, détecte un nœud qui ne répond plus du tout.

Récapitulatif

  • Un client ne vise jamais l'adresse d'un pod : elle change. Un Service donne un nom et une adresse stables devant les pods choisis par sélecteur.
  • port est le port du Service, targetPort celui des pods, de préférence nommé.
  • Les EndpointSlices listent les pods du Service avec leurs conditions ready, serving, terminating ; la sonde de disponibilité et l'arrêt agissent par elles. L'objet Endpoints est déprécié depuis 1.33.
  • kube-proxy programme dans chaque nœud une traduction d'adresse de destination (iptables par défaut, nftables stable depuis 1.33, ipvs déprécié) : la répartition se fait par connexion, et l'adresse virtuelle ne répond pas au ping.
  • CoreDNS publie <service>.<namespace>.svc.cluster.local ; les domaines de recherche permettent les noms courts, et ndots:5 multiplie les requêtes pour les noms externes.
  • NodePort ouvre un port (30000 à 32767) sur tous les nœuds ; LoadBalancer demande un répartiteur au fournisseur, chez Kapsule un Load Balancer Scaleway réglé par des annotations scw-loadbalancer-.
  • Headless (clusterIP: None) renvoie les adresses des pods ; ExternalName est un alias DNS, à éviter pour HTTP.
  • sessionAffinity colle un client à un pod ; trafficDistribution préfère la même zone ou le même nœud.

Pour aller plus loin

  • La page Virtual IPs and Service Proxies de la documentation, qui détaille les modes de kube-proxy et les règles qu'ils installent.
  • La documentation du plugin kubernetes de CoreDNS, pour les enregistrements publiés et les options.
  • La documentation des annotations du Scaleway Cloud Controller Manager, pour régler le répartiteur créé par un Service LoadBalancer sur Kapsule.
  • La leçon suivante, ConfigMaps et Secrets, qui retire la configuration et le mot de passe du manifeste.
Voir ma constellation →

Sources