Une approche de canary release via k8s

L’ingénierie de la release regroupe l’ensemble des méthodes et procédés visant à codifier la gestion des cycles de vie des différentes versions d’un applicatif. Cette approche existe depuis l'avènement de l’ère informatique, elle est aujourd’hui d’autant plus pertinente dans les contextes d'applications et services déployés à grande échelle.

Les principales étapes de l’ingénierie de la release incluent la planification du déploiement, les tests de validation, le suivi de la performance en production et la gestion des risques associés aux mises à jour. Pour adresser ces sujets, plusieurs stratégies de déploiement ont été développées. Le roll forward, le blue-green deployment et le canary release en sont des exemples.

Le roll forward consiste à livrer une nouvelle version à destination de tous les utilisateurs. Le blue-green deployment s’exécute en maintenant deux environnements en parallèle, l’un avec une version plus récente et l’autre avec une version stable. Le nouvel environnement est alors testé et validé avant de se voir recevoir du trafic utilisateur. Le canary release consiste à mettre à jour une fraction de l’infrastructure, puis à étendre la livraison si les métriques considérées sont satisfaisantes.

Les forces principales du canary release sont donc de :

  • Réduire les risques de baisse de SLOs en n’augmentant pas significativement les coûts d'infrastructure dus aux releases.
  • Baser le choix des utilisateurs impactés sur des caractères pouvant être divers (pays, types de comptes, …).
  • Augmenter graduellement la part de déploiement de la nouvelle version.
  • Diminuer le délai pour les premières boucles de feedback utilisateur.
  • Permettre un retour rapide à une version antérieure en cas de besoin.

Le but de cet article est de proposer une implémentation minimaliste de canary release via Kubernetes.

🔭Mise en pratique

Une mise en place simple et rapide dans k8s consiste à utiliser une annotation sur une ingress nginx. Considérons l’infrastructure initiale suivante :

Pour déployer en canary, on va alors créer une nouvelle ingress avec les annotations ”nginx.ingress.kubernetes.io/canary” et ”nginx.ingress.kubernetes.io/canary-weight”. Cette “canary-ingress” utilise le même host que l’ingress de l’infrastructure initiale. 

La différence est que la nouvelle ingress va rediriger le trafic vers une nouvelle infrastructure. On obtient alors la configuration :

⚒️ Maintenant que l’on sait où aller, passons à la pratique !

Tout d’abord les pré-requis !

Pour ma part j’utilise le cluster k8s local intégré à docker desktop.

Occupons-nous du HTML qui nous servira d’exemple. Nous allons utiliser une image nginx pour servir une page web. Voici le code HTML :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Canary Example</title>
    <style>
      :root {
        --base-color: COLOR_PLACEHOLDER;
      }

      body {
        background-color: var(--base-color);
        color: white;
        font-family: Arial, sans-serif;
        text-align: center;
        margin-top: 20%;
      }

      h1 {
        color: white;
      }
    </style>
  </head>
  <body>
    <h1>Just A Colorful Page</h1>
  </body>
</html>

Le dockerfile suivant permet de changer la couleur de la page HTML en la figeant au moment du build docker :

FROM nginx:alpine
# Default color
ARG BASE_COLOR=#3498db
COPY ./app/src/index.html /usr/share/nginx/html/index.html
RUN sed -i "s/COLOR_PLACEHOLDER/${BASE_COLOR}/g" /usr/share/nginx/html/index.html

Construisons maintenant ces deux images ! 🚀

docker build --build-arg BASE_COLOR=#f5c842 -t basek8s-canary:yellow -f app/docker/Dockerfile .
docker build --build-arg BASE_COLOR=#3498db -t basek8s-canary:blue -f app/docker/Dockerfile .

En démarrant les images directement via docker on obtient :
Capture d'écran 2024-11-14 113648.png

Fantastique ! Nous voici maintenant en possession de deux images docker qui sont deux versions différentes d’une même application.

Abordons maintenant la partie kubernetes. La première étape est de créer un namespace dédié et de le sélectionner avec kubens :

kubectl create namespace canary
kubens canary

Maintenant le code de l’infrastructure main.
Deployment :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: main-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: main-app
  template:
    metadata:
      labels:
        app: main-app
    spec:
      containers:
        - name: main-app
          image: basek8s-canary:blue
          ports:
            - containerPort: 80

Service :

apiVersion: v1
kind: Service
metadata:
  name: main-svc
  labels:
    app: main-app
spec:
  type: ClusterIP
  selector:
    app: main-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Ingress :

apiVersion: v1
kind: Service
metadata:
  name: main-svc
  labels:
    app: main-app
spec:
  type: ClusterIP
  selector:
    app: main-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Remarques : 

i- j’utilise ici nip.io pour servir mon localhost. 

ii- la version de l’application avec la couleur bleue est celle utilisée par défaut.

Déployons l’infrastructure principale !

Okay ! 50% du travail effectué ! 

On va maintenant déployer une nouvelle configuration quasiment identique : 

Deployment :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: canary-app
  template:
    metadata:
      labels:
        app: canary-app
    spec:
      containers:
        - name: canary-app
          image: basek8s-canary:yellow
          ports:
            - containerPort: 80

Service :

apiVersion: v1
kind: Service
metadata:
  name: canary-svc
  labels:
    app: canary-app
spec:
  type: ClusterIP
  selector:
    app: canary-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Ingress :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "50"
spec:
  ingressClassName: nginx
  rules:
    - host: canary.127.0.0.1.nip.io
      http:
        paths:
          - pathType: Prefix
            path: /
            backend:
              service:
                name: canary-svc
                port:
                  number: 80

⏩Ici on verra 50% du trafic redirigé vers l’infrastructure canary.
⏩On utilise bien le même host, c’est donc transparent pour nos utilisateurs.

Une fois les éléments canary déployés, voici le rendu quand on actualise la page web en boucle :

Youpi ! On voit que la couleur alterne, cela signifie que l’on touche alternativement l’une ou l’autre infrastructure.

Considérations

Nous avons vu comment mettre en place une infrastructure canary sur un environnement kubernetes local. L’exemple présenté est assez simpliste dans son approche, dirigeant le trafic uniquement via une répartition de charge fixe entre les deux infrastructures. Evidemment, le modèle présenté est loin d’être applicable dans des conditions de production réelles. Pour une approche plus théorique, je vous invite à lire l’excellent workbook sur le SRE chez Google (précisément le chapitre dédié au Canary Release https://sre.google/workbook/canarying-releases/).

Pour aller plus loin sur un aspect technique, il devient intéressant de considérer des solutions de CD dédiées à k8s, ArgoCD  ou flagger en sont des exemples.

Sources