Kubernetes - Chaîne CI/CD - RaspberryPI - Drone (arm64)

La chaîne CI/CD (continuous integration/continuous delivery) va permettre, entre autres, l’automatisation de la mise à jour des applicatifs déployés dans notre cluster Kubernetes.

Cet article présente uniquement la mise à jour d’une application. Le but final est d’effectuer une opération “PUT” sur l’API Kubernetes liée au “Deployment” d’une application. L’installation complète d’un applicatif est tout à fait possible mais nécessite plus de règles en matières d’autorisations sur le cluster (hors cadre pour cette démonstration).

L’outil utilisé pour le CI/CD est Drone, quand au repository git, j’utilise Gitea.

Installer “drone” dans le cluster Kubernetes

Pré-requis

  • Cluster kubernetes opérationnel
  • Dépôt Gitea (repo git)
  • Service Postgresql (persistence des données)
  • Vous savez exposer un service réseau kubernetes (haproxy, traefic,…)

Base de données postgresql

Commencer par créer une base de données pour l’applicatif “drone”. Cette base de donnée devra être accessible de tous les “Workers” du cluster kubernetes (paramétrage pg_hba.conf nécessaire).

Exemple :

  • Nom de la base de données : drone
  • Utilisateur postgresql : drone
  • Mot de passe de l’utilisateur postgresql : mypassword

Connecté au serveur postgresql :

su - postgres
psql
CREATE USER drone with password 'mypassword';
CREATE DATABASE drone WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';
GRANT ALL PRIVILEGES ON DATABASE drone to drone;
\q
exit

Modifiez le fichier pg_hba.conf pour que la nouvelle base de données “drone” soit accessible de tous les workers du cluster Kubernetes.

Exemple, mon cluster kubernetes comporte deux “Workers”, dont les adresses IP respectives sont :

  • 192.168.1.20
  • 192.168.1.21

Ajoutez au fichier pg_hba.conf :

host drone    drone   192.168.1.20/32  md5
host drone    drone   192.168.1.21/32  md5

puis exécuter : systemctl reload postgresql

Les informations de comptes (utilisateur/mot de passe, addresseIP/port du serveur postgresql) seront nécessaires pour effectuer le paramétrage de “drone”.

Installation de drone

Vous êtes maintenant habitués à utiliser les “manifests” Kubernetes, qui décrivent les opérations à réaliser dans le cluster. Nous allons créer le descriptif de déploiement de l’application “drone”.

Manifest de déploiement

Dans cet ordre :

  • Création du “namespace”
  • Création du “Deployment”
  • Création du “Service”

Contenu du fichier “manifest.yml” :

apiVersion: "v1"
kind: "Namespace"
metadata:
  name: "drone"
  labels:
    name: "drone"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "drone"
  namespace: "drone"
spec:
  revisionHistoryLimit: 1
  strategy:
    type: "Recreate"
  selector:
    matchLabels:
      app: "drone"
  replicas: 1
  template:
    metadata:
      labels:
        app: "drone"
    spec:
      containers:
        - name: "drone"
          env:
            - name: DRONE_GITEA_SERVER
              value: "https://git.mondomainelocal"
            # Client ID et secret d'accès à l'application GITEA (voir documentation post-installation drone)
            - name: DRONE_GITEA_CLIENT_ID
              value: "xxxxxxxxxxxx"
            - name: DRONE_GITEA_CLIENT_SECRET
              value: "xxxxxxxxxxxxxx"
            # Permet de passer la vérification du certificat autosigné Gitea
            - name: DRONE_GITEA_SKIP_VERIFY
              value: "true"
            # Paramétrage drone (voir documentation post-installation drone)
            - name: DRONE_RPC_SECRET
              value: "xxxxxxxxx"
            - name: DRONE_SERVER_HOST
              value: "nom de domaine permettant l'accès http à drone, a paramétrer sur votre domaine ex: monurldronesurmoninfrastructure"
            - name: DRONE_SERVER_PROTO
              value: "https"
            # Paramétrage drone base de données (persistence des données)
            - name: DRONE_DATABASE_DRIVER
              value: "postgres"
            - name: DRONE_DATABASE_DATASOURCE
              value: "postgres://[login postgres]:[mot de passe postgres]@[Ip server postgres]:[port server postgres]/[nom de base de données utilisé par drone]?sslmode"
            # Permet d'indiquer que vous êtes admin de l'application drone
            - name: DRONE_USER_CREATE
              value: "username:[votre compte utiisateur gitea],admin:true"

          image: "drone/drone:2.11.1"
          # imagePullPolicy : Always IfNotPresent None
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 80
              protocol: "TCP"
              name: "port-80"
---
## Cette partie est à adapter à votre infrastructure
apiVersion: "v1"
kind: Service
metadata:
  name: "drone-app-port-0-80"
  namespace: "drone"
  annotations:
    balance: "roundrobin"
    url: "https://monurldronesurmoninfrastructure"
spec:
  type: "NodePort"
  ports:
    - port: 80
      protocol: "TCP"
      targetPort: 80
  selector:
    app: "drone"
---

Pour installer, exécuter, à partir du master kubernetes : kubectl apply -f manifest.yml

N.B. : Pour la partie ‘Service’, la partie “annotations” est propre à mon installation. J’utilise “Haproxy” pour exposer mes services et dispose d’un outil permettant la mise à jour automatique des règles “haproxy” en fonction des changements détectés sur les services réseaux de mon cluster kubernetes. Ainsi, en lisant la description d’un service je sais sur quelle url de mon infrastructure est exposée un service applicatif, et ces informations permettent un paramétrage automatique du service haproxy sur le datacenter.

Connexion à l’ihm drone

L’applicatif est démarré, le service reseau est effectif, ouvrir votre navigateur, puis indiquer l’url d’accès au service ex : https://monurldronesurmoninfrastructure. Vous devez vous connecter, ceci va se faire au moyen de l’authentification “oauth” de l’application GITEA. Vous allez être redirigé vers GITEA afin d’autoriser votre compte à se connecter à drone. Vous ne pourrez pas démarrer de “build” puisque le runner n’est pas installé.

Drone runners

“drone” est maintenant actif, nous allons passer au “runner”. Le “runner” est l’applicatif exécuté par “drone” pour réaliser les opérations contenues dans le “pipeline”.

Installation du runner

Dans cet ordre :

  • Création du “namespace”
  • Création du “Deployment”

Contenu du fichier “manifest.yml” :

apiVersion: "v1"
kind: "Namespace"
metadata:
  name: "drone-runner"
  labels:
    name: "drone-runner"
---
apiVersion: "apps/v1"
kind: "Deployment"
metadata:
  name: "drone-runner"
  namespace: "drone-runner"
spec:
  revisionHistoryLimit: 1
  strategy:
    type: "Recreate"
  selector:
    matchLabels:
      app: "drone-runner"
  replicas: 1
  template:
    metadata:
      labels:
        app: "drone-runner"
    spec:
      containers:
        - name: "drone-runner"
          env:
            # Paramétrage drone (voir documentation post-installation drone)
            # Ces informations doivent être similaires aux informations fournies pour la partie serveur drone
            - name: DRONE_RPC_SECRET
              value: "xxxxxxxxx"
            - name: DRONE_SERVER_HOST
              value: "nom de domaine permettant l'accès http à drone, a paramétrer sur votre domaine"
            - name: DRONE_SERVER_PROTO
              value: "https"
            - name: DRONE_NAMESPACE_DEFAULT
              value: "drone-runner"
          image: "drone/drone-runner-kube:linux-arm64"
          # imagePullPolicy : Always IfNotPresent None
          imagePullPolicy: "IfNotPresent"
---

Pour installer, exécuter, à partir du master kubernetes : kubectl apply -f manifest.yml

Règles RBAC pour autoriser les modications sur les Deployment.

Les “drone runners” sont exécutés dans le namespace “drone-runner”, je vais autoriser le serviceAccount “default” de ce namespace à modifier les ressources “Deployment” sur l’intégralité du cluster.

Sur le master du cluster kubernetes, je crée le fichier “clusterrole-deployment-update.yml”.

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: drone-deploy
rules:
  - apiGroups:
      - "apps"
    resources:
      - "deployments"
    verbs:
      - "update"

Ce role autorise la mise à jour de la ressource “Deployment” de tous les “namespaces” du cluster kubernetes (ClusterRole)

Pour créer ce rôle : kubeclt apply -f clusterrole-deployment-update.yml

Associons maintenant le serviceAccount “drone-runner:default” au ClusterRole “drone-deploy”, créer le fichier “clusterrolebinding-dronerunner-deploy.yml” :

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: clusterrole-drone-runner-deploy
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: drone-deploy
subjects:
  - kind: ServiceAccount
    name: default
    namespace: drone-runner

Appliquons les modifications : kubeclt apply -f clusterrolebinding-dronerunner-deploy.yml

Activation d’un repository dans drone

Cette opération va permettre d’ajouter un repository Gitea à l’application “drone”, pour permettre l’exécution des pipeline. Connectez-vous à l’ihm de drone, cliquez sur le bouton “Sync”, “drone” va récupérer la liste des repositories Gitea auxquels vous avez accès. Cliquez sur un de vos projets, puis sur le bouton “Activate repository”. Votre repo est maintenant connecté à GITEA au travers d’un webhook. Pour le vérifier, connectez-vous à Gitea, ouvrir le projet connecté, puis “settings” et webhook. Une webhook a été créé par drone, vous pouvez affiner le réglage de ce trigger…

Pipelines

Un “pipeline” est une suite d’instructions à réaliser, tout comme, vous les réaliserez manuellement, c’est à dire, étape par étape, pour obtenir le résultat attendu. Par exemple, vous souhaitez, à partir d’un code source nodejs, exécuter le “build” de l’appplication :

  • npm install
  • npm run build

Toutes ces opérations peuvent être effectuées automatiquement par “drone” et en utilisant la technologie des “containers”.

Comment fonctionne cette automatisation ? Mes repositories “Gitea” sont paramétrés de telle sorte qu’à chaque “push”, “Gitea” appelle l’API de “drone” qui lui indique de démarrer l’exécution du pipeline. Ce pipeline est décrit dans un fichier “.drone.yml”, et disposé dans chaque repository. Ce pipeline est bien entendu adapté à chaque cas d’utilisation.

graph TD A[Utilisateur] -->|push| B(Gitea) B -->|"call API"| C(drone) C -->|"read .drone.yml"| C C -->|"execute pipeline operations"| D(drone-runner)

Exemple de pipeline

Le pipeline est décrit dans le fichier “.drone.yml” et disposé dans mon repository git.

L’exemple ci-après permet la mise à jour de l’applicatif “swagger” dans mon cluster kubernetes. Je rappelle que “swagger” est déjà déployé dans mon cluster. L’opération consiste à appliquer les modifications nécessaires sur la partie “Deployment” de l’applicatif “swagger”, en l’occurence un changement de version.

L’applicatif “swagger” est déployé en version “4.2.0” que je vais passer en “4.2.2”. Je dispose d’un outillage personnalisé permettant de générer des manifests, fonction d’un fichier de description “yaml”. Cet outil n’est pas décrit ici…

Ci-après, vous trouverez l’ensemble des étapes nécessaires au déploiement de la nouvelle version de Swagger dans le cluster kubernetes :

  • pull de l’outillage situé dans un “submodule” git
  • récupération du code source swagger dans sa release “4.2.2” (format zip)
  • extraction de l’archive
  • exécution des commandes : “npm install et npm run build”
  • Génération du Dockerfile à l’aide de la commande make (outillage personnel)
  • construction de l’image docker et “push” dans ma registry au moyen de secrets stockés dans “drone”
  • construction du manifest “Deployment” à l’aide de la commande make (outillage personel)
  • deploiement dans kubernetes, sans plugin, juste en appelant l’API kubernetes
---
kind: pipeline
type: kubernetes
name: default

##Permet d'utiliser le repo git paramétré avec un certificat autosigné (gitea)
clone:
  skip_verify: true

steps:
  ## pull des submodules git, il s'agit de l'outillage personnel
  ## inclus dans mon repo git (dépendances, voir submodules git)
  - name: "git pull submodules"
    image: alpine/git
    commands:
      - git submodule update --init --recursive

  ## J'utilise une image personnelle de wget et unzip, bâtie à partir d''un système alpine
  - name: "get code swagger tags v4.2.2.zip"
    image: docker.mytinydc.com/drone-tools/wget
    commands:
      - wget https://github.com/swagger-api/swagger-editor/archive/refs/tags/v4.2.2.zip
      - unzip v4.2.2.zip

  ## J'utilise une image personnelle de npm, bâtie à partir d'un système alpine
  ## permettant de construire le package nodejs
  - name: "Swagger npm install-build dist"
    image: docker.mytinydc.com/drone-tools/npm
    commands:
      - ln -s swagger-editor-4.2.2 swagger-editor
      - cd swagger-editor && npm install && npm run build

  ## Génére le Dockerfile nommé "Dockerfile-drone"
  ## qui sera utilisé dans l'étape suivante
  - name: "building Dockerfile-drone"
    image: docker.mytinydc.com/drone-tools/make
    commands:
      - make dronebuilddockerfilecustom

  ## Build de l'image Docker à partir du Dockerfile "Dockerfile-drone"
  ## généré par la commande précédente
  - name: "build and push docker image"
    image: plugins/docker
    settings:
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
      tags:
        - latest
        - "4.2.2"
      dockerfile: Dockerfile-drone
      repo: docker.mytinydc.com/swagger-mtdc
      registry: docker.mytinydc.com

  ## outillage personnel permettant de générer le manifest "deployment.yml"
  - name: "build kubernetes deployment.yml"
    image: docker.mytinydc.com/drone-tools/make
    commands:
      - make k8supdatedeployment

  ## ici j'utilise l'AIP kubernetes permettant la mise à jour d'un "Deployment"
  ## Les variables sont disponibles dans chaque container d'un pod $KUBERNETES...
  ## il nous faut aussi le nom du namespace concerné, ici "swagger"
  ## on peut aussi voir que les secrets liés au ServiceAccount sont disponibles dans le container
  ## au niveau du répertoire /var/run/secrets/kubernetes.io/serviceaccount/ monté automatiquement par kubernetes
  ## dans chaque container du pod
  - name: "deploy in cluster"
    image: docker.mytinydc.com/drone-tools/kube-api-curl
    commands:
      - 'curl https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/apis/apps/v1/namespaces/swagger/deployments/swagger --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt --header "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" -X PUT -H "Content-Type: application/yaml" -d "$(cat deployment.yml)"'

# utilisation des secrets d'accès à la registry
# cette information est stockée dans le paramétrage ("settings") du projet drone
# au niveau du projet ou bien de l'organisation (voir documentation drone)
image_pull_secrets:
  - dockerconfigjson

J’ai automatisé l’ensemble avec de l’outillage personnalisé, il est tout à fait possible de préparer, en amont, tous les fichiers nécessaires (Dockerfile, deployment.yml, …) à la réalisation complète du pipeline.

La partie deploiement sur kubernetes nécessite la mise en place d’autorisations au travers de règles RBAC. En effet, ici j’ai autorisé le serviceAccount du namespace utilisé par le “runner drone”, à modifier les ressources de type “Deployment” sur l’ensemble de mon cluster. Je suis seul à utiliser ce cluster, je n’ai donc pas nécessité d’instaurer des rôles supplémentaires.

But de cette automatisation

Le but final de cette implémentation est de pouvoir mettre à jour une application en exécutant le moins de tâches “manuelles” possible. Dans mon cas, et au moyen de l’outillage personnalisé, la mise à jour de swagger, peut s’exécuter directement de l’interface Gitea, en modifiant le fichier de description de mon application, c’est à dire le numéro de version de swagger, tout simplement, d’effectuer le “commit” sur le repository gitea. Une fois l’information stockée, le “webhook drone” Gitea, associé à ce projet, déclenche l’exécution du pipeline.

Exemple : Mon fichier de description de l’application “swagger” ressemble à ceci :

---
appname: "swagger"
# image name is the complete image without tag
image: "swagger-mtdc"
# docker image is built from :
fromimage: "nginx:1.21.6-alpine"
# Always - IfNotPresent (**default) - None
imagepullpolicy: "Always"
# full tag image ( latest is generally not recommanded )
tag: "4.2.0"
# using private registry : true | false default is true, so image will be prefixed with the private docker registry url
useprivatedockerregistry: true
# execution environment variables container :
containerenv:
  - ""

Remarquez l’attribut : tag: ‘4.2.0’, je me connecte à Gitea, je remplace ‘4.2.0’ par ‘4.2.2’ et commit la modification. Drone démarre le pipeline immédiatement et exécute une à une, toutes les étapes de ce dernier, mon application swagger est déployée automatiquement en version 4.2.2…

Déclenchement du pipeline

Vous avez créé le fichier “.drone.yml” dans votre projet git, effectuer l’opération de “commit”, rendez-vous sur l’ihm “drone” et seléctionnez le repo git, vous pourrez vérifier que le pipeline est démarré.

Vous pouvez également démarrer l’exécution du pipeline en cliquant sur le bouton build situé au niveau d’un repository.

Paramétage d’accès à la registry dans drone

Nous avons déclaré dans notre pipeline l’étape “build and push docker image”. Cette opération crée une image Docker et envoie cette image vers la registry de votre choix (privée, dockerhub,…). Lors de cette exécution, “drone” aura besoin des crédentials nécessaire pour se connecter à cette registry.

Connectez-vous à “drone”, rendez-vous sur un des repository déjà activé, cliquez sur “Settings”. Vous pouvez indiquer ces secrets :

  • compte docker
  • mot de passe docker
  • configuration docker

dans la partie “secrets” du projet ou bien au niveau de l’organisation. Ces secrets peuvent être indiqués au niveau de l’organisation, vous n’aurez plus à les mentionner au niveau de chaque projet.

Ajouter les secrets d’accès à la registry privée

  • Secrets utilisés pour la méthode “push” :

    • compte docker : j’ai utilisé dans mon pipeline l’attribut : “docker_username”, j’ajoute au niveau de l’organisation, le secret “docker_username” avec la valeur qui est le compte d’accès à la registry
    • mot de passe docker : j’ai utilisé dans mon pipeline l’attribut : “docker_password”, j’ajoute au niveau de l’organisation, le secret “docker_password” avec la valeur qui est le mot de passe du compte d’accès à la registry
  • Secrets utilisé par la méthode “pull” (phase initiale de clone du repo, automatiquement géré par “drone”) :

    • configuration docker : j’ai utilisé dans mon pipeline l’attribut : “dockerconfigjson”, j’ajoute au niveau de l’organisation, le secret “dockerconfigjson” avec cette valeur qui devra être adaptée à votre infrasctructure :
    {
      "auths": {
        "docker-registry:5000": {
          "auth": "Y29tcHRlOnBhc3N3b3JkCg=="
        },
        "mon.autre.registryhttps": {
          "auth": "Y29tcHRlOnBhc3N3b3JkCg=="
        }
      }
    }
    

    L’attribut “auths”, contient ici les informations d’accès à 2 “registry”, représentées par leur url d’accès, puis du couple : login:password encodé en base64.

Conclusion

Cette présentation n’est qu’un aspect du monde CI/CD… Après avoir utilisé “Jenkins”, je trouve la solution “drone” plus moderne et plus claire.

Elle est entièrement basée sur l’utilisation de “containers”, parfaitement adaptée au monde Kubernetes, ne nécessite pas de connaître de langage de programmation particulier pour réaliser les “basiques CI/CD”.

Il existe une collection de plugins étendues et facilement intégrable dans un pipeline.

Et la chose la plus importante est que cette solution s’intègre parfaitement au projet Mytinydc :

  • peu gourmand en ressources,
  • adaptée aux plateformes ARM64 de petite capacité,
  • et pour finir, une fois l’installation terminée, sans paramétrages supplémentaires, “CA MARCHE…