December 13, 2021

Kubernetes services, Nodeport, LoadBalancer et externalTrafficPolicy

Les services Kubernetes de type LoadBalancer et Nodeport ont une option peu connue mais très utile appelée externalTrafficPolicy. Voyons dans cet article son fonctionnement.

Créer un service de type NodePort

Vous avez tout d’abord besoin d’un cluster Kubernetes. j’utiliserai ici le cloud Exoscale pour créer mon cluster (vous pouvez retrouver la documentation ci si cela vous intéresse).

Déployons maintenant un pod et un service sur ce cluster:

---
apiVersion: v1
kind: Pod
metadata:
  name: test-python
  labels:
    app: python
spec:
  containers:
  - name: app
    image: python:latest
    command: ["python", "-m", "http.server", "31000"]
    ports:
      - containerPort: 31000
---
apiVersion: v1
kind: Service
metadata:
 name: test-python
spec:
 ports:
 - port: 31000
   protocol: TCP
   targetPort: 31000
   nodePort: 31000
 selector:
   app: python
 type: NodePort

Le pod démarrera un serveur HTTP sur le port 31000. Nous exposons ensuite ce service via un service de type NodePort.

J’ai dans mon cluster 3 noeuds, ayant comme IP 194.182.162.70, 194.182.163.108, 194.182.161.153. Le service de type NodePort est exposé sur chaque noeud à l’adresse 31000 (l’option nodePort indiquant le port sur le noeud, targetPort le port de l’application cible: ces valeurs peuvent être différentes), et redigirera le trafic vers mon application (qui elle n’a aucune instance).

Si je fais par exemple un curl 194.182.162.70:31000, je verrai dans mes logs de mon application Python (kubectl logs test-python -f):

192.168.49.128 - - [12/Dec/2021 16:35:36] "GET / HTTP/1.1" 200 -

192.168.49.128 n’est pas l’IP de ma machine cliente mais l’IP interne du noeud ciblé. On voit donc que Kubernetes a remplacé l’IP source de ma requête Kubernetes par l’IP du noeud, l’IP source étant définitivement perdue.
Cela est bien sûr un problème pour toutes les applications ayant besoin de cette information.

Fonctionnement d’un service nodeport simple sur kubernetes

L’avantage de ce type de service est que vous pouvez envoyer vos requêtes sur n’importe quel noeud du cluster sur le port 31000 et le traffic sera redirigé sur une instance du pod, même si cette instance est sur une autre machine. J’ai par exemple ici 3 noeuds mais qu’une instance de mon application, mais l’application est accessible depuis mes 3 noeuds.

Il est à noter que configurer l’option nodePort est optionnel, par défaut Kubernetes allouera un port aléatoire dans la range 30000-32767 (cette range est configurable dans l’API server)

externalTrafficPolicy

Ajoutez maintenant à la spec de notre service externalTrafficPolicy: Local et réappliquez le.

Essayez encore une fois de faire des requêtes sur les IP de vos noeuds sur le port 31000. Seulement le noeud où votre pod tourne répondra, et ici l’IP affichée dans les logs est bien l’IP de votre client.
l’adresse IP est préservée.

Fonctionnement d’un service nodeport avec externalTrafficPolicy: Local sur kubernetes

Services de type LoadBalancer

Un service Kubernetes de type LoadBalancer permet de provisionner un load balancer ciblant un service donné chez votre Cloud provider.

Voici la définition à utiliser pour par exemple provisionner un load balancer chez Exoscale:

---
apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/exoscale-loadbalancer-name: python
    service.beta.kubernetes.io/exoscale-loadbalancer-service-healthcheck-interval: 10s
    service.beta.kubernetes.io/exoscale-loadbalancer-service-healthcheck-mode: http
    service.beta.kubernetes.io/exoscale-loadbalancer-service-healthcheck-retries: '1'
    service.beta.kubernetes.io/exoscale-loadbalancer-service-healthcheck-timeout: 3s
    service.beta.kubernetes.io/exoscale-loadbalancer-service-healthcheck-uri: /
    service.beta.kubernetes.io/exoscale-loadbalancer-service-strategy: source-hash
  name: python-lb
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  ports:
    - name: http
      port: 80
      nodePort: 31001
      protocol: TCP
      targetPort: 31000
  selector:
    app: python

Comme vous le voyez ici, externalTrafficPolicy: Local est configuré. Sans ce réglage vous perdez comme dit précédemment l’IP source, ce que nous ne voulons pas ici. A noter que le load balancer Exoscale conseerve également l’IP source.

Il y a ici une chose très intéressante qui se passe. J’ai créé un service avec nodePort: 31001, j’ai donc sur chaque noeud le port 31001 exposant mon application (comme dans mon exemple précédent sur 31000).

J’ai demandé la création d’un load balancer Exoscale qui devra exposer sur le port 80 (option port: 80) ce service.

Pourtant, voici ce que je vois sur la définition de mon load balancer sur l’interface d’Exoscale:

La configuration du load balancer chez Exoscale

On voit bien que le load balancer est configuré pour rediriger le port 80 vers 31001 comme attendu. Mais regardez la définition du healthcheck: elle se fait sur le port 31897.

Quel est ce port ? Il n’a jamais été configuré de notre côté. Regardons la définition de notre service LoadBalancer avec kubectl:

kubectl get service python-lb -o yaml | grep 31897
  healthCheckNodePort: 31897

Kubernetes a automatiquement alloué le port 31897 pour la valeur healthCheckNodePort, et c’est ce port qui est utilisé par mon load balancer pour vérifier si mon application est fonctionnelle ou non. Le healthcheck n’est donc pas fait directement sur votre application.

mais que retourne donc ce port ? Essayons deux requêtes curl, la première sur le noeud où tourne mon application (pod), la seconde sur un noeud où elle ne tourne pas:

# Mon pod tourne sur ce noeud
curl -v 194.182.163.108:31897
< HTTP/1.1 200 OK

{
	"service": {
		"namespace": "default",
		"name": "python-lb"
	},
	"localEndpoints": 1
}
# Mon pod ne tourne pas sur ce noeud
curl -v 194.182.162.70:31897
< HTTP/1.1 503 Service Unavailable
{
	"service": {
		"namespace": "default",
		"name": "python-lb"
	},
	"localEndpoints": 0
}

On voit que mes noeuds où mon pod tourne retournent un HTTP 200 OK, avec une valeur 1 pour localEndpoints. Les noeuds où mon application ne tournent pas retournent un HTTP 503 Service Unavailable et 0 pour localEndpoints.

Fonctionnement d’un service LoadBalncer avec externalTrafficPolicy: Local sur kubernetes

C’est donc cette information retournée par Kubernetes qui fera réussir ou échouer le healthcheck de mon load balancer. localEndpoints sera automatiquement mis à jour en fonction de la santé de votre application.

Conclusion

Comprendre comment fonctionnent ces services et plus spécifiquement les healthchecks sur un service de type LoadBalancer est très important.

Il m’est par exemple déjà arrivé de configurer un healthcheck de type tcp sur un service de type LoadBalancer ayant externalTrafficPolicy: Local: grave erreur ! En effet, le port sera toujours ouvert et c’est au niveau HTTP que l’information sur l’état du service est retournée.

Tags: devops cloud

Add a comment








If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).

Top of page