logo
< Back Post-Image

Utilisation de Kustomize avec Flux CD

Aujourd’hui, nous allons parler de deux sujets, un dont nous parlons chaque semaine : GitOps, et un dont nous n’avons jamais parlé : Kustomize.

Nous sommes depuis un petit moment de fervents utilisateurs de Helm. Helm est une solution de templating pour manifestes Kubernetes se basant sur Go template. C’est un petit peu la référence en terme de “packaging” d’application Kubernetes. Vous pouvez trouvez des Helm Charts officiels et communautaires pour la plupart de vos middlewares, par exemple rabbitmq ou encore Kong. Ces Charts sont notamment centralisés sur le Helm Hub.

Certains Charts sont très complets et cela vous évite de réinventer la roue et de profiter de l’avancement de la communauté lorsque que vous souhaitez utiliser des solutions communément déployées.

Sauf qu’arrive ensuite le moment où vous devez packager vos propres application. Dans ce cas, pas de Helm Chart officiel.

La première solution serait de rester natif à Kubernetes et d’écrire ses propres fichiers YAML et de les dupliquer par environnement. C’est un peu la première étape de réflexion. Dans un cas de multi environnements, par exemple avec le classique staging, preprod, prod, vous allez vite vous retrouvez à dupliquer vos fichiers YAML, et comme d’habitude en ce qui concerne la duplication de code : fastidieux et source d’erreurs.

C’est là qu’interviennent les outils de templating, il en existe légion avec chacun leur caractéristiques. Pour ne citer qu’eux :

Nous n’allons pas ici faire un comparatif exhaustif des différentes solutions, chacune est un peu philosophiquement différente. Nous allons surtout nous concentrer sur Kustomize.

Helm est clairement l’outil le plus connu et le plus utilisé. Mais lorsque l’on débute avec les manifestes Kubernetes, la marche peut être assez haute : il faut en plus d’être à l’aise avec l’API Kubernetes, apprendre un nouveau système de templating (Go Template) ainsi que les best practices liées a Helm.

Démarrer avec Kustomize

C’est ici qu’intervient Kustomize, qui était à la base un outil standalone mais qui est maintenant intégré à kubectl depuis Kubernetes v1.14.0.

L’avantage de Kustomize, en plus d’être natif à Kubernetes, est qu’il ne nécessite pas de notions de templating avancées. Kustomize se base sur la notion de patch, si vous avez déjà utilisé la commande kubectl patch afin de mettre à jour une ressource, le fonctionnement est un peu équivalent, nous allons voir cela par la suite.

Nous allons partir de ce dépôt Git et d’un cluster Kubernetes.

Nous allons créer deux namespaces:

kubectl create ns preprod
kubectl create ns prod

La structure du dossier kustomize est la suivante :

.
├── base
│   ├── helloworld-de.yaml
│   ├── helloworld-hpa.yaml
│   ├── helloworld-svc.yaml
│   └── kustomization.yaml
├── preprod
│   └── kustomization.yaml
└── prod
    ├── kustomization.yaml
    └── replicas-patch.yaml

Regardons un peu plus en détail. Notre dossier base contient nos manifestes YAML de référence. Ce sont des manifestes Kubernetes classiques. Nous avons un Deployment, un Service et un HorizontalPodAutoscaler.

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld
  labels:
    app: helloworld
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - name: helloworld
        image: particule/helloworld
        imagePullPolicy: Always
        ports:
        - name: web
          containerPort: 80
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: helloworld
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: helloworld
  minReplicas: 1
  maxReplicas: 2
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 60
---
apiVersion: v1
kind: Service
metadata:
  name: helloworld
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort

On remarque que nos ressources ne spécifient pas de namespace. Ces manifestes ne seront techniquement jamais déployés mais serviront de base pour nos futurs déploiements dans leurs namespaces respectifs. En plus, notre dossier base contient un fichier kustomization.yaml listant les YAML gérés par Kustomize :

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helloworld-de.yaml
- helloworld-svc.yaml
- helloworld-hpa.yaml

Notre prochain objectif est de générer automatiquement les YAML pour nos deux environnements de preprod et prod. Commençons par la preprod.

Dans le dossier preprod nous avons uniquement un fichier kustomization.yaml :

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base/
namespace: preprod
namePrefix: preprod-

Alors que signifie ce fichier ? Dans un premier temps nous allons charger tous les manifestes présents dans le dossier base. Ensuite nous allons appliquer un namespace à toutes ces ressources. Et enfin nous allons préfixer toutes les ressources par le nom du namespace, ici preprod-.

Observons le résultat. Comme nous le disions en début d’article, Kustomize est intégré à kubectl, pas besoin d’outils supplémentaires. Pour générer nos YAML de preprod :

$ kubectl kustomize preprod

apiVersion: v1
kind: Service
metadata:
  name: preprod-helloworld
  namespace: preprod
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: helloworld
  name: preprod-helloworld
  namespace: preprod
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - image: particule/helloworld
        imagePullPolicy: Always
        name: helloworld
        ports:
        - containerPort: 80
          name: web
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: preprod-helloworld
  namespace: preprod
spec:
  maxReplicas: 2
  metrics:
  - resource:
      name: cpu
      targetAverageUtilization: 60
    type: Resource
  minReplicas: 1
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: preprod-helloworld

On remarque bien les subtiles différences entre base et preprod.

Attaquons nous maintenant à la prod. Pour cela, même principe, un fichier kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../base/
namespace: prod
namePrefix: prod-
patchesStrategicMerge:
- replicas-patch.yaml

Nous rajoutons un préfix ainsi que le namespace. Mais, pour corser un peu, nous allons en plus patcher une des ressource. L'Horizontal Pod Autoscaler a par défaut un nombre de replicas minimum fixé à 1 dans base. En prod nous souhaitons en avoir un minimum de 2 et un maximum de 4.

Le fichier replicas-patch.yaml:

---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: helloworld
spec:
  minReplicas: 2
  maxReplicas: 4

Ici, pas le peine de définir toutes les spécifications de la ressource, Kustomize va réaliser un patch de la ressource de base en remplaçant les valeurs souhaitées.

Générons maintenant les manifestes de prod de la même façon que preprod:

apiVersion: v1
kind: Service
metadata:
  name: prod-helloworld
  namespace: prod
spec:
  ports:
  - port: 80
  selector:
    app: helloworld
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: helloworld
  name: prod-helloworld
  namespace: prod
spec:
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - image: particule/helloworld
        imagePullPolicy: Always
        name: helloworld
        ports:
        - containerPort: 80
          name: web
---
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: prod-helloworld
  namespace: prod
spec:
  maxReplicas: 4
  metrics:
  - resource:
      name: cpu
      targetAverageUtilization: 60
    type: Resource
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: prod-helloworld

Remarquez les maxReplicas et minReplicas respectivement à 4 et 2 au lieu de 2 et 1.

Nous pouvons maintenant appliquer nos deux environnements :

kubectl apply -k prod/
service/prod-helloworld created
deployment.apps/prod-helloworld created
horizontalpodautoscaler.autoscaling/prod-helloworld created

kubectl apply -k preprod/
service/preprod-helloworld created
deployment.apps/preprod-helloworld created
horizontalpodautoscaler.autoscaling/preprod-helloworld created

Vérifions nos ressources sur le cluster :

kubectl -n preprod get hpa,deployments,services

NAME                                                     REFERENCE                       TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/preprod-helloworld   Deployment/preprod-helloworld   <unknown>/60%   1         2         1          38m

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/preprod-helloworld   1/1     1            1           38m

NAME                         TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/preprod-helloworld   NodePort   10.103.2.95   <none>        80:30339/TCP   38m
kubectl -n prod get hpa,deployments,services

NAME                                                  REFERENCE                    TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
horizontalpodautoscaler.autoscaling/prod-helloworld   Deployment/prod-helloworld   <unknown>/60%   2         4         2          36m

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/prod-helloworld   2/2     2            2           36m

NAME                      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/prod-helloworld   NodePort   10.107.91.161   <none>        80:31145/TCP   36m

Kustomize permet simplement de surcharger des manifestes YAML sans aucune connaissance préalable niveau templating. Pour aller plus loin, la référence des possibilités est disponible ici

Déployer des templates Kustomize avec Flux CD

Maintenant que nous arrivons à générer nos manifestes pour nos deux environnements, comment pouvons nous intégrer ce processus dans une logique GitOps ?

Nous avons déjà beaucoup parlé de Flux CD ici et la et nous allons encore en parler aujourd’hui puisque Flux permet la génération de manifestes Kubernetes avec Kustomize. En theorie Flux permet la génération de manifestes via n’importe quelle commande de templating mais nous allons ici nous concentrer sur la partie Kustomize.

Pour cela nous allons repartir de nos travaux précédents en créant un nouveau dossier kustomize-flux basé sur notre dossier kustomize.

La structure est la suivante :

.
├── .flux.yaml
├── base
│   ├── helloworld-de.yaml
│   ├── helloworld-hpa.yaml
│   ├── helloworld-svc.yaml
│   └── kustomization.yaml
├── preprod
│   ├── flux-patch.yaml
│   └── kustomization.yaml
├── prod
│   ├── flux-patch.yaml
│   ├── kustomization.yaml
│   └── replicas-patch.yaml
├── values-flux-preprod.yaml
└── values-flux-prod.yaml

Voyons ensemble les fichiers supplémentaires.

Déploiement de Flux

Dans un premier temps nous avons deux fichiers de values Helm qui vont nous servir à déployer deux instances de Flux sur notre cluster :

  • flux-preprod
git:
  pollInterval: 1m
  url: ssh://git@github.com/particuleio/gitops-demo.git
  branch: master
  path: kustomize-flux/preprod
syncGarbageCollection:
  enabled: true
manifestGeneration: true
additionalArgs:
-  --git-sync-tag=flux-sync-preprod

Cette instance de flux pointe sur le dossier preprod

  • flux-prod
git:
  pollInterval: 1m
  url: ssh://git@github.com/particuleio/gitops-demo.git
  branch: master
  path: kustomize-flux/prod
syncGarbageCollection:
  enabled: true
manifestGeneration: true
additionalArgs:
-  --git-sync-tag=flux-sync-prod

Cette instance de flux pointe sur le dossier prod

Nous pouvons ensuite déployer Flux via les commandes suivantes :

helm upgrade -i flux-prod fluxcd/flux --namespace prod --values values-flux-prod.yaml

helm upgrade -i flux-preprod fluxcd/flux --namespace preprod --values values-flux-preprod.yaml

Vérifions que Flux est bien déployé :

kubectl -n prod get pods
NAME                                   READY   STATUS    RESTARTS   AGE
flux-prod-588b66bb64-fsw5q             1/1     Running   0          31m
flux-prod-memcached-546c87f4d4-8rwtw   1/1     Running   0          34m
kubectl -n preprod get pods
NAME                                      READY   STATUS    RESTARTS   AGE
flux-preprod-6bdc5dfb6-thqx9              1/1     Running   0          32m
flux-preprod-memcached-59f5454c6f-ldl25   1/1     Running   0          34m

Le fichier .flux.yaml

Un autre fichier additionnel est le fichier .flux.yaml, c’est lui qui va indiquer à Flux comment générer les manifestes :

version: 1
patchUpdated:
  generators:
    - command: kubectl kustomize .
  patchFile: flux-patch.yaml

Ici nous utilisons la même commande que celle utilisée pour générer les manifestes manuellement.

En plus de cela, nous indiquons à Flux où se trouvent les patchs spécifiques à Flux qui seront utilisés (par exemple dans le cas d’une release automatisée, ou encore pour activer des paramètres spécifiques à Flux que nous allons voir par la suite).

Les fichiers flux-patch.yaml

Si vous avez déjà parcouru nos différents articles sur Flux, vous savez qu’il permet, en plus d’appliquer les fichiers YAML sur un cluster, de gérer le déploiement automatisé des nouvelles images Docker en fonction de différentes règles et notamment le semver.

Cette fonctionnalité est gérée via des annotations sur les deployments, ces annotations peuvent être différentes suivant les environnements, par exemple dans notre cas nous allons interdire le déploiement automatisé en prod mais nous allons l’activer en preprod.

Par exemple en prod, le fichier flux-patch.yaml :

  • prod/flux.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    flux.weave.works/locked: "true"
    flux.weave.works/locked_msg: Lock deployment in production
    flux.weave.works/locked_user: Particule
  name: prod-helloworld
  namespace: prod

En preprod nous allons déployer toutes les versions ~1.X.X :

  • preprod/flux-patch.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    flux.weave.works/automated: "true"
    flux.weave.works/tag.helloworld: semver:~1
  name: preprod-helloworld
  namespace: preprod

Une fois que tout est prêt dans notre dépôt, nous pouvons activer Flux sur le dépôt git, pour cela il nous faut récupérer la clé publique de chaque instance de Flux et l’ajouter sur Github dans Settings -> Deploy Keys.

kubectl -n preprod logs flux-pod | head

ts=2020-06-10T14:35:43.197547567Z caller=main.go:493 component=cluster identity.pub="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNYjo5gddG6fXJg75L3gf2mXNBV+DKd9LPz9ZqK2phhwD0fI7J2LajxKnTQGtxj72VBqU+lweEP8YV15auswyjraIYLgnLEE5POb6H8Cjz0vfVX61j3fcLnH77n48GQDKWo0rYQ9hxSmSthi/E1FGy41thxOYRm/IIErN8whKC0+YWDeKlwLNZatSSs/3XA4Q3eCpdPWwAot8sEWDOexUeno/GyaDhBiHm7gxjKkMPsnW8lj9ovtCzjt2H+vLV57neIcx4hx/bhWr3z+wVxkbnDv8zIfXaziXfy5Ueuz0e9sQ3pE1lbrTkeumQN0ekHNAdRjpIa89RRok6KTfBFN7w8iXoLvuSR1NZe9/aunZwqG0ZDGXQjmE8/AHy00QhXmDQT+1VJX00uq/0Jx87v6yiHV+I3LyA1Rn946S4qpxsvFAqDVyKrxFy6WwDSDhd4GHAlI/gFE6dPn8FXqQtL9NVWUxTqFs6svHTLNq6orQ92oKELcsTPHvUyvflj+5JW6k= root@flux-preprod-865d6d9666-6w4x7"

Une fois les clés rajoutées, Flux commencera à déployer les ressources via Kustomize.

kubectl -n preprod logs -f flux-pod

ts=2020-06-10T14:11:29.602531682Z caller=sync.go:539 method=Sync cmd=apply args= count=3
ts=2020-06-10T14:11:29.98907821Z caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=386.489628ms err=null output="service/preprod-helloworld unchanged\ndeployment.apps/preprod-helloworld unchanged\nhorizontalpodautoscaler.autoscaling/preprod-helloworld unchanged"
kubectl -n preprod logs -f flux-pod

ts=2020-06-10T14:13:58.283012811Z caller=sync.go:539 method=Sync cmd=apply args= count=3
ts=2020-06-10T14:13:58.684738069Z caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=401.666475ms err=null output="service/prod-helloworld unchanged\ndeployment.apps/prod-helloworld unchanged\nhorizontalpodautoscaler.autoscaling/prod-helloworld unchanged"

Nos ressources sont bien appliquées sur le cluster via Flux.

Test déploiement automatisé

Nous allons maintenant pousser sur le Docker Hub une nouvelle version 1.1 de notre image Docker helloworld et voir si Flux met à jour notre déploiement automatiquement ainsi que l’impact sur notre dépôt git.

docker push particule/helloworld:1.1

Environ 5 minutes après (le polling interval par defaut de Flux). Flux devrait commit sur git la mise à jour de l’image :

Détail sur le workflow

Que se passe t-il réellement sur le cluster ? Le workflow de déploiement de Flux est le suivant dans le cadre du déploiement initial :

  • Flux génère les YAML via Kustomize
  • Flux applique les flux-patch.yaml
  • Flux applique les manifestes sur le cluster

Dans le cas d’une mise à jour d’images :

  • Flux scan les registry Docker
  • Flux détecte la nouvelle image
  • Flux met à jour sur le dépôt git le fichier flux-patch.yaml correspondant
  • Répétition du workflow précèdent

Aller encore plus loin

Cet article a été inspiré par la communauté flux qui propose des dépôts git d’exemple afin de déployer facilement des manifestes avec Kustomize dans le cas de cluster multi-tenants.

La documentation officielle de cette fonctionnalité est également disponible ici

Conclusion

Nous vous avions déjà présenté Flux à maintes reprises. C’est un outils bourré de fonctionnalités, du déploiement automatisé d’images au déploiement de Helm Chart via Helm Operator en passant par la génération de manifestes via Kustomize que nous l’avons couvert aujourd’hui.

Flux permet de centraliser vos manifestes Kubernetes et d’utiliser un workflow GitOps tout en gardant la souplesse du choix de technologie, que ce soit via des manifestes Kubernetes statiques, des Helm charts ou du Kustomize.

Nous verrons dans un prochain article comment gérer ce dépot Git hétérogène composé de multiples technologies ainsi que l’intégration avec Github Action via kubernetes-toolset

Kevin Lefevre