Istio de A à Y
Lorsqu’on commence à travailler avec Kubernetes, on se rend vite compte que la gestion des communications entre les services n’est pas si simple. Dès que le trafic passe par un ingress, la seule chose que l’on peut faire pour observer ce qu’il se passe est de consulter les logs des pods, ce qui n’est ni pratique, ni efficace.
C’est pour cela que les services mesh ont été créés. Ils permettent de gérer les communications entre les services, de sécuriser et monitorer les échanges ainsi que contrôler le traffic. L’objectif de cette page est alors de vous présenter Istio, un service mesh open-source.
Découvrons ensemble Istio, comment l’installer, comment l’utiliser avec une bonne tasse de café ☕.
Mais avant de commencer, expliquons d’abord ce qu’est un service mesh.
Qu’est-ce qu’un service mesh ?
Un service mesh est une couche d’infrastructure qui permet de gérer les communications entre les services d’une application.
En bref : le service mesh s’intègre dans l’infrastructure d’une application composée de plusieurs sous-applications (micro-services par exemple) pour rajouter des fonctionnalités.
Dans un précédent article, j’avais presenté Consul pouvant être utilisé comme service mesh à l’aide d’un sidecar basé sur Envoy.
Dans l’exemple ci-dessus, chaque programme a besoin d’accéder à un autre, l’UI vers l’authentification et le backend, le backend vers le stockage et le service de queueing, et pour finir : les consumers vers le service queueing.
Avec ce schéma, se posent d’ores et déjà quelques questions :
- Comment autoriser / interdire les échanges entre deux services ?
- Comment sécuriser les échanges entre les services ?
- Qu’en est-il de l’observabilité ?
Par exemple, si nous voulons que l’UI puisse accéder au backend, mais pas au stockage, comment faire ? Ou si nous voulons que le backend puisse accéder au stockage, mais pas à l’UI ?
Une possibilité est d’utiliser des NetworkPolicies si notre CNI en est capable mais cela ne permet pas de gérer les intéractions en couche 7 (HTTP, gRPC, etc) excepté avec avec Cilium. Notamment, je ne peux pas limiter l’accès à une route spécifique (/api
, /endpoint/v3/ping
) ou par type de requête (GET, POST, etc).
C’est là qu’un service mesh comme Istio (ou Consul) intervient.
Comment un service mesh gère les échanges entre les services ?
Un service mesh utilise des proxies pour intercepter les requêtes entre les services. Ils vont donc servir d’intérmédiaire en rajoutant une couche de contrôle sur les communications.
Un peu comme un WAF (Web Application Firewall) pour les applications web. Chaque application va avoir son propre “routeur” qui va rediriger les requêtes entrantes ET sortantes vers le proxy du service de destination.
Ainsi, voici notre schéma avec un service mesh :
Chaque fois que l’application essaie de communiquer avec un autre service, le proxy intercepte la requête et la redirige vers le proxy du service de destination.
Pourquoi utiliser un service mesh ?
Lorsque vous n’avez que 2-3 applications, un service mesh peut sembler inutile. Mais dès que vous commencez à avoir plusieurs services, plusieurs équipes, plusieurs clusters, un service mesh devient vite pratique.
Vous pouvez permettre aux différents services de communiquer entre eux de manière sécurisée et contrôlée en faisant confiance à l’identité des services et non aux adresses IP ou aux noms DNS (qui peuvent être aiséments spoofés).
Et Istio dans tout ça ?
Istio peut être utilisée comme service mesh dans un cluster Kubernetes. En effet, il répond aux besoins cités plus haut : sécuriser les échanges, contrôler le traffic et monitorer les échanges.
C’est un projet totalement open-source ayant rejoins la CNCF (Cloud Native Computing Foundation) le 30 septembre 2022 et devenu un projet incubé le 12 juillet 2023.
Nous aurons l’occasion de reparler de l’architecture d’Istio, de ses composants, et de ses fonctionnalités plus tard dans cet article.
Mon environnement de lab
Pour ce lab, j’ai utilisé un cluster Kubernetes avec 3 nodes (1 master, 2 workers) installé avec Talos et Flannel comme CNI (habituellement, je préfère Cilium, mais j’ai eu des incompatibilités avec une certaine feature d’Istio dont je parlerai plus tard).
Voici la configuration utilisée pour mon cluster avec talhelper. N’hésitez pas à consulter mon article sur Talos pour avoir plus d’informations sur son installation.
Configuration Talhelper
---
clusterName: istio-cluster
talosVersion: v1.7.4
kubernetesVersion: v1.29.1
endpoint: https://192.168.128.27:6443
allowSchedulingOnMasters: true
cniConfig:
name: flannel
patches:
- |-
- op: add
path: /cluster/discovery/enabled
value: false
- op: replace
path: /machine/network/kubespan
value:
enabled: false
- op: add
path: /machine/kubelet/extraArgs
value:
rotate-server-certificates: true
- op: add
path: /machine/files
value:
- content: |
[metrics]
address = "0.0.0.0:11234"
path: /var/cri/conf.d/metrics.toml
op: create
nodes:
- hostname: controlplane
ipAddress: 192.168.128.27
controlPlane: true
arch: amd64
installDisk: /dev/sda
- hostname: worker-1
ipAddress: 192.168.128.28
controlPlane: false
arch: amd64
installDisk: /dev/sda
- hostname: worker-2
ipAddress: 192.168.128.30
controlPlane: false
arch: amd64
installDisk: /dev/sda
controlPlane:
schematic:
customization:
systemExtensions:
officialExtensions:
- siderolabs/qemu-guest-agent
- siderolabs/iscsi-tools
worker:
schematic:
customization:
systemExtensions:
officialExtensions:
- siderolabs/qemu-guest-agent
- siderolabs/iscsi-tools
Nous aurons également besoin d’un metrics-server pour que les HPAs (Horizontal Pod Autoscaler) d’Istio fonctionnent correctement. Pour cela, j’ai déployé les manifests suivants permettant respectivement de déployer le metrics-server et le certificates-approver pour le kubelet.
kubectl apply -f https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/main/deploy/standalone-install.yaml
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Comme toujours, méfiez-vous des manifests appliqués directement depuis Internet et assurez-vous de les lire avant de les appliquer.
Installer Istioctl
Istioctl est l’outil en ligne de commande permettant de gérer Istio. Il permet de le déployer, de vérifier les status des composants, d’injecter des sidecars par manifest et bien plus.
La méthode la plus simple pour installer Istioctl est d’utiliser le script fourni par Istio (qui va en télécharger la dernière version) ou de récupérer directement le binaire sur la page de release d’Istio.
curl -L https://istio.io/downloadIstio | sh -
Astuce
Il est possible de spécifier la version à installer en utilisant la variable d’environnement ISTIO_VERSION
et l’architecture cible avec TARGET_ARCH
.
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.22.0 TARGET_ARCH=x86_64 sh -
Bien sûr, essayez au maximum d’éviter de télécharger des scripts et de les exécuter sans les lire. Une solution est aussi de télécharger le binaire soit-même.
Un paquet NixOS est également disponible pour installer Istioctl.
nix-env -iA nixpkgs.istioctl # via nixpkgs
nix-env -iA nixos.istioctl # via nixos
Nous avons maintenant le nécessaire pour installer Istio sur notre cluster Kubernetes.
Les profiles Istio
Avant d’installer Istio sur notre cluster, il est important de choisir un profil. Un profil est une configuration prédéfinie d’Istio qui va déterminer les composants à installer, les configurations par défaut et les fonctionnalités activées.
Il existe plusieurs profils Istio, chacun avec ses propres caractéristiques :
$ istioctl profile list
Istio configuration profiles:
ambient
default
demo
empty
minimal
openshift
openshift-ambient
preview
remote
stable
Pour voir la configuration d’un profil, vous pouvez utiliser la commande istioctl profile dump <profile>
.
Par exemple, la configuration du profil default
est visible avec la commande suivante :
istioctl profile dump default
Sa configuration est la suivante :
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
components:
base:
enabled: true
egressGateways:
- enabled: false
name: istio-egressgateway
ingressGateways:
- enabled: true
name: istio-ingressgateway
pilot:
enabled: true
hub: docker.io/istio
profile: default
tag: 1.22.1
values:
defaultRevision: ""
gateways:
istio-egressgateway: {}
istio-ingressgateway: {}
global:
configValidation: true
istioNamespace: istio-system
Pour comparer deux profils, vous pouvez utiliser la commande istioctl profile diff <profile1> <profile2>
.
Pour comparer les profils default
et demo
, vous pouvez utiliser la commande suivante :
istioctl profile diff default demo
Cela va afficher les différences entre les deux profils :
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
creationTimestamp: null
namespace: istio-system
spec:
components:
base:
enabled: true
egressGateways:
- - enabled: false
+ - enabled: true
name: istio-egressgateway
ingressGateways:
- enabled: true
name: istio-ingressgateway
pilot:
enabled: true
hub: docker.io/istio
profile: default
tag: 1.22.1
values:
defaultRevision: ""
gateways:
istio-egressgateway: {}
istio-ingressgateway: {}
global:
configValidation: true
istioNamespace: istio-system
+ profile: demo
Il est possible de personnaliser un profil en réutilisant un existant avec la commande istioctl profile dump <profile> > myprofile.yaml
et modifier le fichier myprofile.yaml
pour ajouter ou supprimer des composants.
Dans ce lab, nous utiliserons majoritairement le profil demo
qui est un profil complet avec toutes les fonctionnalités stables d’Istio activées.
Installer Istio
Pour installer Istio sur notre cluster Kubernetes, nous allons utiliser la commande istioctl install
suivi d’un profil ou d’un fichier de configuration.
- Pour utiliser un profil :
istioctl install --set profile=demo
- Pour utiliser un fichier de configuration :
istioctl install -f myprofile.yaml
Le profil choisit va générer les manifests nécessaires pour installer Istio et le configurer selon les spécifications du profil (il est aussi possible de passer par Helm, mais cette méthode ne semble pas être la plus recommandée).
C’est tout ?
Eh bien, oui. Istio va alors s’installer sur le cluster Kubernetes en créant un namespace istio-system
et en déployant les composants nécessaires.
Mais, il n’est pas encore effectif et aucun proxy n’a été injecté dans les pods. Pour cela, nous devons activer l’injection automatique d’Istio dans le namespace souhaité. On en parle un peu plus tard.
Notre application de test : Bookinfo
Nous allons utiliser l’application de test “Bookinfo” fournie par Istio. C’est une application composée de plusieurs microservices qui sera un bon exemple pour tester ce service mesh.
$ kubectl apply -n default -f https://raw.githubusercontent.com/istio/istio/release-1.22/samples/bookinfo/platform/kube/bookinfo.yaml
L’application Bookinfo est maintenant déployée sur notre cluster Kubernetes. Nous pouvons vérifier ce qui a été déployé avec la commande kubectl get-all -n default
(plugin krew).
$ kubectl get-all -n default
NAME NAMESPACE AGE
serviceaccount/bookinfo-details default 4m15s
serviceaccount/bookinfo-productpage default 4m9s
serviceaccount/bookinfo-ratings default 4m14s
serviceaccount/bookinfo-reviews default 4m12s
service/details default 4m15s
service/kubernetes default 15h
service/productpage default 4m10s
service/ratings default 4m14s
service/reviews default 4m13s
deployment.apps/details-v1 default 4m14s
deployment.apps/productpage-v1 default 4m8s
deployment.apps/ratings-v1 default 4m13s
deployment.apps/reviews-v1 default 4m11s
deployment.apps/reviews-v2 default 4m11s
deployment.apps/reviews-v3 default 4m11s
Essayons maintenant d’accéder à l’application Bookinfo. Pour cela, nous allons utiliser un port-forward pour y accéder depuis notre machine locale.
kubectl port-forward svc/productpage 9080:9080 -n default
Ensuite, ouvrons un navigateur et accédons à l’URL http://localhost:9080/productpage
.
En cliquant sur le bouton “Normal user”, une page similaire à celle-ci devrait s’afficher :
Mais, concrètement, que fait l’application Bookinfo ? Dans quel cas chaque microservice est-il utilisé ?
Nous avons 4 microservices dans l’application Bookinfo :
- productpage : le service frontend de l’application. Il appelle les services details et reviews pour afficher le contenu de la page.
- details : le service qui contient les détails du livre. Il n’appelle aucun autre service.
- reviews : le service qui contient les avis sur le livre. Il appelle le service ratings pour obtenir les notes.
- ratings : le service qui contient les notes des avis. Il n’appelle aucun autre service.
En rechargeant plusieurs fois la page, on peut voir que les “Reviews” changent en fonction de la version du service “reviews” utilisée. En effet, il en existe 3 versions :
- Version 1 : pas de ratings.
- Version 2 : des ratings avec des étoiles noires.
- Version 3 : des ratings avec des étoiles rouges.
Alors, comment la répartition des versions de “reviews” est-elle gérée ? Elle se fait via le service Kubernetes (avec un service de type ClusterIP) qui va rediriger les requêtes en mode “round-robin” vers les pods du service “reviews”.
Maintenant, nous allons activer l’injection des sidecars Istio. Il est possible de le faire de 3 manières :
- En injectant un label dans le pod créé par le deployment :
kubectl patch deployment -n default productpage-v1 -p '{"spec": {"template": {"metadata": {"labels": {"sidecar.istio.io/inject": "true"}}}}}'
- En patchant le manifests avant de le déployer (avec
istioctl kube-inject
, qui va ajouter les sidecars dans le manifest) :
wget https://raw.githubusercontent.com/istio/istio/release-1.22/samples/bookinfo/platform/kube/bookinfo.yaml
istioctl kube-inject -f bookinfo.yaml | kubectl apply -n default -f -
- En activant l’injection automatique dans le namespace et en redéployant les pods :
kubectl label namespace default istio-injection=enabled
kubectl rollout restart deployment -n default details-v1 productpage-v1 ratings-v1 reviews-v1 reviews-v2 reviews-v3
Peu importe l’option choisie, les sidecars Istio vont être injectés dans les pods de l’application Bookinfo. Vous pouvez vérifier que les sidecars ont bien été injectés avec la commande kubectl get pods -n default
.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
details-v1-64b7b7dd99-ctqd4 2/2 Running 0 118s
productpage-v1-6bc7f5c4c6-tsxdt 2/2 Running 0 114s
ratings-v1-c54575675-cq8bv 2/2 Running 0 118s
reviews-v1-76bf7c9d86-zbvts 2/2 Running 0 117s
reviews-v2-bb7869c75-n7rb5 2/2 Running 0 116s
reviews-v3-5f978f677b-2bqw5 2/2 Running 0 116s
Chaque pod a maintenant un sidecar Istio ⛵ qui va intercepter les requêtes entrantes et sortantes.
Afin d’obtenir plus d’informations à ce propos, il est possible d’utiliser les commandes istioctl analyze
et istioctl proxy-status
qui vont respectivement vérifier si la configuration Istio est correcte et si les proxy sont actifs.
$ istioctl analyze
✔ No validation issues found when analyzing namespace: default.
$ istioctl proxy-status
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
details-v1-7b6fb77db6-bwv5b.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
istio-egressgateway-b569895b5-ppk8f.istio-system Kubernetes SYNCED SYNCED SYNCED NOT SENT NOT SENT istiod-8596844f7d-z5rgl 1.21.0
istio-ingressgateway-694c4b4d85-78f95.istio-system Kubernetes SYNCED SYNCED SYNCED NOT SENT NOT SENT istiod-8596844f7d-z5rgl 1.21.0
productpage-v1-68dfd95669-qr69h.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
ratings-v1-6b47557bbb-cr6k9.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
reviews-v1-dd46dd5f-dkkkb.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
reviews-v2-5b65c4bdb-76pd4.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
reviews-v3-685dd59d69-tmzlf.default Kubernetes SYNCED SYNCED SYNCED SYNCED NOT SENT istiod-8596844f7d-z5rgl 1.21.0
Ne faites pas attention aux pods istio-egressgateway
et istio-ingressgateway
, ils ne sont pas liés à l’application Bookinfo.
Mais avant d’aller plus loin, on va s’armer de quelques outils pour faciliter notre travail avec Istio.
Notre suite d’observabilité
Istio est assez impitoyable pour ceux qui refusent de s’équiper suffisamment. Nous allons donc découvrir quelques outils qui vont nous aider à comprendre ce qu’il se passe dans notre cluster.
Kiali
Cet outil est indispensable pour visualiser les échanges entre les pods et il s’interface directement avec Istio pour récupérer les données des proxies. Ça sera notre outil principal pour vérifier le bon fonctionnement de nos applications.
Il est capable de :
- Visualiser les services et les échanges entre ces derniers.
- Vérifier/Modifier notre configuration Istio.
- Avoir des métriques sur les services.
C’est un réel couteau suisse pour Istio.
Pour installer Kiali, nous pouvons utiliser le manifest présent dans le dépôt Istio. Après l’avoir déployé, nous pouvons accéder à l’interface web de Kiali en utilisant un port-forward.
kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/addons/kiali.yaml
istioctl dashboard kiali
Note : istioctl dashboard kiali
permet simplement de faire un port-forward vers le service Kiali, vous pouvez aussi utiliser kubectl port-forward svc/kiali 20001:20001 -n istio-system
.
Astuce
À savoir que le manifests ne déploie qu’une version démo de Kiali (sans authentification), pour une utilisation en production, je vous recommande de consulter la documentation officielle pour configurer Kiali de façon plus pérenne et fidèle à vos besoins.
Générons un peu de traffic pour voir ce que Kiali peut nous montrer.
kubectl port-forward -n default svc/productpage 9080:9080 >/dev/null &
watch -n 1 curl -s http://localhost:9080/productpage -I
Dans la partie “Traffic Graph”, on peut voir les échanges entre les services (il manque le service “ratings”, mais c’est normal).
Cette page sera surement celle que vous allez consulter le plus souvent pour débugguer vos applications. Je vais beaucoup m’y référer dans la suite de cet article.
Jaeger & Zipkin
Zipkin et Jaeger sont des outils de tracing qui permettent de suivre le parcours d’une requête à travers les différents services. Ils permettent de voir le temps de réponse de chaque service, les erreurs et le délai des interractions.
Celui-ci est comparable à OpenTelemetry (que je n’ai pas encore pu tester) et est très utile pour comprendre les performances de vos applications et voir quel service est le goulot d’étranglement dans ces dernières.
Pour installer Jaeger, nous pouvons utiliser le manifest présent dans le dépôt Istio. Après l’avoir déployé, nous accédons à l’interface web de Jaeger en utilisant un port-forward.
kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/addons/jaeger.yaml
istioctl dashboard jaeger
Depuis Jaeger, je peux voir les traces de mes requêtes et voir le temps de réponse de chaque service.
Prenons une trace du service “productpage” (point d’entrée de l’application Bookinfo) et voyons les détails de la trace :
Bien sûr, il m’est possible de voir les détails de chaque service et de voir le temps de réponse de chaque service.
Bref, Jaeger est un outil très utile pour voir le détail des requêtes et avoir plus d’informations sur les performances de vos applications.
Kiali est plus orienté “vue d’ensemble” tandis que Jaeger est plus orienté “détails”.
Schéma de l’architecture de notre application Bookinfo :
Un cas d’usage classique est de chercher les traces d’une requête qui a échoué afin de comprendre pourquoi. Je peux ainsi chercher les traces de la requête qui a échoué et voir quel service a renvoyé une erreur.
En l’occurence, le front a juste renvoyé une erreur 404, nous verrons des cas plus intéressants plus tard.
Prometheus & Grafana
Pour la question des métriques, Istio s’intègre parfaitement avec Prometheus (lui-même lié à Grafana pour la visualisation des métriques). En utilisant les manifests fournis par Istio, nous pouvons déployer Prometheus et un dashboard Grafana pré-configuré pour afficher les métriques.
kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/addons/grafana.yaml
kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/addons/prometheus.yaml
istioctl dashboard grafana
Ainsi, nous avons des dashboards pour voir :
- les ressources utilisées par Istio.
- le temps de réponse entre les services.
- le status des requêtes (200, 404, etc).
- la bande passante utilisée par les services.
Maintenant que nous sommes prêt, on peut commencer à jouer avec Istio.
Exposer notre application
Pour l’instant, le traffic ne fait que passer par les sidecars sans qu’Envoy ne fasse quoi que ce soit (aucun filtre, aucun contrôle, pas de sécurité). On va arranger ça pour qu’Istio puisse faire son travail.
Le premier CRD (Custom Resource Definition) que nous allons voir est le VirtualService. Un VirtualService est un objet Istio qui permet de configurer les règles de routage pour un service Kubernetes.
La route créée par un VirtualService va être propagée à tous les sidecars qui vont alors rediriger le trafic en fonction de règles définies.
Par exemple, je vais créer le premier VirtualService pour le service “details” de l’application Bookinfo.
kind: VirtualService
apiVersion: networking.istio.io/v1beta1
metadata:
name: details-vs
namespace: default
spec:
hosts:
- details # valide pour details et details.default.svc.cluster.local
http:
- route:
- destination:
host: details
Depuis Kiali, on peut voir que “details” possède une nouvelle icône qui indique que le service est maintenant géré par un VirtualService.
Ce VirtualService rajoute 2 fonctionnalités passives :
- Si une requête échoue, Envoy va automatiquement la rejouer jusqu’à 3 fois.
- Le mTLS est activé en mode PERMISSIVE (le HTTP est toujours possible).
On reparlera du mTLS (et on expliquera ce que c’est) un peu plus tard.
Pour l’instant, contentons-nous de rendre le service “productpage” (le front de l’application Bookinfo) accessible via un VirtualService. Je vais en profiter également pour restreindre l’accès à la route “/” (la racine du site) qui contient une page qui n’est pas destinée à être vue par l’utilisateur.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- "productpage"
http:
- match:
- uri:
exact: /productpage
- uri:
prefix: /static
- uri:
exact: /login
- uri:
exact: /logout
- uri:
prefix: /api/v1/products
route:
- destination:
host: productpage
Avec notre kubectl port-forward
, nous ne pouvons pas tester les restrictions apportées par le VirtualService. Pour cela, faisons des requêtes directement depuis un pod du cluster.
$ kubectl exec deployments/ratings-v1 -c ratings -- curl http://productpage:9080/ -I -s
HTTP/1.1 404 Not Found
date: Sat, 22 Jun 2024 09:25:11 GMT
server: envoy
transfer-encoding: chunked
$ kubectl exec deployments/ratings-v1 -c ratings -- curl http://productpage:9080/productpage -I -s
HTTP/1.1 200 OK
server: envoy
date: Sat, 22 Jun 2024 09:25:55 GMT
content-type: text/html; charset=utf-8
content-length: 5293
vary: Cookie
x-envoy-upstream-service-time: 20
Remarque
Le header x-envoy-upstream-service-time
est un header ajouté par Envoy qui indique le temps de réponse du service de destination.
Vous pouvez le supprimer en modifiant le VirtualService pour ne pas l’afficher en rajoutant le code suivant :
options:
stagedTransformations:
early:
responseTransforms:
- responseTransformation:
transformationTemplate:
dynamicMetadataValues:
- metadataNamespace: body-logging
key: upstream-service-time
value:
text: '{{ header("x-envoy-upstream-service-time") }}'
headers:
x-envoy-upstream-service-time:
text: ''
On peut voir que la route “/” renvoie une erreur 404 (ce qui n’était pas le cas avant le VirtualService) tandis que la route “/productpage” renvoie bien la page d’accueil de l’application Bookinfo.
Maintenant… ce serait mieux si on pouvait accéder à l’application sans avoir à passer par un port-forward, non ?
Gateway
Une Gateway est un équivalent d’un Ingress. A la différence qu’un Ingress pointe directement vers un service tandis qu’une Gateway permet des fonctionnalités différentes pour rediriger le trafic différemment ou avec plus de monitoring.
Une Gateway est un point d’entrée pour le trafic entrant dans le cluster. Tout comme un Ingress avec un IngressController, une Gateway a besoin d’un GatewayController pour fonctionner, celle-ci est gérée par le pod istio-ingressgateway
dans le namespace istio-system
.
Avertissement
À savoir que le composant istio-ingressgateway
n’est pas systématiquement installé avec Istio (il l’est avec le profil demo
). Si vous utilisez un fichier de configuration à la place d’un profil, assurez-vous que le composant istio-ingressgateway
est bien activé comme dans l’exemple ci-dessous :
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
components:
base:
enabled: true
ingressGateways: #
- enabled: true # This is the GatewayController
name: istio-ingressgateway #
pilot:
enabled: true
hub: docker.io/istio
tag: 1.21.0
values:
defaultRevision: ""
gateways:
istio-egressgateway: {}
istio-ingressgateway: {}
global:
configValidation: true
istioNamespace: istio-system
profile: a-cup-of-coffee
Comme je n’ai pas de LoadBalancer, je vais utiliser un NodePort pour accéder à la Gateway :
kubectl patch service istio-ingressgateway -n istio-system --type='json' -p='[{"op": "replace", "path": "/spec/type", "value":"NodePort"}]'
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
export INGRESS_HOST=$(kubectl get po -l istio=ingressgateway -n istio-system -o jsonpath='{.items[0].status.hostIP}')
echo $INGRESS_HOST:$INGRESS_PORT # 192.168.128.30:30492
Gardez ces variables sous la main, on va beaucoup les utiliser.
Nous avons maintenant un point d’entrée vers le Gateway-Controller (ingress-gateway) d’Istio ! Essayons de faire une première requête vers la Gateway.
curl -s $INGRESS_HOST:$INGRESS_PORT/productpage -I -v
* Trying 192.168.128.30:30492...
* connect to 192.168.128.30 port 30492 failed: Connexion refusée
* Failed to connect to 192.168.128.30 port 30492 after 105 ms: Connexion refusée
* Closing connection 0
Ah ! Pourtant je suis sûr que j’ai bien exposé la Gateway et je suis bien dans le bon réseau. Pourquoi une connexion refusée ?
La raison : si aucune Gateway n’est liée à notre Gateway-Controller alors le traffic est rejeté. Commençons alors par créer notre objet Gateway.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: bookinfo-gateway
spec:
selector:
istio: ingressgateway # use istio default controller
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*" # It should match a domain-wildcard (ex: '*.istio.a-cup-of.coffee'), but in dev env, we can use a wildcard
Tentons d’accéder à la Gateway :
curl -s $INGRESS_HOST:$INGRESS_PORT -I
HTTP/1.1 404 Not Found
date: Sat, 22 Jun 2024 09:20:20 GMT
server: istio-envoy
transfer-encoding: chunked
Tentons alors d’accéder à la Gateway via la route “/productpage” (utilisée par le VirtualService “productpage”) :
$ curl -s $INGRESS_HOST:$INGRESS_PORT/productpage -I
HTTP/1.1 404 Not Found
date: Sat, 22 Jun 2024 09:52:17 GMT
server: istio-envoy
transfer-encoding: chunked
Pourquoi une 404 ? Parce que le VirtualService “productpage” n’est pas encore lié à la Gateway. On va s’en occuper tout de suite.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- "productpage"
gateways: # We add the Gateway
- bookinfo-gateway #
http:
- match:
- uri:
exact: /productpage
- uri:
prefix: /static
- uri:
exact: /login
- uri:
exact: /logout
- uri:
prefix: /api/v1/products
route:
- destination:
host: productpage
On retente ? Cette fois c’est la bonne !
$ curl -s $INGRESS_HOST:$INGRESS_PORT/productpage -I
HTTP/1.1 404 Not Found
date: Sat, 22 Jun 2024 10:29:52 GMT
server: istio-envoy
transfer-encoding: chunked
Toujours une 404 ? 😅
- Le VirtualService est bien configuré et fonctionne.
- La Gateway est bien configurée avec le bon VirtualService.
- Le gateway-controller est bien actif.
Certains d’entre vous auront peut-être déjà deviné la raison de cette 404. En fait, le service “productpage” n’est accessible que via le domaine “productpage.default.svc.cluster.local”.
$ curl -H "Host: productpage.default.svc.cluster.local" $INGRESS_HOST:$INGRESS_PORT/productpage -I
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 5290
vary: Cookie
x-envoy-upstream-service-time: 27
Astuce
Dans un VirtualService, le champ “hosts” doit correspondre aux noms de domaine où le service sera accessible. Lorsque l’host n’est pas un nom de domaine complet(FQDN), Istio va automatiquement le compléter avec le namespace et le domaine du cluster.
Par exemple, si je mets “productpage” dans le champ “hosts”, Istio va automatiquement le compléter en productpage.default.svc.cluster.local
.
Il est alors préférable de préciser le nom de domaine complet (productpage.default.svc.cluster.local
) dans le champ “hosts” pour éviter toute confusion.
Victoire, nous accédons maintenant à l’application Bookinfo via la Gateway ! 🎉
Depuis Kiali, voici ce que nous pouvons voir :
Astuce
Dans notre environnement de développement, nous pouvons utiliser un wildcard pour le champ “hosts” du VirtualService. Cela permet de rediriger le trafic entrant vers le service sans avoir à spécifier de nom de domaine particulier.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: bookinfo
spec:
hosts:
- "*"
gateways:
- bookinfo-gateway
http:
- match:
- uri:
exact: /productpage
- uri:
prefix: /static
- uri:
exact: /login
- uri:
exact: /logout
- uri:
prefix: /api/v1/products
route:
- destination:
host: productpage
Maintenant, attaquons-nous à l’application “reviews” qui est un peu particulière.
Gestion des versions avec DestinationRules
Le service “reviews” est un service qui a 3 versions différentes. Chaque version est accessible via un label différent (app=reviews, version=v1, v2, v3).
Le service (façon Kubernetes) est configuré pour rediriger le trafic vers les applications possédant le label app=reviews
. Mais comment faire pour rediriger le trafic vers une version spécifique ?
La réponse est : les DestinationRules, un objet Istio permettant d’appliquer un ensemble de traitements après que le trafic ait été routé par un VirtualService.
Par exemple, un DestinationRule peut :
- Modifier le mode de LoadBalancing.
- Créer un circuit breaker.
- Configurer le mTLS.
Et bien sûr le plus important : gérer les “subsets” des applications pour différencier les versions des mêmes services.
Voici à quoi va ressembler notre DestinationRule pour le service “reviews” :
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: reviews
spec:
host: reviews
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
- name: v3
labels:
version: v3
On attribue un nom à chaque subset en se basant sur le label version
de chaque pod. On peut alors rediriger le trafic vers une version spécifique en utilisant le subset correspondant.
Gestion du trafic avec Istio
Maintenant que nous avons une Gateway et que nous avons configuré les VirtualServices et les DestinationRules, nous allons pouvoir jouer avec les proxies Envoy et découvrir certaines fonctionnalités d’Istio.
Traffic-Shifting
Le Traffic-Shifting est une fonctionnalité d’Istio qui permet de rediriger le trafic vers une version spécifique d’un service. Cela permet de tester une nouvelle version d’une application sans impacter les utilisateurs.
Pour cela, on va utiliser un VirtualService pour rediriger les échanges vers un subset spécifique du service “reviews”.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
En rechargeant plusieurs fois la page, on peut voir que les “Reviews” sont toujours les mêmes (pas de ratings). C’est normal, le VirtualService redirige le trafic vers la version 1 de “reviews”.
Allons un peu plus loin et rajoutons les versions 2 et 3 de “reviews” dans le VirtualService avec une nouvelle notion : le poids.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v1
weight: 15
- destination:
host: reviews
subset: v2
weight: 25
- destination:
host: reviews
subset: v3
weight: 60
Globalement, le trafic va être réparti de la manière suivante :
- 15% des requêtes vont être redirigées vers la version 1,
- 25% vers la version 2,
- 60% vers la version 3.
Pour vérifier que le trafic est bien réparti, on peut générer des requêtes en boucle et inspecter les résultats directement depuis Kiali.
while true; do curl $INGRESS_HOST:$INGRESS_PORT/productpage -s -I ; done
Maintenant, on peut voir que le trafic est bien réparti entre les différentes versions de"reviews" : la version 3 est la plus utilisée (60% des requêtes) tandis que la version 1 est la moins utilisée (15% des requêtes).
On peut comparer ça à un déploiement canari (ou “canary deployment”) où une nouvelle version d’une application est déployée et testée sur une petite partie des utilisateurs avant l’être pour tout le monde. Nous pouvons alors rediriger le trafic vers la nouvelle version petit à petit et contrôler le taux d’adoption de cette dernière en fonction des logs et des métriques (grâce à Kiali, Jaeger et Grafana).
Dans notre déploiement canari, nous redirigeons le trafic de manière aléatoire vers les différentes versions du service “reviews”, mais pourquoi ne pas le rediriger en fonction d’autres critères ?
A/B Testing
L’A/B Testing est une technique qui consiste à rediriger le trafic vers différentes versions d’une application en fonction de certains critères (par exemple, le pays de l’utilisateur, le type d’appareil, etc). C’est extrêmement utile pour tester une nouvelle version d’une application sur un groupe d’utilisateurs spécifique sans impacter les autres.
Pour exemple, on va rediriger le trafic vers la version 3 du service “reviews” à l’utilisateur “quentin” et vers la version 2 pour les autres utilisateurs. À savoir que lorsque nous nous connectons à l’application bookinfo, celle-ci va créer une session cookie qui va contenir le nom de l’utilisateur. L’application productpage va transmettre ce nom d’utilisateur aux autres applications (reviews, details) via un header “end-user”. C’est ce header que nous allons utiliser pour rediriger le trafic.
Si vous voulez vérifier ça par vous-même, je vous invite à consulter la fonction getForwardHeaders()
de l’application Productpage ici.
Commençons par nous authentifier avec le nom d’utilisateur “quentin” (le mot de passe n’a pas d’importance, mettez n’importe quoi) :
Dans l’entête de la page, on peut voir avec quel nom d’utilisateur nous sommes connectés :
Modifions ensuite le VirtualService “reviews” comme ceci :
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
end-user:
exact: quentin
route:
- destination:
host: reviews
subset: v3
- route:
- destination:
host: reviews
subset: v2
On peut traduire cette configuration comme ceci :
- Si le header “end-user” est égal à “quentin”, redirige le traffic vers la version 3 de “reviews”.
- Sinon, redirige le traffic vers la version 2.
Sans s’autentifier, on peut voir que le trafic est redirigé vers la version 2 :
Et si on s’authentifie avec le nom d’utilisateur “quentin”, le trafic est redirigé vers la version 3 :
Depuis un autre utilisateur (par exemple “alice”), le trafic est redirigé vers la version 2.
Testons maintenant un autre cas de A/B Testing : rediriger le trafic vers une version spécifique en fonction de si l’utilisateur ouvre la page depuis un appareil mobile ou un ordinateur.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- match:
- headers:
user-agent:
regex: .*Mobile.*
route:
- destination:
host: reviews
subset: v3
- route:
- destination:
host: reviews
subset: v2
- Si je ne précise pas d’user-agent, le trafic est redirigé vers la version 2.
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -s | grep reviews-
<u>reviews-v2-5b65c4bdb-76pd4</u>
- Si je précise un user-agent contenant “Mobile”, le traffic est redirigé vers la version 3.
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage --user-agent "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.110 Mobile Safari/537.36" -s | grep reviews-
<u>reviews-v3-685dd59d69-tmzlf</u>
Voyons maintenant la dernière fonctionnalité d’Istio pour l’intégration des nouvelles versions d’une application : le “Dark Launch”.
Dark Launch (Mirroring)
Le Dark Launch est une technique permettant de tester une nouvelle version d’une application en parallèle de l’ancienne en ne renvoyant pas la réponse de la nouvelle version à l’utilisateur.
Ainsi, lorsqu’un utilisateur ouvre une page, le traffic est redirigé vers la version actuelle de l’application, mais la nouvelle est également appelée en parallèle. La réponse de la nouvelle version n’est pas renvoyée à l’utilisateur, mais un administrateur peut visionner les logs pour voir si l’application s’intègre correctement en vue d’un déploiement futur.
Admettons que nous soyons en train de passer de la version 2 à la version 3 du service “reviews”, on peut configurer un VirtualService pour passer en mode “mirroring”. Ce qui permettra de vérifier que l’application version 3 fonctionne correctement avec les requêtes actuelles.
Bien évidemment, les performances de l’application “ratings” vont être impactées par le mode “mirroring” puisque chaque requête va être appelée deux fois (une fois pour la version 2 et une fois pour la version 3).
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
spec:
hosts:
- reviews
http:
- route:
- destination:
host: reviews
subset: v2
mirror:
host: reviews
subset: v3
Voici le schéma de l’architecture à partir de Kiali :
Dès lors que le mode “mirroring” est activé, nous devons analyser les logs pour voir si la nouvelle version de l’application fonctionne correctement. Pour cela, nous pouvons utiliser les outils de tracing (Jaeger, Zipkin) et les métriques (Prometheus, Grafana).
Fault Injection
Maintenant que nous sommes capables de déployer des nouvelles versions avec ceinture et bretelles, il est temps de voir comment Istio peut nous aider à tester la résilience de nos applications.
Il existe une fonctionnalité qui permet d’injecter des erreurs dans les requêtes pour voir comment l’application réagit en cas d’erreur. C’est la “Fault Injection”.
Attaquons-nous au service “ratings” de l’application Bookinfo. On va injecter des erreurs 403 (Forbidden) sur 30% des requêtes pour voir comment l’application réagit et comment les utilisateurs sont impactés.
kind: VirtualService
apiVersion: networking.istio.io/v1beta1
metadata:
name: ratings
namespace: default
spec:
hosts:
- ratings
http:
- fault:
abort:
httpStatus: 403
percentage:
value: 30
route:
- destination:
host: ratings
Information
Pour cibler dans quels cas l’erreur 403 sera être injectée, on peut utiliser le champ match
pour spécifier les conditions. Ici, l’erreur 403 ne sera injectée que si le header “end-user” est égal à “testing-user”.
kind: VirtualService
apiVersion: networking.istio.io/v1beta1
metadata:
name: ratings
namespace: default
spec:
hosts:
- ratings
http:
- fault:
abort:
httpStatus: 403
percentage:
value: 30
route:
- destination:
host: ratings
match:
- headers:
end-user:
exact: testing-user
- route:
- destination:
host: ratings
Avec la configuration ci-dessus, 30% des requêtes vers le service “ratings” vont renvoyer une erreur 403 :
Depuis Kiali, les requêtes sont jetées vers un “Black Hole” (un trou noir).
Delay Injection
Plutôt que d’injecter des erreurs, on peut ajouter des délais dans les requêtes pour voir comment l’application réagit en cas de latence.
Je vais laisser tranquille le service “ratings” et m’attaquer au service “details” en injectant un délai de 7 secondes sur 50% des requêtes.
kind: VirtualService
apiVersion: networking.istio.io/v1beta1
metadata:
name: details
namespace: default
spec:
hosts:
- details
http:
- route:
- destination:
host: details
fault:
delay:
fixedDelay: 7.000s
percent: 50
Et voilà, 50% des requêtes vers le service “details” vont subir un délai de 7 secondes. Vérifions ça :
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -s -w '\n* Response time: %{time_total}s\n' | grep 'Response time'
* Response time: 3.262570s
Nous n’avons pas eu 7 secondes de délai, pourquoi ? Parce que le délai est injecté entre les services “productpage” et “details” et que notre application front a un timeout de 3 secondes pour les requêtes vers le service “details”.
Passons alors de 7 secondes à 2 secondes pour voir si le délai est bien pris en compte.
kubectl patch virtualservice details -n default --type='json' -p='[{"op": "replace", "path": "/spec/http/0/fault/delay/fixedDelay", "value": "2.000s"}]'
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -s -w '\n* Response time: %{time_total}s\n' | grep 'Response time'
* Response time: 0.282223s
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -s -w '\n* Response time: %{time_total}s\n' | grep 'Response time'
* Response time: 2.264027s
Et voilà, le délai de 2 secondes est bien constaté 🎉 !
Circuit Breaker
Un Circuit Breaker est un mécanisme qui permet de stopper les requêtes vers un service si un certain nombre d’erreurs sont rencontrées. Cela permet de protéger les services en aval d’une surcharge de requêtes et de réduire les temps de latence (en évitant les timeouts puisque les requêtes sont stoppées avant de l’atteindre).
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: productpage
spec:
host: productpage
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1 # Maximum number of connections in http 1.1
http:
http2MaxRequests: 3 # Maximum number of connections in http 2
maxRequestsPerConnection: 1 # Maximum number of requests per connection
Avec cette configuration, si le service “productpage” reçoit plus de 3 requêtes en HTTP/2, le Circuit Breaker va stopper celles en cours et renvoyer une erreur 503 (Service Unavailable). Ainsi, si le serveur reçoit trop de requêtes, le service ne va pas s’effondrer et traiter les requêtes qu’il peut traiter sans impacter les autres applications.
Pour tester ça, on peut utiliser un outil de benchmarking comme fortio
pour envoyer un grand nombre de requêtes vers le “productpage”. (On aura l’occasion de reparler de fortio
un peu plus tard.)
Si on lance avec un accès à la fois :
$ fortio load -c 1 -n 50 http://$INGRESS_HOST:$INGRESS_PORT/productpage
Connection time histogram (s) : count 1 avg 0.13487925 +/- 0 min 0.134879246 max 0.134879246 sum 0.134879246
# range, mid point, percentile, count
>= 0.134879 <= 0.134879 , 0.134879 , 100.00, 1
# target 50% 0.134879
# target 75% 0.134879
# target 90% 0.134879
# target 99% 0.134879
# target 99.9% 0.134879
Sockets used: 1 (for perfect keepalive, would be 1)
Uniform: false, Jitter: false, Catchup allowed: true
IP addresses distribution:
192.168.128.30:30492: 1
Code 200 : 50 (100.0 %)
Response Header Sizes : count 50 avg 188 +/- 0 min 188 max 188 sum 9400
Response Body/Total Sizes : count 50 avg 5399.12 +/- 271 min 4480 max 5481 sum 269956
All done 50 calls (plus 0 warmup) 162.183 ms avg, 6.2 qps
100% des requêtes sont passées avec succès. Maintenant, lançons le même test avec 4 requêtes en concurence :
$ fortio load -c 4 -n 50 http://$INGRESS_HOST:$INGRESS_PORT/productpage
Connection time histogram (s) : count 7 avg 0.1462682 +/- 0.02803 min 0.106653371 max 0.176468802 sum 1.02387742
# range, mid point, percentile, count
>= 0.106653 <= 0.12 , 0.113327 , 28.57, 2
> 0.12 <= 0.14 , 0.13 , 42.86, 1
> 0.14 <= 0.16 , 0.15 , 57.14, 1
> 0.16 <= 0.176469 , 0.168234 , 100.00, 3
# target 50% 0.15
# target 75% 0.166862
# target 90% 0.172626
# target 99% 0.176085
# target 99.9% 0.17643
Sockets used: 7 (for perfect keepalive, would be 4)
Uniform: false, Jitter: false, Catchup allowed: true
IP addresses distribution:
192.168.128.30:30492: 7
Code 200 : 46 (92.0 %)
Code 503 : 4 (8.0 %)
Response Header Sizes : count 50 avg 172.96 +/- 51 min 0 max 188 sum 8648
Response Body/Total Sizes : count 50 avg 5000.3 +/- 1421 min 247 max 5481 sum 250015
All done 50 calls (plus 0 warmup) 195.516 ms avg, 7.0 qps
On peut voir que 8% des requêtes ont renvoyé une erreur 503, c’est le Circuit Breaker qui a stoppé ces requêtes pour éviter une surcharge du service “productpage”.
Sécurité dans Istio
On a beaucoup parlé de la gestion du trafic et des erreurs, mais qu’en est-il de la sécurité ? Istio propose un grand nombre de fonctionnalités pour sécuriser les échanges entre les services, de la gestion des certificats à l’authentification des services.
Je vous propose qu’on se penche sur cet aspect pour voir comment Istio peut nous aider à sécuriser nos applications.
mTLS
J’en ai rapidement parlé au début de cet article, Istio supporte par défaut le mTLS (mutual TLS) pour sécuriser les échanges entre les services.
Chaque fois qu’un Envoy va communiquer avec un nouveau service, il va requêter Istiod pour obtenir un certificat se chargeant d’authentifier les échanges Ainsi, dû à la nature du mTLS, l’expéditeur ET le destinataire vont pouvoir s’authentifier mutuellement.
Par défault, le mTLS est activé en mode “PERMISSIVE” dans Istio. Cela signifie que les services peuvent communiquer en HTTP ou en HTTPS.
Nous pouvons forcer les échanges en mode “STRICT” pour que les services ne puissent communiquer qu’en HTTPS.
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default-mtls
namespace: default
spec:
mtls:
mode: STRICT
Astuce
Plutôt que de forcer le mTLS pour le namespace default, on peut le faire à l’échelle du cluster entier en spécifiant le namespace istio-system
.
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default-mtls
namespace: istio-system
spec:
mtls:
mode: STRICT
Une fois le mode “STRICT” activé, on peut redémarrer les services pour que le mTLS soit pris en compte.
kubectl rollout restart deployment -n default details-v1 productpage-v1 ratings-v1 reviews-v1 reviews-v2 reviews-v3
Depuis Kiali, on peut voir que le mTLS est bien activé avec le symbole 🔒.
Pour tester ça par nous-même, exposons le service “productpage” en dehors du cluster dans un NodePort (un service ClusterIP n’est pas suffisant).
kubectl patch svc productpage -n default --type='json' -p='[{"op": "replace", "path": "/spec/type", "value": "NodePort"}]'
PRODUCTPAGE_PORT=$(kubectl get svc productpage -n default -o jsonpath='{.spec.ports[0].nodePort}')
PRODUCTPAGE_HOST=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
echo $PRODUCTPAGE_HOST:$PRODUCTPAGE_PORT # 192.168.128.27:31447
Maintenant, essayons de visionner le certificat de “productpage” :
$ openssl s_client -connect $PRODUCTPAGE_HOST:$PRODUCTPAGE_PORT < /dev/null 2>/dev/null | openssl x509 -noout -text
Certificate:
Data:
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = cluster.local
Validity
Not Before: Jun 22 06:20:33 2024 GMT
Not After : Jun 23 06:22:33 2024 GMT
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
E5:BD:7C:B1:C3:CE:56:30:B1:9F:59:BE:97:E5:76:BD:6C:7B:D3:02
X509v3 Subject Alternative Name: critical
URI:spiffe://cluster.local/ns/default/sa/bookinfo-productpage
Signature Algorithm: sha256WithRSAEncryption
On peut voir que le certificat est bien signé par le cluster local et est de durée limitée (1 jour).
Sans un certificat valide, on ne peut pas communiquer avec le service “productpage” :
$ curl $PRODUCTPAGE_HOST:$PRODUCTPAGE_PORT -v
* Recv failure: Connexion ré-initialisée par le correspondant
* Closing connection 0
curl: (56) Recv failure: Connexion ré-initialisée par le correspondant
Les ACLs
Passons maintenant à la création d’ACL (Access Control List) pour autoriser ou refuser l’accès à certains services en fonction de certains critères.
Globalement, les ACLs peuvent se baser sur plusieurs critères :
- Le namespace (entrée, sortie);
- l’opération (GET, POST, PUT, DELETE) et son chemin ;
- les labels des services.
L’idée est d’autoriser une certaine application à exposer tel ou tel chemin ou opération HTTP et d’en refuser l’accès si les conditions ne sont pas remplies.
Pour voir cela en action, on va commencer par interdire tous les échanges dans notre namespace “default”.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: allow-nothing
namespace: default
spec: {}
Maintenant, si on essaie d’accéder à la page “productpage”, on va recevoir une erreur 403 (Forbidden) :
curl $INGRESS_HOST:$INGRESS_PORT/productpage -v
* Trying 192.168.128.30:30492...
* Connected to 192.168.128.30 (192.168.128.30) port 30492 (#0)
> GET /productpage HTTP/1.1
< HTTP/1.1 403 Forbidden
< server: istio-envoy
< x-envoy-upstream-service-time: 0
<
* Connection #0 to host 192.168.128.30 left intact
Kiali nous indique que le trafic est bien bloqué :
Jaeger aussi :
Pour ré-autoriser l’accès à la page “productpage”, on peut créer une nouvelle règle d’ACL autorisant les différentes opérations sur le service “productpage” :
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
namespace: default
name: allow-productpage
spec:
selector:
matchLabels:
app: productpage
action: ALLOW
rules:
- to:
- operation:
methods: ["GET"]
paths:
- "/logout"
- "/static/*"
- "/productpage"
- "/api/v1/products"
- operation:
methods: ["POST"]
paths: ["/login"]
Cool, on peut de nouveau accéder à la page “productpage” :
En revanche, les autres services sont toujours bloqués. Pas le choix, on va devoir créer des règles d’ACL pour chacun d’entre eux.
Alors, profitons-en pour les authentifier afin d’éviter que n’importe quelle application puisse communiquer avec des conteneurs dont elle n’a pas besoin.
Authentification par SA
Passons maintenant à l’authentification des services. L’objectif est d’autoriser un service à communiquer avec un autre en utilisant un ServiceAccount comme clé d’authentification. De ce fait, seul ceux ayant le bon ServiceAccount pourront communiquer avec ceux autorisés.
En l’occurence, voici les règles que nous allons mettre en place :
- “details” doit être joignable par “productpage” ;
- “ratings” doit être joignable par “reviews” ;
- “reviews” doit être joignable par “productpage”.
L’application “bookinfo” a prévu des ServiceAccounts pour chaque service. Voici ceux disponibles :
$ kubectl get sa
NAME SECRETS AGE
bookinfo-details 0 3d19h
bookinfo-productpage 0 3d19h
bookinfo-ratings 0 3d19h
bookinfo-reviews 0 3d19h
default 0 4d11h
Commençons par autoriser “details” à être joignable par “productpage”.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
namespace: default
name: allow-details
spec:
selector:
matchLabels:
app: details
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/bookinfo-productpage"]
to:
- operation:
methods: ["GET"]
paths: ["/details/*"]
Depuis ProductPage, les informations de “details” sont bien affichées :
Mais si j’essaie d’accéder à “details” depuis “ratings”, j’ai une erreur 403 (Forbidden) :
$ kubectl exec deployments/ratings-v1 -c ratings -- curl -s http://details:9080/details/0
RBAC: access denied
Maintenant, passons aux autres services. Voici les règles d’ACL pour autoriser “ratings” et “reviews” :
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
namespace: default
name: allow-reviews
spec:
selector:
matchLabels:
app: reviews
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/bookinfo-productpage"]
to:
- operation:
methods: ["GET"]
paths: ["/reviews/*"]
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
namespace: default
name: allow-ratings
spec:
selector:
matchLabels:
app: ratings
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/bookinfo-reviews"]
to:
- operation:
methods: ["GET"]
paths: ["/ratings/*"]
Après avoir appliqué ces règles, on peut voir que “ratings” et “reviews” sont de nouveau accessibles depuis “productpage” :
Authentification par JWT
Maintenant que nous avons vu comment authentifier les services avec des ServiceAccounts, voyons comment nous pouvons authentifier les requêtes avec des JWT (JSON Web Tokens). Cette méthode est un peu plus complexe et nécessite que les requêtes des applications soient adaptées pour envoyer un JWT (Envoy ne va pas le faire pour nous).
Information
JWT, ou JSON Web Token, est un standard ouvert pour créer des tokens d’accès qui permettent la sécurisation des échanges d’informations entre parties. Un JWT contient des informations (appelées “claims”) sur l’utilisateur, encodées en JSON. Elles sont signées numériquement, généralement à l’aide d’une clé privée, pour garantir leur authenticité.
Dans cette partie, je vais fortement m’inspirer du dépôt Github d’InfraCloud sans lequel je n’aurais pas été capable d’implémenter les JWTs dans Istio. Merci à eux pour leur travail !
On va commencer par générer une clé privée et une publique pour signer les JWTs. Pour cela, on va utiliser OpenSSL afin de générer une clé RSA. Un secret sera demandé pour protéger la clé privée.
openssl genrsa -aes256 -out private_encrypted.pem 4096
openssl rsa -pubout -in private_encrypted.pem -out public.pem
openssl rsa -in private_encrypted.pem -out private.pem -outform PEM
Nous obtenons trois fichiers : private.pem
, public.pem
et private_encrypted.pem
.
Maintenant, on va générer une clé JWT en utilisant private.pem
:
# generatekey.py
from authlib.jose import jwt
import os
JWT_ISSUER=os.getenv('JWT_ISSUER') # ex: [email protected]
JWT_EXPIRATION=int(os.getenv('JWT_EXPIRATION')) # ex: 1685505001
header = {'alg': 'RS256'}
payload = {'iss': JWT_ISSUER, 'sub': 'admin', 'exp': JWT_EXPIRATION}
private_key = open('private.pem', 'r').read() #Provide the path to your private key
bytes = jwt.encode(header, payload, private_key)
print(bytes.decode('utf-8'))
export JWT_EXPIRATION=1782191719000
export JWT_ISSUER="[email protected]"
export JWT_TOKEN=$(python3 generatekey.py)
Pour la variable JWT_EXPIRATION, vous pouvez utiliser le site Epoch Converter afin de convertir une date en timestamp.
Vérifions maintenant que la clé publique permet bien de valider le JWT :
# validatekey.py
import os
JWT_TOKEN=os.getenv('JWT_TOKEN')
from authlib.jose import jwt
public_key = open('public.pem', 'r').read() #Provide path to your public key
claims = jwt.decode(JWT_TOKEN, public_key)
claims.validate()
print(claims)
$ python3 validatetoken.py
{'iss': '[email protected]', 'sub': 'admin', 'exp': 1782191719000}
Notre token est bien reconnu et valide (et heureusement car c’est ce même mécanisme qui va être utilisé par Istio pour authentifier les requêtes).
Générons maintenant une clé JWK (JSON Web Key) à partir de notre clé publique pour la configurer dans Istio.
from authlib.jose import jwk
public_key = open('public.pem', 'r').read() #Provide path to your public key
key = jwk.dumps(public_key, kty='RSA')
print(key)
$ python3 generatejwk.py
{'n': 'rPbn21rfrOrjq5AZ4W6XMjfpUu0SMIAIY9zj6skWWRMEYJn4Jvj6v3olLgMd0JjJluPXxgBYalIL2Fv9mKnZIyFcaCWDkTKBj1xN9k4PN-g5pPSGtYEYHT-zfdBfH-8inea8c9XoQGwyqm7TEwmI4M43WsBoqsItBcB_rLTo8DLlRf0mzlbTeK-M0iEC8-Osfj2FV9vtHR_FdsWaLK5QN-c8aJZIAZQ_S81EvRzVYguJ2-3l05JNI0GGNdGwawvp4cXmvIlCGEuZ5fdNJTjd3pcEJqMR8Gzyd_kb32SiHDXvTdI48KHPo_EjUf_i1maufxJToqEBOPwjEdpg1D1BPQ', 'e': 'AQAB', 'kty': 'RSA'}
On donne le json généré par generatejwk.py
à Istio pour qu’il puisse valider les JWTs dans les requêtes vers “productpage”.
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: productpage-jwt
namespace: default
spec:
selector:
matchLabels:
app: productpage
jwtRules:
- forwardOriginalToken: true
issuer: [email protected]
jwks: |
{"keys": [{"n": "rPbn21rfrOrjq5AZ4W6XMjfpUu0SMIAIY9zj6skWWRMEYJn4Jvj6v3olLgMd0JjJluPXxgBYalIL2Fv9mKnZIyFcaCWDkTKBj1xN9k4PN-g5pPSGtYEYHT-zfdBfH-8inea8c9XoQGwyqm7TEwmI4M43WsBoqsItBcB_rLTo8DLlRf0mzlbTeK-M0iEC8-Osfj2FV9vtHR_FdsWaLK5QN-c8aJZIAZQ_S81EvRzVYguJ2-3l05JNI0GGNdGwawvp4cXmvIlCGEuZ5fdNJTjd3pcEJqMR8Gzyd_kb32SiHDXvTdI48KHPo_EjUf_i1maufxJToqEBOPwjEdpg1D1BPQ", "e": "AQAB", "kty": "RSA"}]}
On peut maintenant appliquer une règle d’ACL pour autoriser le trafic sur la page “productpage” uniquement si le JWT est signé par l’issuer “[email protected]”.
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: productpage-jwt
namespace: default
spec:
selector:
matchLabels:
app: productpage
action: ALLOW
rules:
- when:
- key: request.auth.claims[iss]
values: ["[email protected]"]
Maintenant que tout est en place, supprimons la règle ACL créée à l’étape précédente (celle qui autorisait tout le trafic sur la page “productpage”) :
kubectl delete authorizationpolicies.security.istio.io allow-productpage
Dans cette configuration, j’autorise le trafic sur la page “productpage” uniquement si le JWT est signé par l’issuer “[email protected]
”. Essayons de faire une requête pour vérifier ça :
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -s -I
HTTP/1.1 403 Forbidden
server: istio-envoy
date: Sun, 23 Jun 2024 06:13:26 GMT
x-envoy-upstream-service-time: 1
Testons maintenant avec un JWT valide (pour rappel, j’ai généré le token à partir de la commande export JWT_TOKEN=$(python3 generatekey.py)
).
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage --header "Authorization: Bearer $JWT_TOKEN" -s -I
HTTP/1.1 200 OK
server: istio-envoy
date: Sun, 23 Jun 2024 06:16:08 GMT
x-envoy-upstream-service-time: 17
Puis avec un JWT invalide :
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage --header "Authorization: Bearer a-cup${JWT_TOKEN}of-coffee" -s -I
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="http://192.168.128.30:30492/productpage", error="invalid_token"
content-length: 42
content-type: text/plain
date: Sun, 23 Jun 2024 06:18:14 GMT
server: istio-envoy
x-envoy-upstream-service-time: 5
Gérer les accès externes
Il est possible de demander à Envoy de gérer des services externes (c’est-à-dire des services qui ne sont pas dans le mesh Istio). Cela peut être utile pour gérer les communications vers des services tiers en profitant des fonctionnalités d’Istio (retries, observabilité, gestion bande passante, etc).
Pour cela, on peut utiliser un ServiceEntry pour déclarer un service externe. En voici un exemple :
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: coffee-website
spec:
hosts:
- a-cup-of.coffee
- une-tasse-de.cafe
location: MESH_EXTERNAL
ports:
- number: 443
name: https
protocol: TLS
resolution: DNS
Pour nos tests, je vais déclarer un pod qui va nous permettre de faire des requêtes vers des services externes.
apiVersion: v1
kind: Pod
metadata:
name: debug-network
spec:
containers:
- name: debug
image: digitalocean/doks-debug:latest
command: [ "sleep", "infinity" ]
Je vais générer quelques requêtes via ce pod pour voir comment Istio gère les requêtes vers un service enregistré.
while true; do kubectl exec pods/debug-network -c debug exec -- curl https://a-cup-of.coffee ; done
On peut voir qu’Istio reconnait bien le ServiceEntry.
Tentons maintenant une requête vers un service non déclaré dans un ServiceEntry :
kubectl exec pods/debug-network -c debug exec -- curl https://perdu.com
Le trafic est envoyé dans le “PassThroughCluster” (qui indique que les requêtes sont gérées normalement comme un pod classique (Envoy ne traite pas la requête)).
Voyons maintenant le cas où je souhaite limiter le trafic sortant de mon cluster. Pour cela, je vais modifier les paramètres de mon mesh Istio pour bloquer tout le trafic sortant sauf celui qui est déclaré dans un ServiceEntry.
istioctl upgrade --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY
Dès lors, si je refais la requête vers perdu.com
, je vais obtenir une erreur :
$ kubectl exec pods/debug-network -c debug exec -- curl https://perdu.com/ -v
ù* Recv failure: Connection reset by peer
* OpenSSL SSL_connect: Connection reset by peer in connection to perdu.com:443
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
* Closing connection 0
curl: (35) Recv failure: Connection reset by peer
command terminated with exit code 35
En revanche, si je fais une requête vers a-cup-of.coffee
, la requête est bien envoyée :
$ kubectl exec pods/debug-network -c debug exec -- curl https://a-cup-of.coffee -I -s
HTTP/2 200
accept-ranges: bytes
content-type: text/html
server: lighttpd/1.4.71
Remarque
Pour revenir au mode par défaut, il suffit de passer la variable meshConfig.outboundTrafficPolicy.mode
à ALLOW_ANY
.
istioctl upgrade --set meshConfig.outboundTrafficPolicy.mode=ALLOW_ANY
À savoir que le mode REGISTRY_ONLY
autorise le trafic sur tous les services déclarés dans un ServiceEntry pour chaque pod. Je n’ai pas encore trouvé comment limiter le trafic sortant à une seule application. Par exemple, limiter le trafic sortant de “productpage” à “a-cup-of.coffee” et limiter celui sortant de “reviews” à “une-tasse-de.cafe”.
Marre des sidecars ?
Istio propose une fonctionnalité appelée “Ambient” qui permet de ne pas déployer de sidecar dans chaque pod. Dans ce mode, Istio va agir comme un CNI (Container Network Interface) et va intercepter le trafic réseau entrant et sortant des pods pour appliquer les règles de sécurité.
L’objectif premier de ce mode est de réduire la consommation de ressources (CPU, mémoire) en évitant de déployer un sidecar dans chaque pod ainsi que d’accroître la performance du mesh Istio. Nous allons voir si c’est vraiement le cas dans une partie dédiée à ça.
Dès lors, en quoi le mode “Ambient” peut-il améliorer la performance du mesh Istio, me direz-vous ? Et bien, en réduisant le nombre de sidecars et donc le nombre de traitements L7 à chaque requête.
Instead the biggest culprit is the intensive L7 processing Istio needs to implement its sophisticated feature set. Unlike sidecars, which implement two L7 processing steps for each connection (one for each sidecar), ambient mesh collapses these two steps into one. In most cases, we expect this reduced processing cost to compensate for an additional network hop. source
Pour activer le mode “Ambient”, on doit installer Istio avec le profil ambient
et ajouter les Gateways API (un projet Kubernetes officiel visant à remplacer les Ingress par de nouveaux objets. Si le sujet vous intéresse, vous pouvez consulter la documentation ici).
istioctl install --set profile=ambient --skip-confirmation
kubectl get crd gateways.gateway.networking.k8s.io &> /dev/null || \
{ kubectl kustomize "github.com/kubernetes-sigs/gateway-api/config/crd/experimental?ref=v1.1.0" | kubectl apply -f -; }
Ambient utilise un nouveau label pour les namespaces : istio.io/dataplane-mode=ambient
. On peut l’appliquer sur le namespace “coffee” pour activer le mode “Ambient” et retirer le label istio-injection
pour désactiver l’injection automatique des sidecars.
kubectl label namespace coffee istio.io/dataplane-mode=ambient
kubectl label namespace default istio-injection-
kubectl rollout restart deployment -n default details-v1 productpage-v1 ratings-v1 reviews-v1 reviews-v2 reviews-v3
Maintenant, plus besoin du service “istio-ingressgateway” pour gérer le trafic entrant, on peut directement passer par l’objet Gateway..
Pour cela, on applique la configuration suivante :
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: bookinfo-gateway
spec:
gatewayClassName: istio
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: bookinfo
spec:
parentRefs:
- name: bookinfo-gateway
rules:
- matches:
- path:
type: Exact
value: /productpage
- path:
type: PathPrefix
value: /static
- path:
type: Exact
value: /login
- path:
type: Exact
value: /logout
- path:
type: PathPrefix
value: /api/v1/products
backendRefs:
- name: productpage
port: 9080
Un pod “gateway” est bien déployé :
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
bookinfo-gateway-istio-7c755f6876-t59dn 1/1 Running 0 14s
details-v1-cf74bb974-ph5dd 1/1 Running 0 50s
productpage-v1-87d54dd59-nwxgd 1/1 Running 0 49s
ratings-v1-7c4bbf97db-sq475 1/1 Running 0 50s
reviews-v1-5fd6d4f8f8-2r4dz 1/1 Running 0 50s
reviews-v2-6f9b55c5db-4fkwb 1/1 Running 0 50s
reviews-v3-7d99fd7978-nbbdr 1/1 Running 0 49s
N’ayant pas de LoadBalancer, je vais exposer le service “bookinfo-gateway” en NodePort pour pouvoir accéder à l’application depuis l’extérieur.
$ kubectl annotate gateway bookinfo-gateway networking.istio.io/service-type=NodePort --namespace=default
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bookinfo-gateway-istio NodePort 10.98.171.168 <none> 15021:31677/TCP,80:31334/TCP 17s
details ClusterIP 10.105.80.56 <none> 9080/TCP 51s
productpage ClusterIP 10.109.221.79 <none> 9080/TCP 51s
ratings ClusterIP 10.102.161.80 <none> 9080/TCP 51s
reviews ClusterIP 10.100.182.23 <none> 9080/TCP 51s
On utilise les commandes suivantes afin de trouver le port du service de notre gateway :
export INGRESS_PORT=$(kubectl get service bookinfo-gateway-istio -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
export INGRESS_HOST=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
$ curl $INGRESS_HOST:$INGRESS_PORT/productpage -I
HTTP/1.1 200 OK
server: istio-envoy
date: Tue, 25 Jun 2024 20:55:31 GMT
content-type: text/html; charset=utf-8
content-length: 4294
vary: Cookie
x-envoy-upstream-service-time: 28
Dans ce cas, le trafic est uniquement géré en L4 (et non plus en L7 comme c’était le cas avec les sidecars). Cela permet de réduire la charge sur les pods et d’améliorer la performance du mesh Istio. En contrepartie, on perd certaines fonctionnalités comme la gestion du trafic HTTP (retries, circuit breaker, etc). Il est toutefois possible de palier à ce problème en utilisant des Gateway (ou “Waypoint” dans le langage Istio) pour gérer le trafic HTTP.
Je continuerai à explorer le mode “Ambient” dans un prochain article pour éclaircir ces zones d’ombres.
Benchmark de performance
Enfin, je vais faire un benchmark de performance afin de voir comment Istio impacte les performances de mon cluster Kubernetes. Dans ce but, nous allons comparer deux méthodes de communication (HTTP et TCP) sur trois scénarios différents :
- Sans Istio;
- avec Istio;
- avec Istio en mode “Ambient”.
Pour cela, je vais utiliser Fortio qui est un outil de benchmarking pour les services HTTP et TCP développé par Istio.
Le CNI utilisé dans mon cluster est Flannel (j’ai eu des problèmes entre Cilium et Istio Ambient). Mais j’ai quand même réalisé un test de performance avec Cilium pour vous donner une idée de la bande passante entre mes pods.
🔥 Network Performance Test Summary [cilium-test]:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
📋 Scenario | Node | Test | Duration | Min | Mean | Max | P50 | P90 | P99 | Transaction rate OP/s
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
📋 pod-to-pod | same-node | TCP_RR | 10s | 27µs | 75.32µs | 8.115ms | 69µs | 108µs | 211µs | 13149.99
📋 pod-to-pod | same-node | UDP_RR | 10s | 29µs | 81.06µs | 23.993ms | 67µs | 113µs | 308µs | 12222.58
📋 pod-to-pod | same-node | TCP_CRR | 10s | 143µs | 320.87µs | 14.373ms | 284µs | 411µs | 1.068ms | 3106.70
📋 pod-to-pod | other-node | TCP_RR | 10s | 129µs | 298.52µs | 14.168ms | 245µs | 395µs | 1.197ms | 3340.77
📋 pod-to-pod | other-node | UDP_RR | 10s | 147µs | 382.21µs | 37.771ms | 309µs | 573µs | 1.534ms | 2609.31
📋 pod-to-pod | other-node | TCP_CRR | 10s | 440µs | 1.21346ms | 17.531ms | 1.061ms | 1.797ms | 4.255ms | 823.03
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------
📋 Scenario | Node | Test | Duration | Throughput Mb/s
-------------------------------------------------------------------------------------
📋 pod-to-pod | same-node | TCP_STREAM | 10s | 610.56
📋 pod-to-pod | same-node | UDP_STREAM | 10s | 272.15
📋 pod-to-pod | other-node | TCP_STREAM | 10s | 1506.68
📋 pod-to-pod | other-node | UDP_STREAM | 10s | 209.39
-------------------------------------------------------------------------------------
Maintenant que nous avons une idée des performances maximales atteignables, nous allons commencer notre benchmark.
Il va se composer de deux parties :
- Une partie HTTP avec Fortio pour tester la latence ;
- une partie TCP avec Iperf pour tester la bande passante.
Petit disclaimer : Les résultats que je vais obtenir ne seront pas forcément les mêmes que les vôtres. Les performances peuvent varier en fonction de la configuration de votre cluster, de la charge sur celui-ci, de la configuration de votre application, etc. Les résultats que je vais obtenir ne sont pas forcément représentatifs de la réalité. Ils sont là pour donner une idée de la performance d’Istio dans un cluster Kubernetes pour un cas d’utilisation donné.
Si vous souhaitez lire un Benchmark un peu plus complet, je vous invite à consulter ce dépôt Github proposant des résultats très complets et intéressants.
Benchmark HTTP
Comme je l’ai dit plus haut, je vais utiliser Fortio pour tester la latence de mes services. Pour cela, je vais déployer un pod Fortio dans mon cluster et faire des requêtes vers l’un des services de l’application “bookinfo” : “details”.
Pour installer Fortio, j’ai d’abord déployé l’opérateur Fortio avant d’y préférer un déploiement classique (KISS).
apiVersion: v1
kind: Service
metadata:
name: fortio-debug
spec:
ports:
- port: 8080
name: http-debug
selector:
app: fortio-debug
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: fortio-debug-deployment
spec:
replicas: 1
selector:
matchLabels:
app: fortio-debug
template:
metadata:
labels:
app: fortio-debug
spec:
containers:
- name: fortio-debug
image: fortio/fortio:latest_release
imagePullPolicy: Always
ports:
- containerPort: 8080
Pour lancer et configurer les tests, j’ai simplement fait un port-forward sur le pod Fortio :
kubectl port-forward svc/fortio-debug 8080:8080
La suite de la configuration se fait directement sur l’interface web de Fortio à l’adresse http://localhost:8080/fortio/
. J’ai choisi de faire des tests de latence avec 10 connexions simultanées faisant chacune 100 requêtes par seconde. Bien sûr, je me suis assuré que les tests étaient toujours entre deux noeuds différents.
Voici les résultats obtenus pour les trois scénarios :
Sans Istio
Avec Istio sidecar
Avec Istio Ambient
Ce qui est cool, c’est qu’on a des résultats très différents. En terme de latence, voici ce que ça donne :
- Istio Ambient : 2.35ms de latence;
- Sans Istio : 2.8ms de latence;
- Istio Sidecar : 39.3ms de latence.
Je note quand même qu’il y a eu des erreurs de connexion que je n’explique pas avec Istio Ambient (même en réalisant plusieurs tests).
C’est assez incroyable qu’Istio Ambient parvienne à réduire la latence par rapport à un cluster sans Istio (j’ai même vérifié plusieurs fois les résultats pour être sûr). Cela montre bien que le mode “Ambient” est une solution viable pouvant potentiellement améliorer les performances de votre cluster Kubernetes.
Benchmark TCP
Pour ce benchmark, je vais utiliser Iperf afin de tester la bande passante entre mes services. Je vais alors déployer un pod Iperf en tant que serveur et utiliser un pod “tcp-iperf-client” pour faire des requêtes vers le serveur Iperf.
Manifests IPerf
Client Iperf
apiVersion: v1
kind: Pod
metadata:
name: tcp-iperf-client
spec:
containers:
- name: debug
image: digitalocean/doks-debug:latest
command: [ "sleep", "infinity" ]
Serveur Iperf
apiVersion: apps/v1
kind: Deployment
metadata:
name: tcp-iperf
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: tcp-iperf
version: v1
template:
metadata:
labels:
app: tcp-iperf
version: v1
spec:
containers:
- args:
- -s
- --port
- "5201"
image: mlabbe/iperf
imagePullPolicy: IfNotPresent
name: tcp-iperf
ports:
- containerPort: 5201
name: tcp-app
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
labels:
app: tcp-iperf
service: tcp-iperf
name: tcp-iperf
namespace: default
spec:
ports:
- name: tcp-iperf
port: 5201
protocol: TCP
selector:
app: tcp-iperf
type: ClusterIP
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: tcp-iperf
spec:
hosts:
- "*"
gateways:
- tcp-iperf
tcp:
- match:
- port: 5201
route:
- destination:
host: tcp-iperf
port:
number: 5201
Je vais également forcer le mTLS pour être dans un cas d’utilisation plus réaliste.
# Without Istio (no sidecar, no ambient)
iperf -c tcp-iperf --port 5201
------------------------------------------------------------
Client connecting to tcp-iperf, TCP port 5201
TCP window size: 16.0 KByte (default)
------------------------------------------------------------
[ 1] local 10.244.2.4 port 56286 connected with 10.102.184.216 port 5201 (icwnd/mss/irtt=13/1398/585)
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-10.0207 sec 4.22 GBytes 3.62 Gbits/sec
# With Istio Sidecar
iperf -c tcp-iperf --port 5201
------------------------------------------------------------
Client connecting to tcp-iperf, TCP port 5201
TCP window size: 2.50 MByte (default)
------------------------------------------------------------
[ 1] local 10.244.2.215 port 41158 connected with 10.106.115.53 port 31400 (icwnd/mss/irtt=13/1398/34)
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-10.1227 sec 2.22 GBytes 1.88 Gbits/sec
# With Istio Ambient
iperf -c tcp-iperf --port 5201
------------------------------------------------------------
Client connecting to tcp-iperf, TCP port 5201
TCP window size: 2.50 MByte (default)
------------------------------------------------------------
[ 1] local 10.244.1.9 port 54814 connected with 10.110.166.223 port 5201 (icwnd/mss/irtt=13/1398/50)
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-10.0806 sec 2.48 GBytes 2.12 Gbits/sec
À l’inverse de la latence pour laquelle Istio Ambient était le plus performant, c’est le mode “sans Istio” qui bat à plate couture les deux autres modes en terme de bande passante.
- Sans Istio : 3.62 Gbits/sec;
- Istio Ambient : 2.12 Gbits/sec;
- Istio Sidecar : 1.88 Gbits/sec.
Conclusion
Istio est un produit incroyablement puissant et complet, mais il n’est pas sans défauts. Il est très facile de se perdre dans la configuration d’Istio et de se retrouver avec un mesh qui ne fonctionne pas comme prévu (de plus, les logs ne sont pas toujours très explicites). C’est pourquoi il est important de bien comprendre les concepts d’Istio avant de se lancer dans la configuration de son mesh.
Malgré le temps que j’ai passé à apprendre Istio, je ne me sens pas encore à l’aise pour un usage en production, il m reste encore beaucoup de choses à apprendre de cette solution. J’espère que la lecture de cet article sera utile pour ceux qui souhaitent se lancer dans l’apprentissage d’Istio.
Si vous souhaitez m’encourager dans la rédaction de ce genre d’article (et financer mes nuits blanches), n’hésitez pas à me faire un petit don sur ma page Kofi, vous pouvez également me faire un petit coucou sur les réseaux sociaux ci-dessous :
D’ici là, je vous souhaite une bonne journée et bon courage dans votre aventure Istio !