Aller au contenu principal

Les Environnements d'Exécution Ansible

· 11 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Les 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

  1. 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.