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.
Pourquoi un environnement d’exécution ?
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 :
- 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. - 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.
- 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.
Pourquoi kaniko ?
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.
Construire un environnement d’exécution : comment ça fonctionne ?
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) :
pip3 install --user ansible-builder
Nous pouvons maintenant créer le fichier descripteur de notre environnement d’exécution execution-environment.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
🤓.
- 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
- 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)
- galaxy : Un fichier
- 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
Passons aux choses sérieuses : notre environnement d’exécution
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 parcommunity.hashi_vault
Commençons par créér notre fichier execution-environment.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
:
---
collections:
- name: community.general
version: 6.4.0
- name: community.hashi_vault
version: 4.1.0
requirements.txt
:
hvac==1.1.0
Testons notre configuration en local pour s’assurer que tout fonctionne :
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é :
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):
- Une première étape nommée
galaxy
dans laquelle ansible-galaxy est exécuté afin d’installer les collections listées dans notre fichierrequirements.yml
- Une seconde étape nommée
builder
dans laquelle ansible-builder installer les dépendances Python listées dans notre fichierrequirements.txt
, et exécutant le script embarquéassemble
(qui génère le répertoire/output
) - 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)
Place au pipeline GitLab CI
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 :
- Un premier job de génération du Dockerfile et du contexte de build avec
ansible-builder create
- 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
:
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écuteransible-builder create
sur nos sources - exporte comme artifact le répertoire
context
généré
- utilise l’image
- 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 fichierconfig.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
)
- génère dans le répertoire
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 ! 👌