Comment conserver l'IP source des requêtes après équilibrage de charge dans un cluster K8s
Categories:
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 :
- 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.
- 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é.

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-IPlors du transfert. En cas de multiples couches de proxy, utiliser le champX-Forwarded-For, qui enregistre sous forme de pile la liste des IP de la source et du chemin proxy. - Définir
externalTrafficPolicy: Localsur 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: Localconserve l’IP source tout en conservant la capacité d’équilibrage de charge.