Ingress
Dans les derniers articles concernant les objets de type Service, nous avons vu comment on a réussi à exposer une application à l’internet via un load balancer. Nous avons exposé l’application via le protocole TCP et un port spécifique. Le protocole TCP se trouve au niveau 4 du modèle OSI. Du coup, tous les traitements applicatifs, donc niveau 7 du modèle OSI, doivent être assurés par l’application qui tourne dans les pods sur k8s. Et le load balancing qu’on a fourni grâce au Service est donc aussi de niveau 4.
Parfois (même souvent), nous avons besoin de faire du load balancing de niveau 7 et traitements applicatifs, donc de niveau 7, sur les flux avant qu’ils n’atteignent les pods, surtout qu’on l’application en question utilise le protocole HTTP(S) pour fonctionner. C’est le cas des serveurs web et des APIs REST, donc d’une grande partie des applications auxquelles on accède sur internet.
Par exemple, nous avons besoin de faire de la terminaison TLS pour que les pods n’aient pas besoin de le faire, du load balancing et du routage pas uniquement au niveau des ports TCP mais aussi des URLs HTTP. Ces traitements sont faits dans Kubernetes grâce à de nouveaux objets qu’on appelle Ingress, qui sont, vous l’aurez compris, principalement des reverse proxy.
Ingress controller
Pour créer des objets Ingress, comme pour tout objet dans Kubernetes, il nous faut un controller qui va écouter la création des objets et faire la bonne configuration. Il y a énormément d’implémentations d’ingress controllers qui ont plus ou moins de fonctionnalités avancées. L’implémentation la plus connue et utilisée est la Nginx Ingress Controller qui, comme son nom l’indique, utilise Nginx comme reverse proxy. Cette implémentation est celle qui a été développée par la communauté Kubernetes en utilisant la version open source de Nginx, à ne pas confondre avec la Nginx Ingress Controller développée et vendue par l’entreprise Nginx.
Pour donner un exemple du fonctionnement des objets Ingress, nous allons utiliser la version communautaire de Nginx Ingress Controller que nous allons déployer sur un cluster Azure (pour avoir l’accès avec une adresse IP publique).
Pour installer l’ingress controller, nous allons utiliser Helm (dont nous allons parler dans un futur article) :
helm upgrade --install ingress-nginx-test ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace
Depuis notre namespace ingress-nginx, nous pouvons voir le pod de l’ingress controller qui a été démarré :
kubectl get pods
NAME READY STATUS RESTARTS AGE
ingress-nginx-test-controller-76f88fdb49-7b7f4 1/1 Running 0 33s
Ce pod contient le reverse proxy nginx qui va réceptionner les requêtes HTTP(S) depuis internet et va ensuite les transmettre ensuite aux services concernés.
Pour que ce pod réceptionne les requêtes, il doit être exposé à l’extérieur du cluster. Comme on l’a vu dans l’article précédent, nous devons pour ça utiliser un Service de type LoadBalancer. Lors de l’installation avec Helm, un Service a bien été créé avec le bon type :
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-test-controller LoadBalancer 172.16.80.112 20.101.169.20 80:32600/TCP,443:31138/TCP 8m16s
Maintenant nous pouvons bien contacter le reverse proxy via l’adresse publique :
curl http://20.101.169.20
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
C’est normal qu’il nous réponde 404 Not Found parce qu’il n’y a encore aucune application derrière le reverse proxy.
Nous allons maintenant déployer un serveur web simple derrière l’ingress. Nous commencerons par déployer le serveur web sans exposition externe :
kubectl create deployment apache-ingress --image=bitnami/apache:latest --replicas=2
deployment.apps/apache-ingress created
kubectl get pods
NAME READY STATUS RESTARTS AGE
apache-ingress-c8fcbd778-72b8m 1/1 Running 0 15s
apache-ingress-c8fcbd778-lsn4l 1/1 Running 0 15s
Nous allons l’exposer uniquement en ClusterIP :
kubectl expose deployment apache-ingress --port=8080 --target-port=8080 --type=ClusterIP
service/apache-ingress exposed
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
apache-ingress ClusterIP 172.16.101.195 <none> 8080/TCP 10s
On voit bien que le service apache n’est pas exposé avec une EXTERNAL-IP, mais uniquement en interne.
Tout est en place maintenant pour exposer apache avec un objet ingress. Voici la description (web-server-ingress.yaml) de l’objet que nous allons créer :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-server-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: customwebsite.com
http:
paths:
- pathType: Prefix
path: /apache
backend:
service:
name: apache-ingress
port:
number: 8080
Analysons chaque ligne de ce manifest :
apiVersion: l’API à appeler sur k8s pour demander un objet Ingress est la networking.k8s.io/v1 kind: l’objet ingress en question metadata: les métadonnées dont le nom par exemple name: le nom à donner à l’objet ingress annotations: les configurations spécifiques à ajouter à l’objet ingress. Ici on lui dit de réécrire le chemin de la requête, ce qu’on va expliquer plus tard. spec: la spécification de l’objet ingress ingressClassName: le nom de la class ingress qu’on veut utiliser pour exposer apache. On va parler de ça juste après. rules: les règles de routage HTTP qu’on demande au reverse proxy host: le nom de domaine sur lequel doit écouter le reverse proxy. Ici on a mis customwebsite.com pour exemple paths: la liste des chemins que le reverse proxy doit écouter pathType: le type de path qui va correspondre (match en anglais) à cette règle. Ici le type Prefix permet d’intercepter tous les appels précédés de la chaîne de caractères qui correspond. path: Ici il va écouter tous les appels vers /apache, donc tout ce qui arrive pour http://customwebsite.com/apache/* backend: pour ce chemin, quel backend on va utiliser service: on utilise ici comme backend un objet Service, celui qu’on a créé pour apache name: le nom du service vers lequel on renvoie les requêtes qui correspondent à /apache port: vers quel port du service renvoyer les requêtes number: ici on utilise le port 8080 car c’est celui qu’on a exposé avec le service
Cette configuration au final permet de répondre aux appels vers l’URL : http://customwebsite.com/apache
Rewrite target
Qu’est-ce que fait notre annotation “nginx.ingress.kubernetes.io/rewrite-target: /$2” ?
En fait, la configuration qu’on a donné à l’ingress dit qu’il va orienter tout ce qui vient vers http://customwebsite.com/apache/ vers nos pods apache. Sauf que nos serveurs apache sont configurés pour écouter sur le chemin root “/” de l’URL : http://customwebsite.com/
Si les pods apache reçoivent http://customwebsite.com/apache ils vont répondre avec une erreur, car il n’y a rien pour eux sur /apache. Donc ce qu’on dit à l’ingress, c’est de réécrire l’URL avant de l’envoyer au pod. On lui dit, globalement, de mettre tout qu’il a en deuxième position ($2) de l’URL reçue en première position avant de la renvoyer.
C’est-à-dire que lorsque l’ingress controller va recevoir http://customwebsite.com/apache/page1, il va envoyer aux pods l’URL : http://customwebsite.com/page1.
Pour plus d’infos sur les configurations possibles de réécriture d’URL, voici la documentation officielle : rewrite-target
IngressClass
Dans la description de l’Ingress, nous avons demandé à ce qu’on utilise l’IngressClass qui s’appelle nginx. L’IngressClass est un objet qui permet de différencier tous les ingress controllers et leurs configurations qu’on a déployés dans un cluster. On pourrait avoir par exemple un ingress controller pour le trafic critique, et un pour le trafic standard pour ne pas mélanger les deux. On pourrait avoir un ingress controller par environnement déployé sur le cluster, ou un ingress controller par type de transformations à faire sur les requêtes, par exemple un qui fait uniquement reverse proxy comme nginx, et un qui fait aussi API Gateway comme Emissary ou Kong. On pourrait avoir un ingress controller qui repose sur un pod comme nginx qu’on a déployé, et un ingress controller qui repose sur un service cloud comme celui d’Azure qui repose sur le service Application Gateway.
Dans notre cas, nous avons un seul IngressClass qui a été créé lors de l’installation avec Helm :
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 52m
Cette configuration correspond à ce qui est déployé dans le pod du controller :
kubectl describe pods ingress-nginx-test-controller-76f88fdb49-7b7f4
…
Containers:
controller:
Container ID: containerd://bc2bb0fbb6fcee9684be5934f92eb8e4be46ce8d00e80fb878bb95ce0b864210
Image: registry.k8s.io/ingress-nginx/controller:v1.7.0@sha256:7612338342a1e7b8090bef78f2a04fffcadd548ccaabe8a47bf7758ff549a5f7
Image ID: registry.k8s.io/ingress-nginx/controller@sha256:7612338342a1e7b8090bef78f2a04fffcadd548ccaabe8a47bf7758ff549a5f7
Ports: 80/TCP, 443/TCP, 8443/TCP
Host Ports: 0/TCP, 0/TCP, 0/TCP
Args:
/nginx-ingress-controller
--publish-service=$(POD_NAMESPACE)/ingress-nginx-test-controller
--election-id=ingress-nginx-test-leader
--controller-class=k8s.io/ingress-nginx
--ingress-class=nginx
--configmap=$(POD_NAMESPACE)/ingress-nginx-test-controller
--validating-webhook=:8443
--validating-webhook-certificate=/usr/local/certificates/cert
--validating-webhook-key=/usr/local/certificates/key
…
Lorsqu’on crée un objet Ingress et qu’on demande à utiliser un IngressClassName spécifique, l’ingress controller qui est responsable de ce nom se charge de réceptionner les requêtes vers le nom de domaine spécifié.
Nous allons maintenant créer notre objet Ingress dans notre cluster :
kubectl apply -f web-server-ingress.yaml
ingress.networking.k8s.io/web-server-ingress created
Il a bien été créé, et comme on peut le voir, il a été attaché à l’IngressClass nginx. Donc il répond à l’adresse IP publique qui a été reçue par l’ingress controller plus haut, puisque c’est ce dernier qui réceptionne les requêtes comme on l’a dit :
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
web-server-ingress nginx customwebsite.com 20.101.169.20 80 4m31s
On peut voir dans le champ backend dans ce qui suit que l’ingress a bien compris que les requêtes vont être redirigés vers les pods apache qu’on a déployé plus haut :
kubectl describe ingress web-server-ingress
Name: web-server-ingress
Labels: <none>
Namespace: test
Address: 20.101.169.20
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
customwebsite.com
/apache apache-ingress:8080 (172.16.129.37:8080,172.16.142.96:8080)
Annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 37m (x3 over 68m) nginx-ingress-controller Scheduled for sync
L’objet Ingress est en fait une configuration de reverse proxy qu’on demande à l’ingress controller, et qui est transformée dans notre cas en configuration nginx, qu’on peut retrouver dans le pod de l’ingress controller :
kubectl exec -it ingress-nginx-test-controller-76f88fdb49-7b7f4 -- cat /etc/nginx/nginx.conf
…
server {
server_name customwebsite.com ;
listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;
set $proxy_upstream_name "-";
ssl_certificate_by_lua_block {
certificate.call()
}
location /apache/ {
set $namespace "test";
set $ingress_name "web-server-ingress";
set $service_name "apache-ingress";
set $service_port "8080";
set $location_path "/apache";
set $global_rate_limit_exceeding n;
…
Annotation ingress.class :
Pour info, avant d’utiliser les objets IngressClass pour spécifier l’ingress controller cible, on utilisait une annotation dans l’objet Ingress :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-server-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
Cette annotation est aujourd’hui dépréciée en faveur de l’IngressClassName: ingress deprecated annotation
Mais il arrive qu’on trouve certains projets qui créent des objets Ingress qui ne reposent encore que sur l’annotation. Avec le temps, cela ne devrait plus être le cas.
Test de l’accès via l’ingress
Maintenant que notre objet ingress est créé, on peut tester l’accès via l’adresse IP publique qui a été assignée à l’ingress controller :
curl --header 'Host: customwebsite.com' http://20.101.169.20/apache
<html><body><h1>It works!</h1></body></html>
En fait, l’ingress controller ne répond pas directement à l’adresse IP, mais uniquement à un nom de domaine spécifique. Ici nous avons demandé customwebsite.com comme nom de domaine pour l’Ingress. Si on avait enregistré ce nom de domaine dans un DNS, on aurait pu appeler directement http://customwebsite.com/apache . Mais dans notre cas, on peut simplement spécifier le host header (‘Host: customwebsite.com’) avec la commande curl pour que nginx comprenne quel nom de domaine on demande.
Récap graphique du chemin du trafic :
Conclusion
Nous avons vu comment utiliser un ingress pour exposer un ou plusieurs services HTTP à l’internet. La plupart du trafic internet qu’on a avec HTTP est en réalité sécurisé avec une couche TLS entre le client et le serveur. La combinaison HTTP et TLS est ce qu’on appelle HTTPS.
HTTPS est utilisé entre un client et un serveur, par exemple ici entre le navigateur de l’utilisateur et le serveur apache qui répond aux requêtes. Sauf que la couche TLS apporte du traitement en plus au serveur apache (qui tourne sur un pod k8s), ce qui ralentit son fonctionnement. L’ingress controller a la capacité de décharger les serveurs apache de cette tâche, et de s’occuper de la terminaison TLS de la requête du client, fonctionnalité très utile et très utilisée. C’est ce que nous allons voir dans l’article suivant.