Ansible - Les Environnements d'Exécution
Publié le : 1 février 2022 | Mis à jour le : 22 janvier 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: 1
build_arg_defaults:
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:stable-2.10-devel'
ansible_config: 'ansible.cfg'
dependencies:
galaxy: requirements.yml
python: requirements.txt
system: bindep.txt
additional_build_steps:
prepend: |
RUN whoami
RUN cat /etc/os-release
append:
- RUN echo This is a post-install command!
- RUN ls -la /etc
build_arg_defaults : permet de définir les images de base, EE_BASE_IMAGE
et de build EE_BUILDER_IMAGE
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_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.
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.
Construction d’un environnement d’exécution
Pour retrouver les images, il suffit de se rendre sur cette page de quay.io.
En cliquant sur l’icône de téléchargement, vous pourrez générer la commande
docker pull
avec son tag ou son SHA (ma préférence).
docker pull quay.io/ansible/ansible-runner@sha256:1fa60288b1686946783e0ac864a29fa97dfcf0d5776ca6fda77c41fe8f880587
Je vais prendre comme exemple la création d’une image permettant d’utiliser la collection k8s pour construire et piloter un cluster kubernetes.
---
version: 1
build_arg_defaults:
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner@sha256:1fa60288b1686946783e0ac864a29fa97dfcf0d5776ca6fda77c41fe8f880587'
ansible_config: 'ansible.cfg'
dependencies:
galaxy: requirements.yml
python: requirements.txt
additional_build_steps:
prepend: |
RUN cat /etc/os-release
Le fichier ansible.cfg :
[defaults]
strategy=linear
inventory_file = .vagrant/provisioners/ansible/inventory
pipelining = True
gathering = smart
fact_caching_connection = /tmp/facts_cache
fact_caching = jsonfile
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
PreferredAuthentications=publickey%
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
Le fichier requirements.txt
kubernetes==21.7.0
Construction de l’image :
ansible-builder build --tag=my-custom-ee --verbosity 2
Running command:
docker build -f context/Dockerfile -t ansible-execution-env:latest context
Complete! The build context can be found at: /home/vagrant/Projets/veille/test-ansible-runner/ansible-builder/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=quay.io/ansible/ansible-runner@sha256:1fa60288b1686946783e0ac864a29fa97dfcf0d5776ca6fda77c41fe8f880587
ARG EE_BUILDER_IMAGE=quay.io/ansible/ansible-builder:latest
FROM $EE_BASE_IMAGE as galaxy
ARG ANSIBLE_GALAXY_CLI_COLLECTION_OPTS=
USER root
ADD _build/ansible.cfg ~/.ansible.cfg
ADD _build /build
WORKDIR /build
RUN ansible-galaxy role install -r requirements.yml --roles-path /usr/share/ansible/roles
RUN 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
RUN cat /etc/os-release
COPY --from=galaxy /usr/share/ansible /usr/share/ansible
COPY --from=builder /output/ /output/
RUN /output/install-from-bindep && rm -rf /output/wheels
On remarque qu’il utilise le multi-stage pour limiter la taille de l’image
qui est tout de même de 910 MB. 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.
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.