Skip to content

Comment les "alias" DNS Kubernetes nous ont brain toute une journée ?

Context

Aujourd'hui au travail, nous avons migré l'un de nos environnements vers une architecture de cluster Kubernetes multi-namespace. Historiquement, l'ensemble de nos applications étaient déployées dans un seul et gros namespace. L'ensemble de nos communications "est/ouest" (service to service) étaient gérées via des alias Kubernetes.

Les problèmes

Une fois cette migration réalisée, nous avons remarqué un comportement étrange sur l'un de nos appels. Lorsqu'une application spécifique exécutait la requête https://my-app/hello, celle-ci retournait des status code différents. Malheureusement, cette application était celle qui permettait de vérifier l'authentification, ce qui conduisait à une déconnexion de l'utilisateur si le statut de la requête n'était pas 200.

Nous avons passé plusieurs heures à enquêter en suivant les étapes suivantes :

  1. Nous avons créé un pod dans le namespace où l'application était localisée (kubectl run -i --tty --rm --image debian test-shell bash) et installé curl (apt-get update && apt-get install -y curl).
  2. Nous avons exécuté la requête suivante : while true; do curl -i https://my-app/hello && sleep 3s; done. Cette commande exécute plusieurs fois une requête sur https://my-app/hello et retourne le status code. La commande retournait le résultat suivant :
$ while true; do curl -i https://my-app/hello && sleep 3s; done
401 Unauthorized
404 Not Found
401 Unauthorized
404 Not Found

En examinant les logs globaux des services, nous nous sommes aperçus que lorsque nous exécutions la requête https://my-app/hello, en réalité la requête était "loadbalancée" entre 2 services de type ExternalName qui n'étaient pas le service my-app... Étrange, me direz-vous, mais nous allons rapidement comprendre la conclusion.

Avant de vous expliquer le problème, il faut comprendre comment Kubernetes résout le DNS interne.

Fonctionnement du DNS interne

Dans un cluster Kubernetes, les noms des services et des pods sont résolus en utilisant un système de DNS interne. Cela permet aux différents composants du cluster de communiquer entre eux sans avoir à connaître l'adresse IP de chaque pod ou service.

Par exemple, si vous avez un service appelé backend qui est exposé sur le port 8080 et qui est géré par un ensemble de pods, vous pouvez accéder à ce service depuis un autre pod en utilisant l'adresse DNS backend.default.svc.cluster.local.

Le DNS interne de Kubernetes utilise un format de nommage standardisé qui se compose de plusieurs parties :

  • Le nom du service ou du pod
  • Le nom du namespace Kubernetes dans lequel le service ou le pod se trouve (par défaut, cet espace de noms est "default")
  • Le suffixe "svc.cluster.local" qui indique que c'est un service Kubernetes

Lorsqu'un pod souhaite accéder à un service, il envoie une requête DNS pour résoudre le nom du service en une adresse IP. Le système de DNS interne de Kubernetes intercepte cette requête et renvoie l'adresse IP des pods qui gèrent ce service.

Note

Pour en apprendre plus sur la notion de service/networking sur Kubernetes, c'est ici

Les "alias" DNS sur Kubernetes

Nous avons eu plusieurs changements au niveau du namespace dans lequel nos applications étaient déployées, mais aussi sur leur nomenclature. Avec la résolution DNS complète, nous aurions dû changer la configuration des applications à chaque changement.

Pour éviter cela, nous avons décidé de mettre en place des alias Kubernetes. Ce que j'appelle un alias Kubernetes est un service Kubernetes de type ExternalName :

apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: my-company
spec:
  externalName: my-super-app.my-company.svc.cluster.local
  sessionAffinity: None
  type: ExternalName

L'intérêt de ce type de service est de pouvoir résoudre son domaine sans avoir à indiquer $APP_NAME.$APP_NAMESPACE.svc.cluster.local, mais plutôt $SERVICE_NAME. Cette organisation nous permettait de modifier les noms des applications sans avoir à modifier les configurations des applications qui les utilisaient.

Note

Ce type de service peut également être utilisé pour faire référence à des ressources externes en utilisant un nom de DNS. Par exemple, si vous avez un service externe appelé mydatabase.mycompany.com qui est hébergé à l'extérieur de votre cluster Kubernetes, vous pouvez créer un service ExternalName dans Kubernetes qui fait référence à ce service externe. Vous pouvez ensuite accéder à ce service en utilisant le nom de service Kubernetes au lieu de l'adresse IP ou du nom DNS de la ressource externe.

Avec l'ajout des alias, le flux réseau ressemblerait à celui-ci.

La cause

Une fois que nous avons compris comment fonctionne la résolution de noms sur Kubernetes, nous avions une piste.

Cette piste était que lorsque nous appelons la route https://my-app/hello, Kubernetes regardait si un service avec le nom my-app était présent dans le namespace new-namespace-1. Vu qu'il n'y en avait pas de déployé, il allait essayer de voir si un namespace avec le nom my-app existait... Vous voyez où je veux en venir ? Non, toujours pas ?!

Avec un schéma ce sera plus simple :

En gros, pendant la migration, nous avions laissé dans le namespace legacy ( my-app ) des services de type ExternalName qui nous permettaient de réaliser une migration avec un minimum de temps d'arrêt. Les ExternalName other-app-1 & other-app-2 restaient encore accessibles le temps que nous migrons les autres applications vers leur nouveau namespace.

Sauf que... Vu qu'aucun service my-app n'était déployé dans le namespace new-namespace-1, Kubernetes allait voir si un namespace my-app était "résolvable" et là, bingo...

Nous avons supprimé le namespace historique my-app et relancé notre commande :

$ while true; do curl -i https://my-app/hello && sleep 3s; done
could not resolve host my-app
could not resolve host my-app
could not resolve host my-app

Forcément, puisque aucun service du namespace new-namespace-1 et aucun namespace my-app n'étaient présents, Kubernetes ne pouvait plus résoudre le host my-app.

Résolution

Pour résoudre le problème, il a suffi de déployer un service Kubernetes de type ExternalName avec le nom my-app dans le namespace new-namespace-1.

Ce service avait la configuration suivante :

apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: new-namespace-1
spec:
  externalName: my-app.new-namespace-2.svc.cluster.local
  sessionAffinity: None
  type: ExternalName

Avec l'ajout de ce service, le flux réseau ressemblait à celui-ci :

Conclusion

Après plusieurs années de maintenance de Kubernetes en production, je n'avais jamais été confronté à ce problème. Ce bug m'a permis de me remettre en question et de me dire que rien n'est jamais acquis.

Nous avons appris qu'avec notre configuration actuelle, nous étions capables de résoudre tous les services de type ExternalName à partir du nom de leur namespace.

J'espère que cet article vous aura au moins appris quelque chose. Je suis conscient que je ne suis pas rentré dans les détails sur certains points, donc si vous avez des questions, n'hésitez pas à me contacter sur Twitter @sreguys.