Ingress-Nginx dans Kubernetes
Principes de l'Ingress-Nginx
Équilibrage de charge : couche 4 vs couche 7
Équilibrage de couche 4 (ex : LVS) : Le répartiteur agit uniquement comme un aiguilleur de trafic. Le client établit une connexion TCP directe avec le serveur backend via le répartiteur. Celui-ci ne fait que transmettre les paquets sans inspecter le contenu des requêtes.
Équilibrage de couche 7 (ex : Nginx) : Le répartiteur joue le rôle de proxy inverse. Il termine la connexion TCP du client, analyse la requête HTTP, puis établit une nouvelle connexion TCP vers le serveur backend. Cette approche permet une inspection approfondie du trafic et un routage basé sur les en-têtes, les chemins d'URL ou les noms d'hôte.
Avantages de la double connexion TCP
Avec un répartiteur de couche 4 comme LVS, si un client se connecte en HTTPS, le répartiteur transmet simplement le flux chiffré tel quel. Avec Nginx (couche 7), le répartiteur peut décider du protocole entre lui et le backend : HTTP ou HTTPS. Dans un environnement réseau interne sécurisé au sein d'un cluster, l'utilisation de HTTP entre Nginx et les backends évite la surcharge liée aux handshakes TLS multiples.
Routage par chemin d'URL
upstream groupe_admin {
server 10.0.1.11;
server 10.0.1.12;
}
upstream groupe_client {
server 10.0.1.14;
server 10.0.1.15;
}
server {
location /admin {
proxy_pass http://groupe_admin:80;
}
location /client {
proxy_pass http://groupe_client:80;
}
}
Le répartiteur de couche 4 ne peut router que par IP + port. Le répartiteur de couche 7 peut inspecter le chemin de l'URL et diriger le trafic vers différents pools de backends selon les règles location.
Simulation d'Ingress dans un cluster Kubernetes
Dans un cluster Kubernetes, on pourrait manuellement déployer un pod Nginx, l'exposer via un Service de type NodePort, puis configurer le nginx.conf pour pointer vers les Services backend. Cependant, cette approche pose un problème : à chaque modification des Pods backend, il faut mettre à jour manuellement la configuration Nginx et recharger.
L'Ingress-Nginx résout ce problème en surveillant automatiquement les objets Ingress dans l'API Kubernetes. Lorsqu'une règle Ingress est créée ou modifiée, le contrôleur Nginx met à jour dynamiquement nginx.conf et applique les changements.
Architecture interne du contrôleur Ingress-Nginx
L'API Kubernetes fait tourner une goroutine qui surveille les changements d'objets Ingress. Les événements sont classés en deux canaux : les informations non critiques passent par un tampon intermédiaire puis sont récupérées périodiquement, tandis que les informations critiques sont envoyées directement à la file de synchronisation. Le contrôleur décide ensuite s'il faut recharger Nginx ou appliquer une mise à jour asynchrone via Lua. Les rechargements sont minimisés car chaque reload interrompt les connexions actives.
Composants alternatifs
- Istio Ingress Gateway : orienté microservices, offre une excellente observabilité du trafic avec visualisation graphique.
- Traefik : proxy inverse écrit en Go, nativement conçu pour le cloud, détecte automatiquement les changements de configuration.
- APISIX Ingress Controller : populaire dans l'écosystème microservices Java.
Déploiement et configuration
Installation via Helm
Préparation des images sur tous les nœuds :
# Sur le nœud master
docker load -i ingress-nginx-controller-v1.9.4.tar
docker load -i ingress-nginx-kube-webhook-certgen-v20231011-8b53cabe0.tar
# Distribution vers les workers
scp *.tar worker1:/root/
scp *.tar worker2:/root/
# Sur chaque worker
for img in *ingress*.tar; do docker load -i "$img"; done
Configuration du chart Helm (values.yaml) :
# Réseau hôte pour éviter le NodePort
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
# DaemonSet pour une haute disponibilité
kind: DaemonSet
# Classe Ingress par défaut
ingressClassResource:
name: nginx
enabled: true
default: true
# Désactiver les vérifications de digest
# digest: sha256:...
# digestChroot: sha256:...
La stratégie DNS ClusterFirstWithHostNet est nécessaire quand hostNetwork: true. Le Pod partage la pile réseau de l'hôte et résout d'abord via le DNS de l'hôte, puis via kube-dns.
kubectl create namespace ingress-nginx
helm install ingress-nginx -n ingress-nginx . -f values.yaml
kubectl get pod -o wide -n ingress-nginx
NAME READY STATUS NODE
ingress-nginx-controller-abc12 1/1 Running worker1
ingress-nginx-controller-xyz34 1/1 Running worker2
Proxy HTTP
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app-v1
spec:
replicas: 2
selector:
matchLabels:
app: web-v1
template:
metadata:
labels:
app: web-v1
spec:
containers:
- name: app
image: myapp:v1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: web-app-v1-svc
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: web-v1
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-app-v1-ingress
spec:
ingressClassName: nginx
rules:
- host: app1.exemple.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app-v1-svc
port:
number: 80
L'accès direct à l'IP sans en-tête Host renvoie 404, car aucun serveur par défaut n'est configuré. Après ajout d'une entrée DNS locale dans /etc/hosts ou C:\Windows\System32\drivers\etc\hosts, le domaine app1.exemple.com répond correctement.
Proxy HTTPS avec terminaison SSL
# Génération du certificat
openssl req -x509 -sha256 -nodes -days 365 \
-newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=nginxsvc/O=nginxsvc"
# Création du Secret Kubernetes
kubectl create secret tls tls-secret --key tls.key --cert tls.crt
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
rules:
- host: secure.exemple.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-app-svc
port:
number: 80
tls:
- hosts:
- secure.exemple.com
secretName: tls-secret
La terminaison SSL se fait au niveau d'Ingress-Nginx : le client se connecte en HTTPS, mais le trafic vers le Service backend utilise HTTP en clair. C'est ce qu'on appelle la « déchargement SSL » (SSL offloading).
Authentification BasicAuth
# Création du fichier d'authentification
htpasswd -c authfile adminuser
# Saisir le mot de passe deux fois
# Encapsulation dans un Secret
kubectl create secret generic basic-auth-secret --from-file=authfile
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: protected-app-ingress
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth-secret
nginx.ingress.kubernetes.io/auth-realm: 'Accès restreint'
spec:
ingressClassName: nginx
rules:
- host: protect.exemple.com
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: protected-app-svc
port:
number: 80
Redirection d'URL
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: redirect-ingress
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: https://www.exemple.org
nginx.ingress.kubernetes.io/permanent-redirect-code: '301'
spec:
ingressClassName: nginx
rules:
- host: old.exemple.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: dummy-svc
port:
number: 80
# Vérification
curl old.exemple.com -I
HTTP/1.1 301 Moved Permanently
Location: https://www.exemple.org
Réécriture d'URL (Rewrite)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: api.exemple.com
http:
paths:
- path: /svc(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: rewrite-backend-svc
port:
number: 80
Différence entre Rewrite et Redirect :
- Rewrite : modifie le chemin de la requête côté serveur, de manière transparente pour le client. Aucun code HTTP de redirection n'est renvoyé.
- Redirect : le serveur informe le client qu'il doit se rendre à une nouvelle URL (codes 301, 302, etc.). Le client effectue une nouvelle requête vers la nouvelle adresse.
Page d'erreur personnalisée - Backend par défaut
Activation du backend par défaut dans values.yaml :
defaultBackend:
enabled: true
name: defaultbackend
image:
registry: docker.io
image: custom-error-pages
tag: "errweb1.0"
pullPolicy: IfNotPresent
port: 80
# Mise à jour du déploiement
helm upgrade -f values.yaml ingress-nginx . -n ingress-nginx
Désormais, toute requête ne correspondant à aucune règle Ingress renvoie la page d'erreur personnalisée au lieu d'un 404 standard.
Page d'erreur personnalisée - Backend spécifique
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: custom-error-ingress
annotations:
nginx.ingress.kubernetes.io/default-backend: 'error-page-svc'
nginx.ingress.kubernetes.io/custom-http-errors: "404,415"
spec:
rules:
- host: err.exemple.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: main-app-svc
port:
number: 80
Le Service error-page-svc reçoit les requêtes uniquement lorsque le backend principal renvoie les codes 404 ou 415.
Routage basé sur les en-têtes (User-Agent)
Activation des annotations snippet dans le ConfigMap :
kubectl edit configmap ingress-nginx-controller -n ingress-nginx
# Ajouter :
data:
allow-snippet-annotations: "true"
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ua-routing-ingress
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
set $mobile_flag 0;
if ($http_user_agent ~* "(Android|IPhone)") {
set $mobile_flag 1;
}
if ($mobile_flag = 1) {
return 302 http://m.exemple.com;
}
spec:
rules:
- host: www.exemple.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: desktop-svc
port:
number: 80
# Test
curl www.exemple.com -H 'User-Agent: Android' -I
HTTP/1.1 302 Moved Temporarily
Location: http://m.exemple.com
Listes de contrôle d'accès (IP)
ConfigMap (portée globale) vs Annotations (portée par Ingress). Les annotations ont une priorité supérieure au ConfigMap.
Liste noire via ConfigMap :
kubectl edit configmap ingress-nginx-controller -n ingress-nginx
data:
block-cidrs: 192.168.1.50
Liste noire via Annotations :
metadata:
annotations:
nginx.ingress.kubernetes.io/server-snippet: |-
deny 192.168.1.50;
allow all;
Liste blanche via ConfigMap :
data:
whitelist-source-range: 192.168.1.60
Liste blanche via Annotations :
metadata:
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: 192.168.1.60
Limitation de débit (Rate Limiting)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rate-limited-ingress
annotations:
nginx.ingress.kubernetes.io/limit-connections: "1"
# Autres options disponibles :
# nginx.ingress.kubernetes.io/limit-rps: "5"
# nginx.ingress.kubernetes.io/limit-rpm: "100"
# nginx.ingress.kubernetes.io/limit-rate: "100" # en Ko/s
# nginx.ingress.kubernetes.io/limit-whitelist: "192.168.1.0/24"
spec:
rules:
- host: limited.exemple.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: limited-svc
port:
number: 80
# Test de charge sans limitation
ab -c 10 -n 100 http://limited.exemple.com/ | grep requests
Complete requests: 100
Failed requests: 0
# Avec limit-connections: "1"
ab -c 10 -n 100 http://limited.exemple.com/ | grep requests
Complete requests: 100
Failed requests: 88
Déploiement Canary (Grayscale)
Version stable (v1) :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: stable-ingress
spec:
rules:
- host: canary.exemple.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: stable-svc
port:
number: 80
Version canary (v2) avec 10% du trafic :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: canary-ingress
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: canary.exemple.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: canary-svc
port:
number: 80
# Test de distribution
for i in {1..100}; do curl canary.exemple.com; done | sort | uniq -c
91 Hello MyApp | Version: v1
9 Hello MyApp | Version: v2
# Ajustement dynamique à 50%
kubectl edit ingress canary-ingress
# Modifier canary-weight: "50"
for i in {1..100}; do curl canary.exemple.com; done | sort | uniq -c
49 Hello MyApp | Version: v1
51 Hello MyApp | Version: v2
Proxy vers un backend HTTPS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backend-https-ingress
annotations:
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
spec:
rules:
- host: backend-tls.exemple.com
http:
paths:
- backend:
service:
name: tls-backend-svc
port:
number: 443
path: /
pathType: ImplementationSpecific
Le champ http dans les règles est utilisé quel que soit le protocole backend ; c'est l'annotation backend-protocol qui détermine le protocole de la connexion amont.
Proxy de couche 4 (TCP)
Ajout de l'argument au DaemonSet :
kubectl edit daemonset -n ingress-nginx ingress-nginx-controller
# Ajouter dans les args :
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-cm
apiVersion: v1
kind: ConfigMap
metadata:
name: tcp-services-cm
namespace: ingress-nginx
data:
"9000": "default/backend-svc:80"
Le port 9000 du nœud est maintenant redirigé vers le port 80 du Service spécifié.
Proxy UDP
kubectl edit daemonset -n ingress-nginx ingress-nginx-controller
# Ajouter :
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services-cm
apiVersion: v1
kind: ConfigMap
metadata:
name: udp-services-cm
namespace: ingress-nginx
data:
"53": "kube-system/kube-dns:53"
Tracing distribué avec Jaeger
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
labels:
app: jaeger
spec:
replicas: 1
selector:
matchLabels:
app: jaeger
template:
metadata:
labels:
app: jaeger
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one
env:
- name: COLLECTOR_ZIPKIN_HTTP_PORT
value: "9411"
ports:
- containerPort: 5775
protocol: UDP
- containerPort: 6831
protocol: UDP
- containerPort: 16686
protocol: TCP
- containerPort: 9411
protocol: TCP
readinessProbe:
httpGet:
path: "/"
port: 14269
initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: jaeger-query
spec:
ports:
- name: query-http
port: 80
targetPort: 16686
selector:
app: jaeger
type: NodePort
Activation du tracing dans le ConfigMap :
kubectl edit configmap ingress-nginx-controller -n ingress-nginx
data:
enable-opentracing: "true"
jaeger-collector-host: jaeger-agent.ingress-nginx.svc.cluster.local
Redémarrage du pod pour appliquer la configuration :
kubectl delete pod -l app=jaeger -n ingress-nginx
kubectl get svc jaeger-query -n ingress-nginx
# Accès à l'interface Jaeger via le NodePort exposé
Récapitulatif : couche 4 vs couche 7
Couche 4 : une seule connexion TCP de bout en bout entre le client et le serveur. Le répartiteur ne fait que transmettre les paquets au niveau réseau.
Couche 7 : deux connexions TCP distinctes. La première entre le client et le proxy, la seconde entre le proxy et le serveur backend. Le proxy peut inspecter, modifier et router le trafic en fonction du contenu HTTP.