Déployer un projet Docker avec Ansible et Swarm
J'ai conçu mon script Ansible qui prend en compte plusieurs paramètres, afin de rendre le déploiement flexible. Ce script n'est pas définitif et rien ne garanti qu'il s'adapte à tous les besoins. Je le fais évoluer au fil de mes besoins.
J'ai fait le choix de ne pas utiliser, pour le moment, les modules de la communauté. Les tâches que j'effectue restent assez simple, je préfère donc connaitre le code qui se trouve derrière. J'évite aussi d'avoir une dépendance supplémentaire et dans le temps un paquet non supporté. A l'inverse, le jour ou Docker change de comportement, je devrais mettre à jour mon code ou l'adapter suivant la version.
Les paramètres de mon playbook
Aujourd'hui, le script prend des paramètres obligatoires et optionnels.
Paramètres obligatoires :
- compose_file : chemin du fichier docker-compose.yml à déployer ;
- stack_name : Nom de la stack Swarm à créer ;
Paramètres optionnels :
- docker_volumes : liste de volumes à créer sur le serveur, séparés par ";" ;
- docker_registry, docker_registry_username, docker_registry_token : permettent de s’authentifier auprès d’un registre privé pour récupérer l’image Docker ;
- docker_image : image Docker à télécharger avant d’exécuter le docker-compose. Si la valeur est définie, elle sera exportée sous la variable d'environnement D_IMAGE dans le docker-compose ;
- docker_network : nom du réseau overlay à créer pour la stack ;
Présentation du playbook
Mon playbook est découpé en trois grandes parties :
- Prépatation : Création des volumes, authentification, récupération de l'image et inspection des métadonnées ;
- Déploiement : Création éventuelle du réseau, copie du docker-compose.yml et lancement de la stack ;
- Nettoyage : Suppression des anciennes images et conteneurs obsolètes pour libérer de l'espace disque.
Le playbook complet
Voici le playbook complet :
Fichier : deploy_on_swarm.yaml (yml)1--- 2- name: Prepare Docker Compose on Swarm 3 hosts: all 4 become: yes 5 gather_facts: no 6 tasks: 7 - name: Créer les dossiers 8 file: 9 path: "{{ item | trim }}" 10 state: directory 11 mode: "0755" 12 loop: "{{ docker_volumes.split(';') }}" 13 when: docker_volumes is defined 14 15 - name: Authentification au registre 16 docker_login: 17 registry_url: "{{ docker_registry }}" 18 username: "{{ docker_registry_username }}" 19 password: "{{ docker_registry_token }}" 20 reauthorize: true 21 when: docker_registry is defined and docker_registry_username is defined and docker_registry_token is defined 22 no_log: true 23 24 - name: Récupération de l'image 25 command: docker pull {{ docker_image }} 26 when: docker_image is defined 27 28 - name: Récupération des métadonnées de l’image 29 command: docker image inspect {{ docker_image }} 30 when: docker_image is defined 31 register: image_metadata 32 33 - name: Parser les métadonnées de l’image 34 set_fact: 35 image_data: "{{ image_metadata.stdout | from_json | first }}" 36 when: docker_image is defined 37 38 - name: Extraire le digest et la date de création 39 set_fact: 40 image_digest: "{{ image_data.RepoDigests[0] }}" 41 image_created: "{{ image_data.Created }}" 42 when: docker_image is defined 43 44 - name: Afficher le digest et la date de création dans la console 45 debug: 46 msg: | 47 Digest de l'image : {{ image_digest }} 48 Date de création : {{ image_created }} 49 when: docker_image is defined 50 51 - name: De-authentification au registre 52 docker_login: 53 state: absent 54 when: docker_registry is defined 55 56- name: Deploy Docker Compose on Swarm 57 hosts: master 58 become: yes 59 gather_facts: no 60 tasks: 61 - name: Create Docker network if specified 62 docker_network: 63 name: "{{ docker_network }}" 64 driver: overlay 65 attachable: true 66 when: docker_network is defined 67 68 - name: Copy Docker Compose file to master node 69 copy: 70 src: "{{ compose_file }}" 71 dest: "~/{{ stack_name }}.yml" 72 73 - name: Deploy Docker Compose stack 74 #docker_stack: 75 # state: present 76 # name: "{{ stack_name }}" 77 # compose: 78 # - ~/{{ stack_name }} 79 shell: > 80 {{ "D_IMAGE=" + docker_image + " " if docker_image is defined else "" }} 81 docker stack deploy --compose-file ~/{{ stack_name }}.yml {{ stack_name }} 82 args: 83 executable: /bin/bash 84 85- name: Nettoie le disque 86 hosts: all 87 become: yes 88 gather_facts: no 89 tasks: 90 - name: Récupérer les id des anciennes versions de l'image Docker associées à "{{ docker_image }}" 91 shell: | 92 docker images "{{ docker_image | regex_replace(':[^:]+$', '') }}" --format "{{ '{{.Repository}}:{{.Tag}} {{.ID}}' }}" | grep "<none>" | awk '{print $2}' 93 register: old_images 94 when: docker_image is defined 95 ignore_errors: yes 96 97 - name: Supprimer les conteneurs utilisant les anciennes images de "{{ docker_image }}" 98 shell: | 99 for img in {{ old_images.stdout_lines | join(' ') }}; do 100 docker ps -a --filter "ancestor=$img" --format "{{ '{{.ID}}' }}" | xargs -r docker rm 101 done 102 when: docker_image is defined and old_images.stdout_lines | length > 0 103 ignore_errors: yes 104 105 - name: Supprimer les anciennes images de "{{ docker_image }}" 106 shell: | 107 for img in {{ old_images.stdout_lines | join(' ') }}; do 108 docker rmi $img 109 done 110 when: docker_image is defined and old_images.stdout_lines | length > 0 111 ignore_errors: yes
Exemple d'utilisation avec Terraform/OpenTofu
Fichier : project.tf (hcl)1variable "gitlab_registry_blog_user" { 2 type = string 3 description = "Project registry username" 4 sensitive = true 5 nullable = false 6} 7 8variable "gitlab_registry_blog_token" { 9 type = string 10 description = "Project registry token" 11 sensitive = true 12 nullable = false 13} 14 15variable "gitlab_registry_blog_registry" { 16 type = string 17 description = "Registry" 18 default = "registry.gitlab.com" 19} 20 21variable "gitlab_registry_blog_image" { 22 type = string 23 description = "Image" 24 default = "blog:latest" 25} 26 27resource "null_resource" "run_ansible_swarm_deploy_blog" { 28 depends_on = [local_file.ansible_inventory, null_resource.run_ansible_swarm_deploy_traefik] 29 30 triggers = { 31 instances_id_hash = md5(join("-", [ 32 for i in range(length(openstack_compute_instance_v2.swarm_node)) : 33 openstack_compute_instance_v2.swarm_node[i].id 34 ])) 35 deploy_swarm_hash = md5(filesha256("ansible/deploy_on_swarm.yaml")) 36 blog_compose_hash = md5(filesha256("blog.compose.yaml")) 37 gitlab_registry_blog_image = var.gitlab_registry_blog_image 38 } 39 40 provisioner "local-exec" { 41 working_dir = "ansible" 42 command = "ansible-playbook --private-key ${var.private_key_path} deploy_on_swarm.yaml -e 'compose_file=blog.compose.yaml stack_name=blog docker_network=traefik-public-network docker_volume=/mnt/swarm_data/blog_files/db_files/;/mnt/swarm_data/blog_files/uploads_files/ docker_registry=${var.gitlab_registry_blog_registry} docker_registry_username=${var.gitlab_registry_blog_user} docker_registry_token=${var.gitlab_registry_blog_token} docker_image=${var.gitlab_registry_blog_registry}/${var.gitlab_registry_blog_image}'" 43 } 44}
Conclusion
Avec ce playbook, je peux :
- déployer rapidement une stack Docker sur Swarm
- tracer les images utilisées (digest + date de build)
- garder mes serveurs propres grâce au nettoyage automatique
Ce script est une base que je continue d’améliorer. L’idée est d’automatiser un maximum les étapes manuelles du déploiement, tout en restant lisible et adaptable.
― Valentin LORTET