Comment conserver l'IP source des requêtes après équilibrage de charge dans un cluster K8s

Introduction

Le déploiement d’applications n’est pas toujours aussi simple qu’une simple installation et exécution, il faut parfois considérer les problèmes de réseau. Cet article explique comment faire en sorte que les services dans un cluster k8s puissent obtenir l’IP source de la requête.

Les applications fournissent généralement des services en s’appuyant sur des informations d’entrée. Si ces informations d’entrée ne dépendent pas du tuple à cinq éléments (IP source, port source, IP destination, port destination, protocole), alors ce service a une faible couplage réseau et n’a pas besoin de se soucier des détails du réseau.

Par conséquent, la plupart des gens n’ont pas besoin de lire cet article. Si vous êtes intéressé par le réseau ou souhaitez élargir vos horizons, vous pouvez continuer à lire pour en savoir plus sur les scénarios de service.

Cet article est basé sur k8s v1.29.4. Certaines descriptions mélangent pod et endpoint ; dans le contexte de cet article, ils peuvent être considérés comme équivalents.

Si des erreurs sont trouvées, n’hésitez pas à les signaler, je les corrigerai rapidement.

Pourquoi l’information IP source est-elle perdue ?

Clarifions d’abord ce qu’est l’IP source : lorsque A envoie une requête à B, et que B transfère la requête à C, bien que C voie l’IP source du protocole IP comme celle de B, cet article considère l’IP de A comme l’IP source.

Il existe principalement deux types de comportements qui entraînent la perte des informations source :

  1. Traduction d’adresses réseau (NAT), dans le but d’économiser les IPv4 publiques, l’équilibrage de charge, etc. Cela fait que le serveur voit l’IP de l’équipement NAT comme IP source, et non l’IP source réelle.
  2. Proxy, proxy inverse (RP, Reverse Proxy) et équilibrage de charge (LB, Load Balancer) appartiennent tous à cette catégorie, appelée ci-après serveur proxy. Ces services proxy transmettent les requêtes aux services backend, mais remplacent l’IP source par leur propre IP.
  • Le NAT consiste simplement à échanger l’espace de ports contre l’espace IP. Les adresses IPv4 étant limitées, une adresse IP peut mapper 65535 ports. La plupart du temps, ces ports ne sont pas épuisés, permettant à plusieurs sous-réseaux IP de partager une IP publique, en les distinguant par les ports. Sa forme d’utilisation est : IP publique:port public -> IP privée_1:port privé. Pour plus de détails, veuillez consulter Traduction d’adresses réseau.
  • Les services proxy servent à masquer ou exposer. Les services proxy transmettent les requêtes aux services backend tout en remplaçant l’IP source par leur propre IP, masquant ainsi l’IP réelle des services backend pour les protéger. La forme d’utilisation des services proxy est : IP client -> IP proxy -> IP serveur. Pour plus de détails, veuillez consulter Proxy.

Le NAT et les serveurs proxy sont très courants, et la plupart des services ne peuvent pas obtenir l’IP source de la requête.

Ceci sont les deux voies courantes de modification de l’IP source ; n’hésitez pas à en ajouter d’autres.

Comment conserver l’IP source ?

Voici un exemple de requête HTTP :

Champ Longueur (octets) Décalage de bits Description
En-tête IP
IP source 4 0-31 Adresse IP de l’expéditeur
IP destination 4 32-63 Adresse IP du destinataire
En-tête TCP
Port source 2 0-15 Numéro de port source
Port destination 2 16-31 Numéro de port destination
Numéro de séquence 4 32-63 Utilisé pour identifier le flux de bytes envoyé par l’expéditeur
Numéro d’accusé de réception 4 64-95 Si le drapeau ACK est défini, c’est le numéro de séquence attendu suivant
Décalage de données 4 96-103 Nombre d’octets de l’en-tête TCP jusqu’au début des données
Réservé 4 104-111 Champ réservé, non utilisé, défini à 0
Drapeaux 2 112-127 Divers drapeaux de contrôle comme SYN, ACK, FIN, etc.
Taille de fenêtre 2 128-143 Quantité de données que le récepteur peut recevoir
Somme de contrôle 2 144-159 Utilisée pour détecter les erreurs pendant la transmission
Pointeur urgent 2 160-175 Position des données urgentes que l’expéditeur souhaite que le récepteur traite en priorité
Options Variable 176-… Peut inclure horodatage, longueur maximale de segment, etc.
En-tête HTTP
Ligne de requête Variable …-… Inclut la méthode de requête, l’URI et la version HTTP
Champs d'en-tête Variable …-… Contient divers champs d’en-tête comme Host, User-Agent, etc.
Ligne vide 2 …-… Utilisée pour séparer l’en-tête et le corps
Corps Variable …-… Corps optionnel de la requête ou de la réponse

En examinant la structure de cette requête HTTP, on voit que les options TCP, la ligne de requête, les champs d’en-tête et le corps sont variables. L’espace des options TCP est limité et généralement pas utilisé pour transmettre l’IP source. La ligne de requête porte des informations fixes non extensibles. Le corps HTTP chiffré ne peut pas être modifié. Seuls les champs d'en-tête HTTP conviennent pour étendre et transmettre l’IP source.

On peut ajouter le champ X-REAL-IP dans l’en-tête HTTP pour transmettre l’IP source. Cette opération est généralement effectuée sur le serveur proxy, qui transmet ensuite la requête au service backend, permettant à ce dernier d’obtenir l’information IP source via ce champ.

Notez qu’il faut s’assurer que le serveur proxy est avant l’équipement NAT pour obtenir la véritable IP source de la requête. Chez Alibaba Cloud, on voit le produit Load Balancer comme une catégorie distincte, sa position dans le réseau diffère de celle d’un serveur d’application ordinaire.

Guide d’opérations K8S

Prenons l’exemple du projet whoami pour le déploiement.

Créer un Deployment

Créons d’abord le service :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
        - name: whoami
          image: docker.io/traefik/whoami:latest
          ports:
            - containerPort: 8080

Cette étape crée un Deployment contenant 3 Pods, chacun avec un conteneur exécutant le service whoami.

Créer un Service

On peut créer un service de type NodePort ou LoadBalancer pour un accès externe, ou un service de type ClusterIP pour un accès interne au cluster, puis ajouter un service Ingress pour exposer l’accès externe.

Le NodePort peut être accédé via NodeIP:NodePort ou via un service Ingress, ce qui est pratique pour les tests. Cette section utilise un service NodePort.

apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30002

Après création du service, accéder via curl whoami.example.com:30002 montre que l’IP retournée est le NodeIP, et non l’IP source de la requête.

Veuillez noter que ce n’est pas la bonne IP cliente ; ce sont des IP internes du cluster. Voici ce qui se passe :

  • Le client envoie le paquet à node2:nodePort
  • node2 remplace l’IP source du paquet par son propre IP (SNAT)
  • node2 remplace l’IP destination du paquet par l’IP du Pod
  • Le paquet est routé vers node1, puis vers l’endpoint
  • La réponse du Pod est routée vers node2
  • La réponse du Pod est envoyée au client

Représenté en diagramme :

Configurer externalTrafficPolicy: Local

Pour éviter cela, Kubernetes a une fonctionnalité pour conserver l’IP source du client. Si on définit service.spec.externalTrafficPolicy à Local, kube-proxy ne proxyfie les requêtes que vers les endpoints locaux, sans transférer le trafic vers d’autres nœuds.

apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30002

Testez avec curl whoami.example.com:30002. Lorsque whoami.example.com est résolu vers les IP de plusieurs nœuds du cluster, il y a une certaine probabilité d’échec d’accès. Assurez-vous que l’enregistrement DNS ne contient que les IP des nœuds où se trouvent les endpoints (pods).

Cette configuration a un coût : elle perd la capacité d’équilibrage de charge au niveau du cluster. Le client n’obtient une réponse que s’il accède à un nœud où un endpoint est déployé.

Limitation des chemins d’accès

Lorsque le client accède au Nœud 2, il n’y a pas de réponse.

Créer un Ingress

La plupart des services fournis aux utilisateurs utilisent http/https. La forme https://ip:port peut sembler étrangère aux utilisateurs. Généralement, on utilise un Ingress pour mapper le service NodePort créé ci-dessus vers les ports 80/443 d’un domaine.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
  namespace: default
spec:
  ingressClassName: external-lb-default
  rules:
    - host: whoami.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami-service
                port:
                  number: 80

Après application, testez avec curl whoami.example.com. On voit que ClientIP est toujours l’IP du Pod de l’Ingress Controller sur le nœud de l’endpoint.

root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...

root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME                                       READY   STATUS    RESTARTS   AGE    IP           NODE          NOMINATED NODE   READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7   1/1     Running   0          3d2h   10.42.1.10   k3s-agent-1   <none>           <none>

Utiliser un Ingress comme proxy inverse pour un service NodePort ajoute deux couches de service avant l’endpoint. Le diagramme suivant montre la différence entre les deux.

graph LR
    A[Client] -->|whoami.example.com:80| B(Ingress)
    B -->|10.43.38.129:32123| C[Service]
    C -->|10.42.1.1:8080| D[Endpoint]
graph LR
    A[Client] -->|whoami.example.com:30001| B(Service)
    B -->|10.42.1.1:8080| C[Endpoint]

Dans le chemin 1, lors d’un accès externe à Ingress, le trafic arrive d’abord à l’endpoint Ingress Controller, puis à l’endpoint whoami.
L’Ingress Controller est en substance un service LoadBalancer,

kubectl -n ingress-nginx get svc

NAMESPACE   NAME             CLASS   HOSTS                       ADDRESS                                              PORTS   AGE
default     echoip-ingress   nginx   ip.example.com       172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707   80      18h
default     whoami-ingress   nginx   whoami.example.com   172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707   80      16h

Par conséquent, on peut définir externalTrafficPolicy sur l’Ingress Controller comme mentionné précédemment pour conserver l’IP source.

Il faut également définir use-forwarded-headers à true dans le configmap de ingress-nginx-controller, afin que l’Ingress Controller puisse reconnaître les champs X-Forwarded-For ou X-REAL-IP.

apiVersion: v1
data:
  allow-snippet-annotations: "false"
  compute-full-forwarded-for: "true"
  use-forwarded-headers: "true"
  enable-real-ip: "true"
  forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.10.1
  name: ingress-nginx-controller
  namespace: ingress-nginx

La différence principale entre un service NodePort et le service ingress-nginx-controller est que le backend du NodePort n’est généralement pas déployé sur chaque nœud, tandis que le backend de ingress-nginx-controller l’est généralement sur chaque nœud exposé.

Contrairement au service NodePort où définir externalTrafficPolicy entraîne l’absence de réponse pour les requêtes inter-nœuds, l’Ingress peut d’abord définir l’EN-TÊTE avant de proxyfier, réalisant ainsi la conservation de l’IP source et l’équilibrage de charge.

Conclusion

  • La traduction d’adresses (NAT), les proxies, proxies inverses (Reverse Proxy) et équilibrage de charge (Load Balance) entraînent la perte de l’IP source.
  • Pour éviter la perte de l’IP source, le serveur proxy peut définir l’IP réelle dans le champ d’en-tête HTTP X-REAL-IP lors du transfert. En cas de multiples couches de proxy, utiliser le champ X-Forwarded-For, qui enregistre sous forme de pile la liste des IP de la source et du chemin proxy.
  • Définir externalTrafficPolicy: Local sur un service NodePort du cluster conserve l’IP source, mais perd la capacité d’équilibrage de charge.
  • Sous la prémisse que ingress-nginx-controller est déployé sous forme de daemonset sur tous les nœuds de rôle loadbalancer, définir externalTrafficPolicy: Local conserve l’IP source tout en conservant la capacité d’équilibrage de charge.

Références