Ansible - Les Environnements d'Exécution
Publié le : 1 février 2023 | Mis à jour le : 27 juin 2023Les environnements d’exécution Ansible sont là pour vous aider à écrire et à exécuter des playbooks quel que soit le contexte. Ils définissent donc des environnements portables et partageables pour exécuter des tâches Ansible.
Ces environnements ont été créés pour faciliter le développement de tâches d’automatisation et de contenu Ansible destinés à être exécutés dans AWX , Ansible Tower ou sur Ansible Automation Platform, et cela de manière cohérente. Mais nous allons voir que nous pouvons les utiliser aussi dans nos chaines de CI/CD.
Principes
L’utilisation de code Ansible possédant des dépendances autres que celles par défaut peuvent être délicat à mettre en place et à maintenir. Les environnements d’exécution d’Ansible se basent donc sur des images OCI dans lequel sont déposés Ansible Core, les collections et les dépendances python nécessaires à l'exécution d’un Code Ansible.
Un outil spécifique, ansible-builder
est mis à disposition pour faciliter la
création de ces images. Cet outil utilise l’image de base Red Hat Enterprise
Linux pour y installer tout cet environnement d’exécution.
Un autre outil, ansible-runner
permet lui l’exécution du code Ansible dans ces
environnements avec des moyens d’isolation et de protection de ceux-ci.
Voyons comment mettre en œuvre ces environnements d’exécution avec ces deux
outils que sont ansible-builder
et ansible-runner
.
Ansible-builder
Comme dit plus haut, ansible-builder
est un outil qui automatise le processus
de création d’environnements d’exécution.
Installation d’ansible-builder
Bien sûr, il est nécessaire d’avoir installé docker
ou podman
sur la machine
de build. Comme le reste de la collection Ansible ansible-builder
est écrit en
python et s’installe donc avec pip
.
pip install ansible-builder --user
Je préfère installer ces outils au niveau user plutôt que système d’où
l’utilisation de l’option --user
.
Fichier de définition d’ansible-builder
ansible-builder
utilise un fichier de définition pour décrire ce qu’il doit
contenir. Ce fichier est écrit en YAML et se nomme execution-environment.yml
dont voici un exemple :
---
version: 3
build_arg_defaults:
ANSIBLE_GALAXY_CLI_COLLECTION_OPTS: '--pre'
dependencies:
ansible_core:
package_pip: ansible-core==2.14.4
ansible_runner:
package_pip: ansible-runner
galaxy: requirements.yml
python:
- six
- psutil
system: bindep.txt
images:
base_image:
name: registry.redhat.io/ansible-automation-platform-21/ee-minimal-rhel8:latest
additional_build_files:
- src: files/ansible.cfg
dest: configs
additional_build_steps:
prepend_galaxy:
- ADD _build/configs/ansible.cfg ~/.ansible.cfg
prepend_final: |
RUN whoami
RUN cat /etc/os-release
append_final:
- RUN echo This is a post-install command!
- RUN ls -la /etc
build_arg_defaults : permet de définir les arguments de bases qui seront
utilisé pour construire l’environnement d’exécution.
ANSIBLE_GALAXY_CLI_COLLECTION_OPTS
permet de passer un flag -pre
s’il est
nécessaire d’installer des pre-release de collections.
ANSIBLE_GALAXY_CLI_ROLE_OPTS
permet de transmettre n’importe quel option, tel
que –no-deps, à l’installation des rôles.
ansible_config : permet d’inclure un fichier de configuration d’Ansible
dependencies : permet de définir les collections galaxy, les librairies python et les packages systèmes à installer.
Elles peuvent être directement indiqué dans la déclaration plutôt que dans des fichiers.
dependencies:
python:
- pywinrm
system:
- iputils [platform:rpm]
galaxy:
collections:
- community.windows
- ansible.utils
ansible_core:
package_pip: ansible-core==2.14.2
ansible_runner:
package_pip: ansible-runner==2.3.1
python_interpreter:
package_system: "python310"
python_path: "/usr/bin/python3.10"
additional_build_steps : permet d’ajouter des commandes s’exécutant avant (prepend) et/ou après (append) la phase d’installation des outils Ansible.
prepend_base
: Commandes à insérer avant la construction de l’image de base.append_base
: Commandes à insérer après la construction de l’image de base.prepend_galaxy
: Commandes à insérer avant la construction de l’image de la galaxy.append_galaxy
: Commandes à insérer après la construction de l’image de la galaxy.prepend_builder
: Commandes à insérer avant la génération de l’image du générateur.append_builder
: Commandes à insérer après la construction de l’image du générateur.prepend_final
: Commandes à insérer avant la construction de l’image finale.append_final
: Commandes à insérer après la construction de l’image finale
images : Cette section est un dictionnaire utilisé pour définir l’image de base à utiliser.
base_image
: Dictionnaire définissant l’image parente de l’environnement d’exécution.
options :
options:
container_init:
package_pip: dumb-init>=1.2.5
entrypoint: '["dumb-init"]'
cmd: '["csh"]'
package_manager_path: /usr/bin/microdnf
relax_password_permissions: false
skip_ansible_check: true
workdir: /myworkdir
user: bob
additional_build_files
: Cette section vous permet d’ajouter n’importe quel fichier au répertoire de contexte de génération.
Chaque élément de liste doit être un dictionnaire contenant les clés suivantes (non facultatives) :
src
: Spécifie le ou les fichiers sources à copier dans le répertoire de contexte de génération.dest
: Spécifie le chemin au sous-répertoire qui doit contenir le(s) fichier(s) source(s)
container_init
: Un dictionnaire avec des clés qui permettent la
personnalisation du conteneur et des directives (et des comportements associés).
cmd
: Valeur littérale pour la directive Containerfile. La valeur par défaut est .CMD[“bash”]entrypoint
: Valeur littérale pour la directive Containerfile.package_pip
: Package à installer via pip pour la prise en charge des points d’entrée. Ce package sera installé dans l’image de build finale. La valeur par défaut est.dumb-init==1.2.5
package_manager_path
: Chaîne avec le chemin d’accès au gestionnaire de
paquets (dnf ou microdnf) à utiliser.
skip_ansible_check
: Cette valeur booléenne contrôle si la vérification d’une
installation d’Ansible et Ansible Runner est exécuté sur l’image finale.
relax_passwd_permissions
: Cette valeur booléenne contrôle si le groupe (GID
- est explicitement accordé.
workdir
: Répertoire de travail par défaut pour les nouveaux processus
démarrés sous le conteneur final image.
user
: Cela définit le nom d’utilisateur ou l’UID à utiliser comme utilisateur
par défaut pour l’image de conteneur finale. Valeur par défaut 1000
Construction d’un environnement d’exécution
Je vais prendre comme exemple la création d’une image assez simple fixant quelques dépendances.
---
version: 3
build_arg_defaults:
ANSIBLE_GALAXY_CLI_COLLECTION_OPTS: '--pre'
dependencies:
ansible_core:
package_pip: ansible-core==2.13.1
ansible_runner:
package_pip: ansible-runner
galaxy: requirements.yml
python:
- six
- psutil
system: bindep.txt
options:
container_init:
package_pip: dumb-init>=1.2.5
entrypoint: '["dumb-init"]'
cmd: '["csh"]'
package_manager_path: /usr/bin/microdnf
images:
base_image:
name: registry.redhat.io/ansible-automation-platform-21/ee-minimal-rhel8:latest
additional_build_files:
- src: files/ansible.cfg
dest: configs
additional_build_steps:
prepend_galaxy:
- ADD _build/configs/ansible.cfg ~/.ansible.cfg
prepend_final: |
RUN whoami
RUN cat /etc/os-release
append_final:
- RUN echo This is a post-install command!
- RUN ls -la /etc
Le fichier ansible.cfg :
[defaults]
strategy=linear
pipelining = True
gathering = smart
fact_caching_connection = /tmp/facts_cache
fact_caching = jsonfile
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
Le fichier requirements.yml :
collections:
- name: community.general
version: 3.8.0
- name: kubernetes.core
version: 2.2.0
- name: ansible.posix
version: 1.3.0
Construction de l’image :
docker login https://registry.redhat.io
Username: robert.stephane.28
Password:
WARNING! Your password will be stored unencrypted in /home/vagrant/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
ansible-builder build --tag=my-custom-ee --verbosity 2
Running command:
docker build -f context/Dockerfile -t my-custom-ee context
Complete! The build context can be found at: /home/vagrant/Projets/personal/ansible-test-runner/context
Lors de l’exécution de cette commande on voit qu’il crée un répertoire context dans lequel on retrouve le Dockerfile généré :
ARG EE_BASE_IMAGE="registry.redhat.io/ansible-automation-platform-21/ee-minimal-rhel8:latest"
ARG PYCMD="/usr/bin/python3"
ARG PKGMGR_PRESERVE_CACHE=""
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS="--pre"
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS=""
ARG ANSIBLE_INSTALL_REFS="ansible-core==2.13.1 ansible-runner"
ARG PKGMGR="/usr/bin/microdnf"
# Base build stage
FROM $EE_BASE_IMAGE as base
USER root
ARG EE_BASE_IMAGE
ARG PYCMD
ARG PKGMGR_PRESERVE_CACHE
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS
ARG ANSIBLE_INSTALL_REFS
ARG PKGMGR
RUN $PYCMD -m ensurepip
RUN $PYCMD -m pip install --no-cache-dir $ANSIBLE_INSTALL_REFS
COPY _build/scripts/ /output/scripts/
COPY _build/scripts/entrypoint /opt/builder/bin/entrypoint
# Galaxy build stage
FROM base as galaxy
ARG EE_BASE_IMAGE
ARG PYCMD
ARG PKGMGR_PRESERVE_CACHE
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS
ARG ANSIBLE_INSTALL_REFS
ARG PKGMGR
ADD _build/configs/ansible.cfg ~/.ansible.cfg
RUN /output/scripts/check_galaxy
COPY _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"
# Builder build stage
FROM base as builder
WORKDIR /build
ARG EE_BASE_IMAGE
ARG PYCMD
ARG PKGMGR_PRESERVE_CACHE
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS
ARG ANSIBLE_INSTALL_REFS
ARG PKGMGR
RUN $PYCMD -m pip install --no-cache-dir bindep pyyaml requirements-parser
COPY --from=galaxy /usr/share/ansible /usr/share/ansible
COPY _build/requirements.txt requirements.txt
COPY _build/bindep.txt bindep.txt
RUN $PYCMD /output/scripts/introspect.py introspect --sanitize --user-pip=requirements.txt --user-bindep=bindep.txt --write-bindep=/tmp/src/bindep.txt --write-pip=/tmp/src/requirements.txt
RUN /output/scripts/assemble
# Final build stage
FROM base as final
ARG EE_BASE_IMAGE
ARG PYCMD
ARG PKGMGR_PRESERVE_CACHE
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS
ARG ANSIBLE_GALAXY_CLI_ROLE_OPTS
ARG ANSIBLE_INSTALL_REFS
ARG PKGMGR
RUN whoami
RUN cat /etc/os-release
RUN /output/scripts/check_ansible $PYCMD
COPY --from=galaxy /usr/share/ansible /usr/share/ansible
COPY --from=builder /output/ /output/
RUN /output/scripts/install-from-bindep && rm -rf /output/wheels
RUN chmod ug+rw /etc/passwd
RUN mkdir -p /runner && chgrp 0 /runner && chmod -R ug+rwx /runner
WORKDIR /runner
RUN $PYCMD -m pip install --no-cache-dir 'dumb-init>=1.2.5'
RUN echo This is a post-install command!
RUN ls -la /etc
RUN rm -rf /output
LABEL ansible-execution-environment=true
USER 1000
ENTRYPOINT ["dumb-init"]
CMD ["csh"]
On remarque qu’il utilise le multi-stage pour limiter la taille de l’image
qui est tout de même de 355 MB (en nette amélioration depuis la version 1 :
910MB). Par contre, je trouve bête qu’il génère une image taguée latest
et pas
avec le numéro de version du fichier de configuration.
docker images ✔ 690 07:35:55
REPOSITORY TAG IMAGE ID CREATED SIZE
my-custom-ee latest a29d4f0b94ce About a minute ago 355MB
Plus loin avec ansible-builder
Je vous renvoie à sa documentation
Ansible-runner
Maintenant que nous avons généré notre environnement d’exécution voyons comment l’utiliser avec ansible-runner.
Installation d’ansible-runner
Comme pour ansible-builder
on l’installe avec pip
:
pip install ansible-runner --user
Lancement d’un playbook avec ansible-runner
Je vais utiliser des machines provisionnées avec vagrant.
Je vais reprendre le Vagrantfile
de mon projet
CKASandbox où j’ai enlevé
la partie provisioning Ansible (la partie configurant le cluster)
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
base_ip_str = "10.240.0.1"
number_master = 1 # Number of master nodes kubernetes
cpu_master = 2
mem_master = 1792
number_worker = 1 # Number of workers nodes kubernetes
cpu_worker = 1
mem_worker = 1024
config.vm.box = "generic/ubuntu2004" # Image for all installations
kubectl_version = "1.23.1-00"
kube_version = "1.23.1-00"
docker_version = "5:20.10.12~3-0~ubuntu-focal"
# Compute nodes
number_machines = number_master + number_worker
nodes = []
(0..number_machines).each do |i|
case i
when 0
nodes[i] = {
"name" => "controller",
"ip" => "#{base_ip_str}#{i}"
}
when 1..number_master
nodes[i] = {
"name" => "master#{i}",
"ip" => "#{base_ip_str}#{i}"
}
when number_master..number_machines
nodes[i] = {
"name" => "worker#{i-number_master}",
"ip" => "#{base_ip_str}#{i}"
}
end
end
# Provision VM
nodes.each do |node|
config.vm.define node["name"] do |machine|
machine.vm.hostname = node["name"]
machine.vm.provider "libvirt" do |lv|
if (node["name"] =~ /master/)
lv.cpus = cpu_master
lv.memory = mem_master
else
lv.cpus = cpu_worker
lv.memory = mem_worker
end
end
machine.vm.synced_folder '.', '/vagrant', disabled: true
machine.vm.network "private_network", ip: node["ip"]
machine.vm.provision "ansible" do |ansible|
ansible.playbook = "playbooks/provision.yml"
ansible.groups = {
"masters" => ["master[1:#{number_master}]"],
"workers" => ["worker[1:#{number_worker}]"],
"kubernetes:children" => ["masters", "workers"],
"all:vars" => {
"base_ip_str" => "#{base_ip_str}",
"kubectl_version" => "#{kubectl_version}",
"kube_version" => "#{kube_version}",
"docker_version" => "#{docker_version}",
"number_master" => "#{number_master}"
}
}
end
end
end
end
Lancement du playbook avec ansible-runner :
ansible-runner run ./ --inventory .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory --container-image my-custom-ee:latest -p playbooks/init-cluster.yml
PLAY [masters[0]] **************************************************************
TASK [Check that cluster is not yet initialized] *******************************
ok: [master1]
TASK [Launch kubeadm init] *****************************************************
changed: [master1]
...
PLAY RECAP *********************************************************************
controller : ok=12 changed=8 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
master1 : ok=8 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
worker1 : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Tout s’est passé de manière impeccable alors que je n’ai pas utilisé d’environnement virtuel ansible comme d’habitude. Ansible n’était pas accessible depuis le répertoire où je l’ai lancé.
Ce qui est intéressant, c’est de voir qu’il génère un dossier artifacts
contenant tout ce qui s’est passé durant l’exécution du playbook. Et c’est là
qu’entre jeu un autre outil ansible :
ansible-navigator
Pour rappel cet outil permet de lancer et d’analyser les artifacts
produits
par ansible. D’ailleurs il existe des paramètres permettant d’indiquer quelle
est l’image d’environnement d’exécution.
Le fichier ansible-navigator.yml
---
ansible-navigator:
execution-environment:
enabled: True
# environment-variables:
# pass:
# - ONE
# - TWO
# - THREE
# set:
# KEY1: VALUE1
# KEY2: VALUE2
# KEY3: VALUE3
image: my-custom-ee
pull-policy: never
# volume-mounts:
# - src: "/test1"
# dest: "/test1"
# label: "Z"
# container-options:
# - "--net=host"
ansible:
inventories:
- .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
playbook: playbooks/init-cluster.yml
logging:
level: debug
editor:
command: code-server {filename}
console: false
playbook-artifact:
enable: True
replay: artifacts/ansible_artifact.json
save-as: artifacts/ansible_artifact.jsonl
Pour le moment je n’ai pas réussi à le faire fonctionner, mais dès que je trouve
la solution, je la publierai. Mon erreur : Please enter a valid file path
Plus loin avec ansible-runner
Le gain que je vois tout de suite :
- la simplification de la construction des nœuds d’exécution des playbooks
- l'immutabilité des images produites
Je vois tout de suite l’intégration d'ansible-builder
et d'ansible-runner
dans mes chaines de CI/CD. On pourrait par exemple utiliser
renovate couplé à du
terraform pour construire
un environnement où on testerait qu’on peut toujours construire notre
infrastructure avec ces nouvelles versions de dépendances. Un vaste chantier.
ansible-runner
regorge de paramètres qui permettent de sécuriser son
fonctionnement, d’envoyer les artifacts sur des systèmes distants, de lancer des
exécutions sur des machines distantes, de le lancer depuis le code source de
vos applications python, …. Je vous renvoie à sa
documentation
Voyons la mise en pratique des environnements d’exécution sur Ansible AWX.