Construire un Environnement d'Exécution Ansible/AWX avec GitLab CI et Kaniko

Les environnements d’exécution sont un composant essentiel de l’écosystème AWX (le projet de base d’Ansible Tower / RedHat Automation Platform). Ils permettent de décrire un environnement pré-packagé contenant tous les éléments nécessaires à l’exécution d’un playbook Ansible :

  • Distribution Ansible
  • Packages Python
  • Collections & Roles Ansible
  • Autres binaires

L’environnement d’exécution par défaut d’AWX (image quay.io/ansible/awx-ee - Voir le repo sur GitHub) contient uniquement une distribution Ansible, installée sur l’image de container de base de RHEL : ubi. Nous allons voir ici pourquoi et comment construire son propre environnement d’exécution de manière automatique dans un pipeline GitLab CI en utilisant kaniko pour construire l’image de container.

Dans un projet AWX, il est possible de télécharger, lors de la récupération du projet depuis un repo Git, des collections et roles Ansible listés dans un fichier requirements.yml. Dès lors, pourquoi ai-je besoin d’un environnement d’exécution personnalisé ?

Pour moi, il y a plusieurs raisons à cela :

  1. Seul les fichiers requirements.yml au format Ansible sont pris en charge. Il est donc impossible d’installer une librairie Python, ou un binaire (sauf à le faire manuellement dans un rôle ad hoc), mais uniquement des collections et rôles.
  2. Cela signifie que les collections et rôles sont récupérés à chaque mise à jour du projet depuis Git, ce qui n’est pas fondamentalement nécessaire.
  3. Pour qu’AWX installe à la volée des collections/roles depuis un registry privé autre qu’Ansible Galaxy (dans mon cas : le Package Registry de mon GitLab on-premise), le requirements.yml de votre projet AWX doit contenir les identifiants de connexion à votre registry (Access Token dans mon cas), ce qui n’est clairement pas une bonne pratique. Avec un environnement d’exécution, j’ai la main sur l’injection de ces identifiants à la volée lors de la construction de l’image.

Kaniko est un outil conçu pour compiler des images de container à partir d’un Dockerfile/Containerfile sans Docker Engine.

Dans un pipeline GitLab CI, chaque job est exécuté dans un container OCI/Docker. L’utilisation de docker pour compiler les images de container nécessiterait donc d’exécuter les jobs GitLab CI dans des containers privilégiés, de monter le socket unix du Docker Engine dans les containers des jobs, ou d’utiliser Docker in Docker.

Ces 3 solutions présentent des problèmes notamment de sécurité. En étant prévu pour l’absence de Docker Engine, Kaniko est donc pour moi une solution plus adaptée à la compilation d’images de container à l’intérieur même d’une image de container (Docker/Kubernetes), donc idéale dans un pipeline GitLab CI (ou autre solution de CI/CD basée sur des containers).

Pour les tests en local, nous utiliserons docker pour compiler les images.

Soulevons le capot. La construction d’environnements d’exécution utilise ansible-builder.

Comme tout l’écosystème Ansible, ansible-builder s’installe avec pip (après avoir préalablement installé python) :

bash

pip3 install --user ansible-builder

Nous pouvons maintenant créer le fichier descripteur de notre environnement d’exécution execution-environment.yml :

yml

---
version: 1

build_arg_defaults:
  EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:stable-2.12-latest'

dependencies:
  galaxy: requirements.yml
  python: requirements.txt
  system: bindep.txt

additional_build_steps:
  prepend: |
    RUN pip3 install --upgrade pip setuptools    
  append:
    - RUN ls -la /etc

Regardons en détails le contenu du fichier :

  • build_arg_defaults : Les paramètres de build à transmettre lors de la compilation de l’image
    • EE_BASE_IMAGE : L’image de container qui servira de base à notre environnement d’exécution. Il est évidemment fortement recommandé d’utiliser une image basée sur quay.io/ansible/ansible-runner 🤓.
  • depencencies : Indique le nom des fichiers listant les composants à installer dans l’environnement d’exécution
    • galaxy : Un fichier requirements.yml listant des collections et/ou rôles Ansible
    • python : Un fichier requirements.txt listant des librairies python
    • system : Un fichier bindep listant des paquets RPM (la liste des paquets sera passée à dnf pour installation dans l’image de container finale)
  • additional_build_steps : Décrit des instructions Dockerfile/Containerfile additionnelles à ajouter au Dockerfile qui sera généré
    • prepend : Instructions à placer avant l’installation des dependencies
    • append : Instructions à placer après l’installation des dependencies

Il est temps de mettre les mains dans le cambouis !

Partons du postulat que nos playbooks Ansible nécessitent les dépendances suivantes :

  • La collection community.general
  • La collection community.hashi_vault
  • La librairie python hvac, elle-même requise par community.hashi_vault

Commençons par créér notre fichier execution-environment.yml :

yml

---
version: 1

build_arg_defaults:
  EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:stable-2.12-latest'

dependencies:
  galaxy: requirements.yml
  python: requirements.txt

Dans notre cas nous n’aurons pas besoin de toutes les options présentées plus haut, mais uniquement de l’image de base (pour fixer notre version d’ansible et éviter les surprises), et des fichiers de dépendances Ansible et Python.

Créons maintenant nos fichiers de dépendances. Les versions que nous indiquons ici sont les dernières versions stables des dépendances à l’heure où j’écris ce billet.

requirements.yml :

yml

---
collections:
  - name: community.general
    version: 6.4.0
  - name: community.hashi_vault
    version: 4.1.0

requirements.txt :

yml

hvac==1.1.0

Testons notre configuration en local pour s’assurer que tout fonctionne :

bash

ansible-builder build
File context/_build/requirements.yml had modifications and will be rewritten
File context/_build/requirements.txt had modifications and will be rewritten
Running command:
  docker build -f context/Dockerfile -t ansible-execution-env:latest context

Complete! The build context can be found at: /home/leo/workspaces/gitlab.com/lgatellier/awx-ee/context

Nous voilà rassurés, notre environnement d’exécution se construit correctement ! Pour en savoir un peu plus, jetons un oeil au Dockerfile présent dans le répertoire context tel qu’indiqué :

Dockerfile

ARG EE_BASE_IMAGE=quay.io/ansible/ansible-runner:stable-2.12-latest
ARG EE_BUILDER_IMAGE=quay.io/ansible/ansible-builder:latest

FROM $EE_BASE_IMAGE as galaxy
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS=
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS=
USER root

ADD _build /build
WORKDIR /build

RUN ansible-galaxy role install $ANSIBLE_GALAXY_CLI_ROLE_OPTS -r requirements.yml --roles-path "/usr/share/ansible/roles"
RUN ANSIBLE_GALAXY_DISABLE_GPG_VERIFY=1 ansible-galaxy collection install $ANSIBLE_GALAXY_CLI_COLLECTION_OPTS -r requirements.yml --collections-path "/usr/share/ansible/collections"

FROM $EE_BUILDER_IMAGE as builder

COPY --from=galaxy /usr/share/ansible /usr/share/ansible

ADD _build/requirements.txt requirements.txt
RUN ansible-builder introspect --sanitize --user-pip=requirements.txt --write-bindep=/tmp/src/bindep.txt --write-pip=/tmp/src/requirements.txt
RUN assemble

FROM $EE_BASE_IMAGE
USER root

COPY --from=galaxy /usr/share/ansible /usr/share/ansible

COPY --from=builder /output/ /output/
RUN /output/install-from-bindep && rm -rf /output/wheels
LABEL ansible-execution-environment=true

Nous voyons que le Dockerfile généré comporte 3 étapes de compilation (voir Multi-Stage Build):

  1. Une première étape nommée galaxy dans laquelle ansible-galaxy est exécuté afin d’installer les collections listées dans notre fichier requirements.yml
  2. Une seconde étape nommée builder dans laquelle ansible-builder installer les dépendances Python listées dans notre fichier requirements.txt, et exécutant le script embarqué assemble (qui génère le répertoire /output)
  3. Une dernière étape récupérant tous les fichiers installés précédemment, et exécutant le script /output/install-from-bindep qui va mettre à jour l’OS sous-jacent et installer les éventuels packages bindep décrits (aucun dans notre cas)

Et maintenant ? C’est bien d’avoir une configuration d’environnement d’exécution qui fonctionne en local, mais comme tout le reste, on aime bien que la construction soit automatique !

Oui, mais :

  • Nous ne pouvons pas ajouter la commande ansible-builder build telle quelle dans notre pipeline, car comme on vient de le voir, celle-ci utilise la commande docker, que nous ne voulons/pouvons pas utiliser dans nos pipelines.
  • Ansible-builder propose bien l’option --container-runtime pour utiliser un autre runtime que Docker, mais seul podman est pour l’instant supporté, et mes premiers essais dans un pipeline GitLab CI n’ont pas été très concluants. J’en ferai un autre billet le jour où je prendrai le temps de faire fonctionner ça.
  • Le runtime kaniko n’est pas (encore ?) pris en charge

C’est ici que nous allons utiliser une autre commande d’ansible-builder : la commande create !

La commande ansible-builder create est en charge de générer le Dockerfile et le contexte de build de notre image. C’est d’ailleurs cette commande qui est exécutée en premier par la commande build avant de passer le contexte au container runtime !

Nous allons donc pouvoir séparer notre construction en 2 jobs :

  1. Un premier job de génération du Dockerfile et du contexte de build avec ansible-builder create
  2. Un second job qui fera appel à kaniko pour compiler notre environnement d’exécution avec les éléments générés juste avant

Ne faisons pas durer le suspens plus longtemps, et regardons notre .gitlab-ci.yml :

yml

stages:
  - prepare
  - build

context:create:
  image: quay.io/ansible/ansible-builder
  stage: prepare
  script:
    - ansible-builder create
  artifacts:
    paths:
      - context/

ee:build:
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  stage: build
  before_script:
    - mkdir -p "$DOCKER_CONFIG"
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > "$DOCKER_CONFIG/config.json"
  script:
    - /kaniko/executor --context context --dockerfile context/Dockerfile --destination $CI_REGISTRY_IMAGE:latest

Un peu de détails ? Nous avons ici :

  • 2 stages : prepare et build pour chacun des 2 jobs
  • Un job context:create qui :
    • utilise l’image quay.io/ansible/ansible-builder pour exécuter ansible-builder create sur nos sources
    • exporte comme artifact le répertoire context généré
  • Un job ee:build qui :
    • génère dans le répertoire $DOCKER_CONFIG (défini par l’image kaniko, par défaut /kaniko/.docker) un fichier config.json contenant les informations d’identification auprès du GitLab Container Registry (voir Predefined variables)
    • utilise l’image gcr.io/kaniko-project/executor:debug pour lancer l’exécuteur kaniko sur le contexte récupéré du premier job, et ainsi construire l’image de notre environnement d’exécution (attention à ne pas oublier de préciser l’entrypoint de l’image, car celui-ci est par défaut à /kaniko/executor)

Un petit aperçu du résultat ? Jetez un oeil sur le projet : gitlab.com/lgatellier/awx-ee 😉

J’espère que cet article vous a plu ! Dans tous les cas, n’hésitez pas à venir en parler avec sur Twitter (@leo_auth2 ou Mastodon (@leo_auth2@piaille.fr), vos retours me permettent de m’améliorer, et d’améliorer mes articles. 😉

Un gros remerciement en passant à Stéphane Robert, dont l’article sur les Environnements d’Exécution m’a bien aidé à découvrir le sujet. N’hésitez pas à consulter également son blog, c’est une mine d’or ! 👌