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.

Service Mesh avec Proxy

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 :

Service Mesh avec Proxy

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.

Test app

$ 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.

Index page

En cliquant sur le bouton “Normal user”, une page similaire à celle-ci devrait s’afficher :

Bookinfo sample

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.

Services on bookinfo

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.

alt text

  • Version 2 : des ratings avec des étoiles noires.

alt text

  • Version 3 : des ratings avec des étoiles rouges.

alt text

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).

Kiali b4 VS

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.

alt text

Prenons une trace du service “productpage” (point d’entrée de l’application Bookinfo) et voyons les détails de la trace :

alt text alt text

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.

alt text

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 :

alt text

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.

alt text

alt text

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.

alt text


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.

alt text

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.

Index page

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 :

alt text

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

alt text

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) :

alt text

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 : alt text

Et si on s’authentifie avec le nom d’utilisateur “quentin”, le trafic est redirigé vers la version 3 : 55139e6424e5d54bbd7ea49327ae4492.png

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.

Dark Launch

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 :

alt text

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 :

c17bb7d4cc0022c40e328bb3e704c959.png

Depuis Kiali, les requêtes sont jetées vers un “Black Hole” (un trou noir).

4db0624f7e92b4e0a251d8027287eb44.png

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.

Im

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.

Citadel

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 🔒.

alt text

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é :

alt text

Jaeger aussi :

alt text

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” :

alt text

a6e19e77fbeae1e5629f47e25c7b2044.png

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 :

alt text

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

alt text

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” :

alt text

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

alt text

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

854dc23b91c2fd92f6f75e6ea4bddb48.png

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

alt text

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.

alt text

Voici les résultats obtenus pour les trois scénarios :

Sans Istio

6f25496656b67355c3149d7b819b8def.png

Avec Istio sidecar

f1b75ffbb034f1aadbcedad63815c95a.png

Avec Istio Ambient

1141abfb3858c60718eefbe6e3125d98.png

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 :

  1. Istio Ambient : 2.35ms de latence;
  2. Sans Istio : 2.8ms de latence;
  3. 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.

MTLS Benchmark
# 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.

  1. Sans Istio : 3.62 Gbits/sec;
  2. Istio Ambient : 2.12 Gbits/sec;
  3. 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 !