Aller au contenu

Maîtriser les workflows Ansible AWX

logo ansible

Les workflows disponibles sur Ansible Tower (depuis la version 3.1) permettent aux utilisateurs de créer des séquences à partir de plusieurs ressources Ansible : playbooks, synchronisation de projet, autres workflows, approbations, …

Préparation d’un workflow Ansible AWX

Il faut avant créer un projet contenant tout ce qu’il faut pour construire notre infrastructure. Je vous fournis un exemple qui permet de provisionner une machine sur un cluster libvirt, de l’enregistrer dans un inventaire (donc dynamique), de la tester, pour finir par la détruire.

Clonons le projet :

Terminal window
https://github.com/stephrobert/test-awx-workflow.git
cd test-awx-workflow

Construction de l’environnement d’exécution

Je vais bien sûr créer mon image d’environnement d’exécution intégrant tous les modules et les modules python nécessaires. L’environnement se trouve dans le répertoire EE/terraform. Il contient un fichier de déclaration d’environnement permettant de spécifier comment sera construite l’image :

  • un fichier execution-environment.yml
---
version: 1
build_arg_defaults:
EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner@sha256:1fa60288b1686946783e0ac864a29fa97dfcf0d5776ca6fda77c41fe8f880587'
dependencies:
galaxy: requirements.yml
python: requirements.txt
additional_build_steps:
prepend: |
RUN yum install -y yum-utils genisoimage
RUN yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
RUN yum -y install terraform

J’ai ajouté l’installation de terraform et la commande mkisofs. mkisofs permet de créer les isos lors de la construction de la machine dans le cluster libvirt.

  • un fichier requirements.txt
python-terraform==0.10.1

Nécessaire au fonctionnement du module Ansible terraform

  • un fichier requirements.yml
---
collections:
- name: community.general
version: 4.4.0
- name: awx.awx
version: 19.4.0

Je vais utiliser le module awx pour créer l’inventaire dynamique. J’ai dû recourir à ce fonctionnement, car j’aurais pu utiliser l’inventaire terraform mais un bug du provider libvirt empêche de récupérer l’IP de la VM provisionnée.

Pour construire l’image et la pousser dans votre registry docker (changer le tag ):

Terminal window
ansible-builder build --tag=devbox2.robert.local:31320/ee-terraform:1.0.0
docker push devbox2.robert.local:31320/ee-terraform:1.0.0

Ecriture du Code Terraform

Je créé une machine virtuelle en utilisant un cluster libvirt distant. Dans la partie provider je déclare donc un uri qemu+ssh://root@devbox.robert.local/system qui utilise une connexion ssh.

La VM est construire à partir d’une image Ubuntu téléchargé à la volée.

terraform {
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
}
}
}
// instance the provider
provider "libvirt" {
// uri = "qemu:///system"
uri = "qemu+ssh://root@devbox.robert.local/system"
}
// variables that can be overriden
variable "hostname" { default = "test" }
variable "domain" { default = "example.com" }
variable "ip_type" { default = "dhcp" } # dhcp is other valid type
variable "memoryMB" { default = 1024*1 }
variable "cpu" { default = 1 }
// fetch the latest ubuntu release image from their mirrors
resource "libvirt_volume" "os_image" {
name = "${var.hostname}-os_image"
pool = "default"
source = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img"
format = "qcow2"
}
// Use CloudInit ISO to add ssh-key to the instance
resource "libvirt_cloudinit_disk" "commoninit" {
name = "${var.hostname}-commoninit.iso"
pool = "default"
user_data = data.template_cloudinit_config.config.rendered
network_config = data.template_file.network_config.rendered
}
data "template_file" "user_data" {
template = file("${path.module}/cloud_init.cfg")
vars = {
hostname = var.hostname
fqdn = "${var.hostname}.${var.domain}"
public_key = file("~/.ssh/id_ed25519.pub")
}
}
data "template_cloudinit_config" "config" {
gzip = false
base64_encode = false
part {
filename = "init.cfg"
content_type = "text/cloud-config"
content = "${data.template_file.user_data.rendered}"
}
}
data "template_file" "network_config" {
template = file("${path.module}/network_config_${var.ip_type}.cfg")
}
// Create the machine
resource "libvirt_domain" "domain-ubuntu" {
# domain name in libvirt, not hostname
name = "${var.hostname}"
memory = var.memoryMB
vcpu = var.cpu
disk {
volume_id = libvirt_volume.os_image.id
}
network_interface {
network_name = "bridged-network"
}
cloudinit = libvirt_cloudinit_disk.commoninit.id
# IMPORTANT
# Ubuntu can hang is a isa-serial is not present at boot time.
# If you find your CPU 100% and never is available this is why
console {
type = "pty"
target_port = "0"
target_type = "serial"
}
graphics {
type = "spice"
listen_type = "address"
autoport = "true"
}
}
terraform {
required_version = ">= 0.12"
}
output "ips" {
#value = libvirt_domain.domain-ubuntu
#value = libvirt_domain.domain-ubuntu.*.network_interface
# show IP, run 'terraform refresh' if not populated
value = libvirt_domain.domain-ubuntu.*.network_interface
}

J’ai créé aussi une ressource cloud-init qui se charge de configurer la machine, mais surtout d’installer les packages python3 et qemu-guest-agent. C’est lui qui va permettre de récupérer l’adresse IP de la machine :

hostname: ${hostname}
fqdn: ${fqdn}
manage_etc_hosts: true
users:
- name: ubuntu
sudo: ALL=(ALL) NOPASSWD:ALL
groups: users, admin
home: /home/ubuntu
shell: /bin/bash
lock_passwd: false
ssh-authorized-keys:
- ${public_key}
ssh_pwauth: true
disable_root: false
chpasswd:
list: |
ubuntu:linux
expire: False
packages:
- qemu-guest-agent
- python3
bootcmd:
- [ sh, -c, 'echo $(date) | sudo tee -a /root/bootcmd.log' ]
runcmd:
- [ sh, -c, 'echo $(date) | sudo tee -a /root/runcmd.log' ]
final_message: "The system is finall up, after $UPTIME seconds"

Pour la partie réseau, j’ai modifié l’interface réseau pour qu’elle utilise un pont. Cela permet à ma VM d’être accessible depuis les autres machines de mon réseau. Du coup l’adresse IP est géré par mon serveur DHCP.

version: 2
ethernets:
ens3:
dhcp4: true

Avec ces trois fichiers, on peut déjà créé la VM avec :

Terminal window
terraform init
terraform apply --auto-approve

Construction des playbooks Ansible

Maintenant que ma VM se créé je vais pouvoir écrire les playbooks qui seront utilisés dans le workflow :

Le premier terraform-deploy.yml :

---
- name: Deploy Stack
hosts: all
become: false
gather_facts: false
vars:
known_hosts: |
devbox.robert.local,192.168.1.42 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIpDg+z8B5AWiJF89PRzSppCE3aW8czN+8GE9Cc8vOJ9JK2fut9of62+Zz+7cqtWz2KXnRdQbrJymIkdYoQY8Wc=
id_ed25519: !vault |
$ANSIBLE_VAULT;1.1;AES256
38666539653431373639613935323362303438373664636363393564313663356263356164396265
6364306534383365313331313233303762613034323234630a343661303631623731333636356531
31663264643337346636643364393263306235663537343166336330616538626535393733373738
6363343336336561610a353039363135623839616162656230646233316430373233366130646362
33623864343237363739666663623231363933383763316166383466326434653136653636323665
38373936653263396334343932633030633332643838666139636437626663313263663338643836
30326136353965323639653965633336633465616661613134366532333266363634653266623633
62346433633138333264636236646465646631343861643164643666633338373766303665323265
34356663353434373431646162636131623662643536353664366537383162636264363931646433
36326330616465646563653937363361313436643739306565363533616134316265613462373333
32666332336261646662636238333733393635343463346163313339633961363061383263636635
65343133373430616665663736646333326435633337313966366232663233386664633864313066
65653931303666643332303266383138393063386336316162613633343634333130663330626337
33393263373263636639663364666363363662633561376134653634303330646364346531643639
64643133633139666433663464613639333064393531376338333666656236346638326166666338
65653533356132316365333765333138303531336266306639643333646138363366373931303064
61373031316338663534356364323734363536343931386665383265643766313964346639656637
32313737373038663561303366623637373235303836393639643166633635303064333762383230
36636130303663366563383561353535313632613664333430353136633334363165303339613936
34366234666635316132363162626437643332613338313433383465386430613131343666623239
36653730363030646639633164613464653766303338623461633630396330383332336538656230
31313361663066623934333434303337633831633066623539663034353663343066616363316465
34316136613066613234616535653938663065353966366533323236346562303162393263356131
34373837353033623965613361653662346363393833623662326565616538653764303935643531
3933
tasks:
- name: create .ssh directory
ansible.builtin.file:
path: $HOME/.ssh
state: directory
mode: 0755
delegate_to: localhost
- name: Create file id_ed25519
ansible.builtin.copy:
dest: $HOME/.ssh/id_ed25519.pub
content: "{{ id_ed25519 }}"
mode: 0644
delegate_to: localhost
- name: Ensure ssh host key known
ansible.builtin.copy:
dest: $HOME/.ssh/known_hosts
content: "{{ known_hosts }}"
mode: 0600
delegate_to: localhost
- name: Deploy VM
community.general.terraform:
project_path: './'
state: present
force_init: true
delegate_to: localhost
- name: Store tfstate
ansible.builtin.copy:
src: terraform.tfstate
dest: ~/terraform.tfstate
mode: 0644

Vous remarquez que j’ai inclus des variables dont une que j’ai encrypté avec la commande ansible-vault. Il s’agit de la clé publique SSH permettant de me connecter à mon cluster libvirt.

Comme tout se passe dans l’environnement d’exécution je reconstruis le répertoire .ssh dans le container qui contient la clé publique et le fichier known_hosts (sinon le provider libvirt se plante).

Ensuite je fais appel au module ansible terraform qui lance les deux phases init et apply. Je finis par stocker le state terraform, pour le restaurer lors de la destruction de la VM.

Le second playbook create-inventory.yml :

---
- name: Test
hosts: all
become: false
gather_facts: false
vars:
token: !vault |
$ANSIBLE_VAULT;1.1;AES256
33653263303262396336316133343531306639333732366634323331356163393136653864613865
3132613930323166666534383431363063326362353634330a616531366634613939366237306534
36613963393764366135383664343765313933633364346337383830313762316637663562333932
3339643033323236330a323065313632653637346561356337633639643336386561333937326433
31653062393135643132353665383332333836643263363437333338643538653733
tasks:
- name: Get info from VM
ansible.builtin.shell: "virsh -c qemu+ssh://root@devbox.robert.local/system qemu-agent-command test '{\"execute\":\"guest-network-get-interfaces\"}'"
register: info
changed_when: info.rc != 0
retries: 15
delay: 10
until: info is success
- name: register value
ansible.builtin.set_fact:
ip_address: "{{ (info.stdout | from_json).return[1]['ip-addresses'][0]['ip-address'] }}"
- name: Delete Inventory
awx.awx.inventory:
controller_host: "http://192.168.1.74:30932"
controller_oauthtoken: "{{ token }}"
controller_username: "admin"
name: "Test"
state: absent
organization: "Default"
validate_certs: false
- name: Create Inventory
awx.awx.inventory:
controller_host: "http://192.168.1.74:30932"
controller_oauthtoken: "{{ token }}"
controller_username: "admin"
name: "Test"
description: "My first Servers"
state: present
organization: "Default"
validate_certs: false
- name: Add Host to Inventory
awx.awx.host:
controller_host: "http://192.168.1.74:30932"
controller_oauthtoken: "{{ token }}"
controller_username: "admin"
inventory: "Test"
description: "My first Servers"
state: present
name: "{{ ip_address }}"
enabled: true
validate_certs: false
- name: Add inventory to Job template Test
awx.awx.job_template:
name: test-VM
inventory: Test

Ce playbook récupère l’adresse IP de la VM en utilisant virsh en mode commande : virsh qemu-agent-command test '{"execute":"guest-network-get-interfaces"}' Cela retourne un json dont j’extrais l’IP. Si vous lisez attentivement j’ai ajouté la relance de cette tache jusqu’à ce qu’elle s’exécute avec succès (max 15 fois). Et ou le provisionnement n’est pas instantané.

Ensuite je fais appel au module awx.awx pour détruire l’ancien inventaire, le recréer avec le host dont j’ai récupéré l’IP et l’ajouter au modèle de job test-vm (comme il est détruit et recréé il n’a plus le même ID).

  • Le troisième playbook test.yml est un simple ping.
Terminal window
---
- name: Test Stack
hosts: all
become: false
gather_facts: false
tasks:
- name: Ping
ansible.builtin.ping:

Le dernier terraform-destroy :

---
- name: Deploy Stack
hosts: all
become: false
gather_facts: false
vars:
known_hosts: |
devbox.robert.local,192.168.1.42 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIpDg+z8B5AWiJF89PRzSppCE3aW8czN+8GE9Cc8vOJ9JK2fut9of62+Zz+7cqtWz2KXnRdQbrJymIkdYoQY8Wc=
id_ed25519: !vault |
$ANSIBLE_VAULT;1.1;AES256
38666539653431373639613935323362303438373664636363393564313663356263356164396265
6364306534383365313331313233303762613034323234630a343661303631623731333636356531
31663264643337346636643364393263306235663537343166336330616538626535393733373738
6363343336336561610a353039363135623839616162656230646233316430373233366130646362
33623864343237363739666663623231363933383763316166383466326434653136653636323665
38373936653263396334343932633030633332643838666139636437626663313263663338643836
30326136353965323639653965633336633465616661613134366532333266363634653266623633
62346433633138333264636236646465646631343861643164643666633338373766303665323265
34356663353434373431646162636131623662643536353664366537383162636264363931646433
36326330616465646563653937363361313436643739306565363533616134316265613462373333
32666332336261646662636238333733393635343463346163313339633961363061383263636635
65343133373430616665663736646333326435633337313966366232663233386664633864313066
65653931303666643332303266383138393063386336316162613633343634333130663330626337
33393263373263636639663364666363363662633561376134653634303330646364346531643639
64643133633139666433663464613639333064393531376338333666656236346638326166666338
65653533356132316365333765333138303531336266306639643333646138363366373931303064
61373031316338663534356364323734363536343931386665383265643766313964346639656637
32313737373038663561303366623637373235303836393639643166633635303064333762383230
36636130303663366563383561353535313632613664333430353136633334363165303339613936
34366234666635316132363162626437643332613338313433383465386430613131343666623239
36653730363030646639633164613464653766303338623461633630396330383332336538656230
31313361663066623934333434303337633831633066623539663034353663343066616363316465
34316136613066613234616535653938663065353966366533323236346562303162393263356131
34373837353033623965613361653662346363393833623662326565616538653764303935643531
3933
tasks:
- name: create .ssh directory
ansible.builtin.file:
path: $HOME/.ssh
state: directory
mode: 0755
delegate_to: localhost
- name: Ensure ssh host key known
ansible.builtin.copy:
dest: $HOME/.ssh/known_hosts
content: "{{ known_hosts }}"
mode: 0600
delegate_to: localhost
- name: Create file id_ed25519
ansible.builtin.copy:
dest: $HOME/.ssh/id_ed25519.pub
content: "{{ id_ed25519 }}"
mode: 0644
delegate_to: localhost
- name: Restore tfstate
ansible.builtin.fetch:
src: ~/terraform.tfstate
dest: ./
flat: true
validate_checksum: false
- name: Destroy VM
community.general.terraform:
project_path: './'
state: absent
check_destroy: true
force_init: true
delegate_to: localhost

Ce playbook est identique à celui du deploy sauf bien sur le status du module terraform qui est à absent. Comme cela tourne dans un container, il faut tout reconstruire et récupérer le state terraform.

Construction des ressources AWX

Nous avons tout le code. Maintenant créons ce qu’il faut dans AWX pour en faire un workflow. Pour cette partie je vous renvoie au billet sur les premiers pas avec AWX, car cela ferait trop de copies d’écran.

Il faut créer les :

  • un environnement d'exécution pointant sur l’image qui a été poussé dans la registry docker.
  • une ressource de type projet qui pointe sur le projet donné en exemple.
  • une ressource de type informations d'identifications :
    • un de type Vault contenant le mot de passe du vault
    • un de type Machine pour se connecter au cluster libvirt
    • un autre de type Machine pour se connecter à la VM provisionné
  • Une ressource de type inventaire pointant sur le cluster libvirt.
  • Des ressources de type Modèle de Job pointant sur les playbooks :
    • deploy-vm avec inventaire cluster libvirt avec la clé SSH et le mdp vault dans les informations d’identifications
    • create-inventory comme le précédent
    • test-vm avec comme inventaire Test
    • destroy-vm configuré comme le deploy.

Construction du Workflow AWX

Maintenant que nous avons tout ce qu’il nous faut créons le Worflow.

Dans [Ressources / Modèles] Ajouter un modèle de flux de Travail

  • nom : le nom que vous voulez
  • inventaire : celui du cluster libvirt

Pour finir Cliquez sur [Enregistrer].

Ansible awx Workflow

Cliquez désormais sur [Visualiseur]. C’est ici que l’on va dessiner le WorkFlow :

En déplaçant la souris sur [Démarrer] vous devriez voir apparaitre un [+]. Cliquez dessus. Dans [type de Noeud] prenez Sync Projet et sélectionnez votre projet.

Ansible awx Workflow

De la même façon déplacer la souris au-dessus de la première étape et cliquez sur [+]. Et là vous voyez apparaître des choix sur les conditions d’exécution de ce job. Prenez [En cas de succès] puis [Suivant]. On va prendre un Type de Noeud [Modèle de job] et le premier job est [deploy-vm].

Répétez l’opération jusqu’à obtenir ce workflow. Il est possible d’ajouter des relations en cliquant sur les deux petits anneaux et en pointant sur le noeud destinataire. Vous pourrez alors choisir le type de condition : [En cas d’échec] (rouge) et [Toujours] (bleu).

Ansible awx Workflow

Pour lancer le workflow il suffit de cliquer sur la petite fusée en haut à droite de la partie visualizer.

Je dois encore corriger la partie ajout dans l’inventaire. Mais cela permet de valider qu’en cas d’échec de ce playbook la vm est détruite. Cool

Conclusion

Les gros avantages que j’ai trouvés :

  • Cela permet de ne pas avoir besoin d’écrire de gros playbooks monolithiques. Au lieu d’enchainer les rôles les uns derrières les autres on découpe en tâches plus unitaires.
  • Prise en charge de l’état en échec à la sortie d’un job
  • Possiblité de mettre des approbations.

Cet exemple n’est pas parfait, mais j’espère que cela vous aura permis de voir tout le potentiel de cet outil qu’est Ansible AWX. J’ai encore pas mal de travail de découvertes à faire dessus.

Je me souviens des premières versions où je n’avais vu aucun bénéfice à son utilisation. Ce n’est plus le cas désormais !!!