Helm Chart Releaser avec GitLab CI et Parent-Child Pipelines

Sommaire

En tant qu'admin système, je gère quelques clusters Kubernetes, y déploie des applications, notamment avec Helm. J'utilise essentiellement GitLab comme plateforme de développement, avec la fonctionnalité d'Intégration et Déploiement Continue associée (GitLab CI).

Introduction

Pour livrer quelques charts Helm, j'ai cherché quelque chose de similaire à ce que fait GitHub avec les actions et le chart-releaser. J'ai trouvé quelques projets utilisant les GitLab Pages et Chartmuseum, mais rien que ne me satisfasse.

J'ai alors décidé d'écrire mon propre GitLab CI helm-releaser, avec les spécifications suivantes :

  • Le dépôt des charts Helm doit avoir la même structure que ceux que l'on trouve sur GitHub (comme ceux de Bitnami, Grafana, etc.)
  • Le "releaser" de charts helm doit ne délivrer que les charts qui sont modifiés / mis à jour. La CI, en se déclenchant, ne doit pas tout régénérer.
  • Les charts Helm doivent être mis à disposition sur Harbor, dépôt d'images Docker et charts Helm, que j'utilise déjà.
  • Pouvoir le déployer dans un environnement de staging Kubernetes.
  • Optionnellement, pouvoir générer une release GitLab associée.

Ci-après quelques informations sur comment le chart releaser a été construit, avec les fonctionnalités de la CI GitLab et un peu de bash, comme d'habitude.

La fonctionnalité principale utilisée ici est de pouvoir générer des pipelines dynamiques, avec l'option "parent-child pipelines". Vous trouverez plus d'information ici : https://about.gitlab.com/blog/2020/04/24/parent-child-pipelines/

Structure du dépôt GitLab

La structure du dépôt git ressemble basiquement à cela :

1$ tree -aL 1
2.
3├── charts
4├── gci-templates
5├── .gitlab-ci.yml
6└── scripts
  1. Le dossier charts contient tous les charts.
  2. Le dossier gci-templates contient les templates de jobs CI/CD.
  3. Le dossier scripts contient les scripts permettant la génération automatique des jobs de CI.
  4. Le fichier .gitlab-ci.yml classique pour utiliser la CI GitLab.

Publication Helm avec la CI/CD

Publier un chart Helm est assez simple :

  1. On valide le linting (visibilité / syntaxe du code) du chart.
  2. On vérifie que l'installation peut passer avec un --dry-run.
  3. On package le chart et on l'envoie sur le registre de charts Harbor (ou un autre, au choix), avec un tag dev (voir la partie "Problèmes & Contournements" à ce propos).
  4. On déploie une version correctement taggée sur Harbor (voir semver).
  5. On crée une release GitLab.

Les pipelines de CI ne doivent se déclencher seulement si charts/my-chart est modifié. J'inclus donc un template pour éviter le copier/coller pour chaque chart que je dois générer.

Note : pour ce blog post, uniquement le lint & release sont affichées. Un pipeline plus complet est disponible à la fin de l'article.

Ce qui donne le code suivant dans le job de CI .gitlab-ci.yml :

 1include: '/gci-templates/.gitlab-ci.yml'
 2
 3stages:
 4  - test
 5  - install
 6  - release
 7
 8'my-chart:lint':
 9  stage: test
10  extends: .lint
11  variables:
12    HELM_NAME: "my-chart"
13  rules:
14    - if: '$CI_COMMIT_TAG =~ "/^$/"'
15      changes:
16        - charts/my-chart/**/*
17
18'my-chart:release':
19  stage: release
20  extends: .release
21  variables:
22    HELM_NAME: "my-chart"
23  rules:
24    - if: '$CI_COMMIT_TAG =~ "/^$/"'
25      changes:
26        - charts/my-chart/Chart.yaml

Le template inclus ressemble à cela :

 1---
 2image: dtzar/helm-kubectl:3.5.3 # last version using k8s 1.20
 3
 4.lint:
 5  script:
 6    - helm lint .
 7
 8.release:
 9  script:
10    - apk add git
11    - helm plugin install https://github.com/chartmuseum/helm-push
12    - helm repo add --username=${DOCKER_REGISTRY_USER} --password=$(cat "$DOCKER_REGISTRY_PASSWORD") ${HELM_PROJECT} https://${DOCKER_REGISTRY}/chartrepo/${HELM_PROJECT}
13    - helm package .
14    - helm cm-push . ${HELM_PROJECT}

OK, maintenant que le job de CI est là, on peut publier le chart... Mais comme je n'ai pas envie de récrire les tâches de CI à chaque fois, on va utiliser les pipelines générés !

Generated Pipelines

Les pipelines générés sont très simples : il s'agit d'une tâche de CI qui génère d'autres tâches. On fait cela en 2 étapes :

  1. On génère un fichier yalm qui inclut toutes les étapes de la CI qu'on doit effectuer. Ce fichier yaml peut être créé via des scripts (ici bash)
 1chart-generator:
 2  stage: generate
 3  image: alpine:3.15
 4  script:
 5   - ./scripts/generate-pipeline.sh > generated-pipeline.yml
 6  artifacts:
 7    expire_in: 1 hour
 8    paths:
 9      - generated-pipeline.yml
10  rules:
11    - if: '$CI_COMMIT_TAG =~ "/^$/"'
12      changes:
13      - charts/**/*

Le script generate-pipeline.sh contient le code simple précédemment vu pour générer un seul chart :

 1#!/usr/bin/env bash
 2
 3cat <<EOF
 4include: '/gci-templates/.gitlab-ci.yml'
 5stages:
 6  - test
 7  - install
 8  - release
 9EOF
10
11for f in $(find charts/* -maxdepth 0 -type d)
12do
13  CHART_VERSION=$(grep "^version:" ${f}/Chart.yaml | cut -d' ' -f2-)
14  CHART_RELEASE="${f##*/}-${CHART_VERSION}"
15  cat <<EOF
16'${f##*/}:lint':
17  stage: test
18  extends: .lint
19  variables:
20    HELM_NAME: "${f##*/}"
21  rules:
22    - if: '\$CI_COMMIT_TAG =~ "/^$/"'
23      changes:
24        - ${f}/**/*
25'${f##*/}:release':
26  stage: release
27  extends: .release
28  variables:
29    HELM_NAME: "${f##*/}"
30  rules:
31    - if: '\$CI_COMMIT_TAG =~ "/^$/"'
32      changes:
33        - ${f}/Chart.yaml
34EOF
35
36done
  1. Appelons ce pipeline généré dans la prochaine étape de la CI, avec 2 mots-clés :

    • trigger : pour appeler un pipeline "enfant".
    • include artefact : pour appeler l'artefact précédemment généré.
 1chart-jobs:
 2  stage: generate
 3  needs:
 4    - chart-generator
 5  trigger:
 6    include:
 7      - artifact: generated-pipeline.yml
 8        job: chart-generator
 9    strategy: depend
10  rules:
11    - if: '$CI_COMMIT_TAG =~ "/^$/"'
12      changes:
13      - charts/**/*

Avec toutes ces étapes, un pipeline enfant sera déclenché à chaque fois qu'un chart sera créé ou modifié, sans devoir recopier toutes les tâches de la CI.

Magic

Maintenant que l'on a tout ce qu'il faut pour ne pas réécrire les pipelines à chaque fois, on peut pousser autant de charts que l'on veut, il suffit juste de l'inclure dans le dépôt, et la CI fera le reste !

GitLab Child Pipelines

Remarques et Ressources

On retrouvera le "Helm Chart Releaser" quasiment complet ici, il suffit "juste" de modifier quelques variables. Bien-sûr, à tester avant utilisation ;)

https://gitlab.com/rverchere/helm-chart-release-example

Voir aussi la section release_tag pour pousser les releases sur directement sur GitLab !

"Last but not the least", il ne reste plus qu'un peu de GitOps pour déployer en prod, mais cela est une autre histoire ;)

Problèmes & Contournements

Avec Helm, il n'y a pas de notion de tag "latest" comme dans Docker. Pour contourner cela, on utilisera avec Harbor le tag O.0.0-dev. Ainsi, à chaque nouvelle release orienté dev dans Harbor, on utilisera ce tag.