Maîtriser les workflows Ansible AWX
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 :
https://github.com/stephrobert/test-awx-workflow.gitcd 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 ):
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 providerprovider "libvirt" { // uri = "qemu:///system" uri = "qemu+ssh://root@devbox.robert.local/system"}
// variables that can be overridenvariable "hostname" { default = "test" }variable "domain" { default = "example.com" }variable "ip_type" { default = "dhcp" } # dhcp is other valid typevariable "memoryMB" { default = 1024*1 }variable "cpu" { default = 1 }
// fetch the latest ubuntu release image from their mirrorsresource "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 instanceresource "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 machineresource "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: trueusers: - 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: truedisable_root: falsechpasswd: list: | ubuntu:linux expire: Falsepackages: - qemu-guest-agent - python3bootcmd: - [ 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: 2ethernets: ens3: dhcp4: true
Avec ces trois fichiers, on peut déjà créé la VM avec :
terraform initterraform 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.
---- 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é
- un de type
- 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].
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.
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).
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 !!!