Aller au contenu
ansible — Kubernetes — helm — Cloud — DevOps

Helm, Kustomize, Ansible - le mauvais tool pour le bon usage

Il arrive que l'on rencontre des situations ou les outils conçu pour un besoin ne peuvent pas répondre à la situation. Le bon outil n'est pas toujours celui que l'on image.

Illustration d'outils pour manipuler Kubernetes
Illustration d'outils pour manipuler Kubernetes (généré par IA)

Abstract

Il existe des cas où des outils adaptés à un usage ne correspondent pas forcément à notre besoin. Cela peut notamment être le cas quand on se retrouve dans la situation finale que les outils adaptés sont censés nous éviter. La question à ce moment-là devient "Comment je peux revenir à une situation me permettant l'usage des outils adéquats ?"

La gestion des manifestes Kubernetes est un enjeu significatif pour les équipes de développement actuelles. Bien que la gestion du cluster soit confié à des "équipes DevOps", il est généralement laissé à la charge de l'équipe de définir les manifestes de Deployment pour déployer les applications dans les clusters.

Plusieurs outils se sont imposés comme références pour gérer les manifestes Kubernetes comme Helm et Kustomize. Cependant, dans certains contextes spécifiques, Ansible peut s'avérer être une alternative intéressante.

Helm et Kustomize

Helm : Le gestionnaire de packages de Kubernetes

Helm est souvent décrit comme le "gestionnaire de packages" de Kubernetes. Il permet de définir, installer et mettre à jour des applications Kubernetes complexes à travers des Charts.

Helm possède plusieurs avantages comme la gestion des dépendances entre applications (à l'image d'un gestionnaire de packages Linux), le versionnement ou encore le templating. Mais cela se traduit par un outil complexe à apprendre et un peu lourd pour les cas simples.

Kustomize : L'approche par superposition

Kustomize adopte une approche différente en se basant sur le principe de superposition. Il permet de personnaliser des configurations Kubernetes sans modifier les fichiers YAML originaux, et celles-ci restent donc valides en dehors de tout moteur de templates. Il est souvent beaucoup plus simple que Helm pour les besoins simples et permet de customiser simplement des petites modifications.

Ni l'un, ni l'autre

Prenons une situation donnée qu'un consultant pourrait rencontrer chez un client, avec une application dont les manifestes s'organisent de la façon suivante :

├── api
│   ├── myApp-api-vs.yaml.j2
│   └── myApp-api.yaml.j2
├── common
│   └── kafka-external-secrets.yaml.j2
├── core
│   ├── myApp-batch-vs.yaml.j2
│   ├── myApp-batch.yaml.j2
│   ├── myApp-core-vs.yaml.j2
│   ├── myApp-front.yaml.j2
│   ├── myApp-mobile.yaml.j2
│   ├── myApp-server-hpa.yaml.j2
│   └── myApp-server.yaml.j2
└── kafka
    ├── myApp-kafka-hpa.yaml.j2
    └── myApp-kafka.yaml.j2

Arboresence des manifestes

A coté de cela, considérons que nous devons déployer sur six cluster : les trois plaques continentales principales (Europe, Asie et Amérique), chacune possédant un environnement de production et de pré-production.
Sur cela, ajoutons le fait que chaque couple plaque/environnement aura des configurations différentes et également plusieurs instances. Chaque instance pouvant aussi avoir des configurations différentes, que l'on dira spécifique.

Actuellement, chaque manifeste est édité à la main et il faut réduire les sources d'erreurs humaines. Mais cette écriture manuelle fait aussi que les configurations ne sont pas harmonisées.

Dans cette situation, Helm ne nous aidera qu'en partie. Nous allons changer la gestion directe des manifestes contre la gestion des values.yaml qui sera aussi très complexe.
De plus, vu l'état hétérogène, Helm n'est pas adapté. Il aurait pu l'être dans une situation où l'on repart de 0 avec des manifestes propres dès le début.
De son côté, Kustomize n'est pas assez puissant pour gérer les structures de contrôle complexes qu'il pourrait y avoir à cause des spécificités de chaque instance.

Ajoutons pour notre cas que des manifestes presque identiques ont aussi des différences. Ces manifestes ayant été traités manuellement pendant des mois, de légers écarts se sont créés et il faut garder le plus possible un état "iso" avec l'actuel afin de limiter les risques.

Ansible et Jinja2

Malgré les avantages de Helm et Kustomize, on vient de décrire une situation où Ansible, avec son moteur de templating Jinja2, peut s'avérer plus adapté pour générer des manifestes Kubernetes.

L'outil idéal pour notre situation nous permettrait d'avoir l'approche par superposition de Kustomize avec la complexité que Helm permet pour des instructions conditionnelles. En détournant l'usage d'Ansible, on peut réussir à produire un tel outil.

Mise en place de la matrice d’environnement

Comme dit plus haut, nous avons plusieurs environnements à prendre en compte, ainsi que plusieurs instances. Pour cela, nous allons utiliser les notions de "host" et "groupe" d'Ansible. Dans notre cas, Ansible se connectera toujours en localhost. Son but n'est que de générer des manifestes en local. En revanche, nous aurons plusieurs hosts qui vont correspondre à chaque instance de notre application. Ensuite, nous les rangerons dans des groupes afin de les affecter aux environnements correspondants.

On obtient ainsi la matrice suivante où chaque instance (host) sera affectée à deux groupes pour représenter sa plaque continentale et son environnement business :

all:
	hosts:
		fr:
		fr_preprod:
		us:
		us_preprod:
		jp:
		jp_preprod:
		common_europe_prod:
		common_europe_preprod:
		common_america_prod:
		common_america_preprod:
		common_asia_prod:
		common_asia_preprod:
	children:
		common:
			common_europe_prod:
			common_europe_preprod:
			common_america_prod:
			common_america_preprod:
			common_asia_prod:
			common_asia_preprod:
		europe_prod:
			fr:
			common_europe_prod:
		europe_preprod:
			fr_preprod:
			common_europe_preprod:
		america_prod:
			us:
			common_america_prod:
		america_preprod:
			us_preprod:
			common_america_preprod:
		asia_prod:
			jp:
			common_asia_prod:
		asia_preprod:
			jp_preprod:
			common_asia_preprod:
		preprod:
			europe_preprod:
			asia_preprod:
			america_preprod:
		prod:
			europe_prod:
			america_prod:
			asia_prod:

Fichier d'inventory Ansible pour les hosts et groupes

La superposition via les variables Ansible

Ansible possède une notion de surcharge des variables. Une même variable peut être déclarée plusieurs fois à plusieurs endroits. Ansible va ensuite, dans un ordre défini, lire les valeurs et prendre celle qui sera la plus proche de ce que l'utilisateur pourra saisir. Dans notre situation, nous allons avoir 4 sources de variables :

  • le fichier group_vars/all.yaml. Ce fichier contiendra nos valeurs par défaut, accessibles pour chaque host
  • les fichiers group_vars/prod.yaml et group_vars/preprod.yaml pour les valeurs propres à l'environnement business
  • les fichiers group_vars/europe.yaml, group_vars/asia.yaml et group_vars/america.yaml pour les valeurs propres à la plaque continentale
  • le fichier host_vars/instance.yaml. Ce fichier contiendra les valeurs absolues pour une instance. Elles lui seront spécifiques
# group_vars/all.yaml
replica_count: 2
image_tag: "stable"

# group_vars/prod.yaml
replica_count: 4

# group_vars/europe.yaml
kafka_cluster: "kafka-eu-01.internal"

# host_vars/fr_prod.yaml
image_tag: "1.4.2-fr-hotfix"

Exemples de superposition des variables

Pour l'host fr, on aura l'ensemble de variables suivant :

replica_count: 4        # vient de prod.yaml (écrase all.yaml)
image_tag: "1.4.2-fr-hotfix"  # vient de host_vars (écrase tout le reste)
kafka_cluster: "kafka-eu-01.internal"  # vient de europe.yaml

Résultat de l'exemple de superposition

Ansible prendra toujours les valeurs les plus proches de l'host. N'importe quelle valeur mise dans le fichier dédié à un host aura le dernier mot et permet d'override n'importe quelle configuration en cas de besoin. De cette façon, nous avons une mécanique nous permettant de reproduire la superposition de Kustomize.

Le templating via Jinja2

Pour obtenir les mêmes avantages que les templates Helm, Ansible se base sur le moteur de template du langage Python : Jinja2. Les deux moteurs sont très semblables en termes de possibilités. Seule la syntaxe est un peu différente. Ces templates vont nous permettre d'avoir les structures de contrôle complexes, notamment des conditions se basant sur des valeurs obtenues via les variables ou encore selon l'appartenance ou non de l'instance/host courant à des groupes spécifiques.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myApp-server
spec:
  replicas: {{ replica_count }}
  template:
    spec:
      containers:
        - name: myApp-server
          image: "registry.internal/myApp:{{ image_tag }}"
          env:
            - name: TZ
              value: "Asia/Tokyo"
{% if 'prod' in group_names %}
            - name: PROD_ONLY_VAR
              value: "{{ prod_only_var }}"
{% endif %}

Exemple d'un template de Deployment Kubernetes sous Jinja2

Une mise en pratique

Pour effectuer la génération des manifestes, on va utiliser un playbook sur ce pattern :

- name: Generate manifests kubernetes  
  hosts: "{{ host }}:!common"  
  gather_facts: no  
  
  tasks:  
    - name: Create core output folder  
      file:  
        path: "{{ gen_output_dir }}/core"  
        state: directory  
        mode: '0755'  
        recurse: true  
  
    - name: Find all templates manifests to deploy  
      find:  
        paths: "templates/manifests/core"  
      register: files  
  
    - name: Extract only filename  
      set_fact:  
        manifest_templates: "{{ files.files | map(attribute='path') | map('basename') }}"  
  
    - name: Generate manifests  
      with_items: "{{ manifest_templates }}"  
      template:  
        src: "manifests/core/{{ item }}"  
        dest: "{{ gen_output_dir }}/core/{{ item.split('.') | first }}.yaml"

Exemple d'un playbook Ansible

On aura un playbook par microservice qui est matérialisé par un dossier dans le dossier racine des templates.
Cette partie est ce qui remplace les fonctions propres à Helm, mais il nous manque les variables et la personnalisation que propose Kustomize. Pour cela, Ansible propose les group et host vars. La structure ressemble au snippet suivant :

host_vars
├── fr.yml
├── fr_preprod.yml
├── jp.yml
├── jp_preprod.yml
├── us.yml
├── us_preprod.yml
├── common_europe_prod.yml
├── common_europe_preprod.yml
├── common_america_prod.yml
├── common_america_preprod.yml
├── common_asia_prod.yml
└── common_asia_preprod.yml

group_vars
├── all.yml
├── common.yml
├── preprod.yml
├── prod.yml
├── europe_prod.yml
├── europe_preprod.yml
├── america_prod.yml
├── america_preprod.yml
├── asia_prod.yml
└── asia_preprod.yml

Arborescence des yaml pour surcharger les valeurs des différents hosts et groupes

Et le cas des communs ?

Dans les exemples, le pattern {{ host }}:!common n'a pas encore été expliqué. Il s'agit là d'une manipulation rendue possible par les groupes d'Ansible qui permettent des choses plus poussées. Il signifie simplement de prendre la valeur {{ host }} (format Jinja2), comme par exemple europe, et de retirer les éléments de common. Il s'agit d'une soustraction des éléments de common à {{ host }} qui donne l'ensemble final d'application du playbook.

Imaginons que nous voulons maintenant avoir les ConfigMap ou des Secrets communs (des crédentials kafka propres à un couple plaque/environnement par exemple). Comment faire pour les générer une fois et ne pas générer les autres manifestes ? L'astuce ici est d'avoir un host nommé "common" (par exemple) avec un suffixe du couple plaque/environnement. Dans notre exemple, nous aurons donc 6 hosts common : common_europe_prod, common_europe_preprod, common_america_prod, common_america_preprod, common_asia_prod et common_asia_preprod. Ce sont ensuite ces hosts qui portent les fichiers utiles pour un ensemble d'hosts. Ils possèdent leur propre groupe "common" qui permet, en fonction de l'host pour lequel on veut générer les manifestes, d'avoir soit les fichiers communs et/ou les fichiers spécifiques aux hosts.

Conclusion

Bien que Helm et Kustomize soient des outils puissants et spécialisés pour la gestion des manifestes Kubernetes, ils ne s'appliquent de façon efficace que si le contexte le permet. Dans des situations non standard, Ansible peut permettre de remettre un existant en état. La puissance du moteur de templating Jinja2 couplé aux hosts Ansible permet une solution plus flexible que Helm ou Kustomize en ayant une technique proche de la superposition avec des templates. Une fois l'existant remis à plat, il est ensuite possible de repasser sur des outils dédiés.

Le choix entre ces différentes approches dépendra de vos besoins spécifiques, de l'expertise de votre équipe et de votre infrastructure globale. Ces outils peuvent même être complémentaires, chacun étant utilisé là où il excelle.

Dernier