Déployer un projet Docker avec Ansible et Swarm

Cet article est accessible en avance mais peut contenir des parties incomplètes.
Ansible et Docker occupent une place essentielle dans la gestion et le déploiement de mes projets sur mes serveurs. Pour automatiser et fiabiliser mes déploiements, j’ai conçu un playbook Ansible qui me permet de lancer mes applications Docker.

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 :

  1. Prépatation : Création des volumes, authentification, récupération de l'image et inspection des métadonnées ;
  2. Déploiement : Création éventuelle du réseau, copie du docker-compose.yml et lancement de la stack ;
  3. 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

Cet article vous a plu ? N'hésitez pas à le partager:

Découvrir d’autres articles