Aller au contenu principal

Optimiser les temps d'exécution d'Ansible

Quand on développe du code Ansible, rien de plus agaçant que d'attendre la fin de l'exécution. Autre problème comment limiter autant que possible l'indisponibilité d'une application en optimisant l'exécution de vos playbooks Ansible ? Voyons comment résoudre ces problèmes. Ce sera aussi le moyen de vérifier que les anciennes recommandations sont toujours d'actualité.

Mise en place du lab

Je vais utiliser terraform pour provisionner 6 machines virtuelles tournant sur ubuntu 22.04 chez AWS avec juste des différences en termes de capacités (cpu et mémoire). Sur ces machines, je vais utiliser des rôles fournis par geerlingguy, en l'occurrence celui permettant d'installer mysql et apache.

Pour ceux qui veulent reproduire les tests je vous fournis le code :

Le fichier amin.tf :

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = var.aws_zone
}

variable "aws_project" {
  type        = string
  default     = "test-ansible"
  description : "The aws project to launch test into."
}

variable "aws_zone" {
  type        = string
  default     = "eu-west-3"
  description : "The aws region to deploy instances."
}

variable "SSH_KEY" {
  type      = string
}


data "aws_ami" "latest-ubuntu" {
  most_recent = true

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  filter {
    name = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy**amd64-server*"]
  }
}

resource "aws_key_pair" "ssh-key" {
  key_name   = "ssh-key"
  public_key = file("~/.ssh/id_ed25519.pub")
}

resource "aws_instance" "test1" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t2.small"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test1"
  }
}

resource "aws_instance" "test2" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t3.micro"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test2"
  }
}

resource "aws_instance" "test3" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t2.micro"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test3"
  }
}

resource "aws_instance" "test4" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t2.small"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test4"
  }
}

resource "aws_instance" "test5" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t3.micro"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test5"
  }
}

resource "aws_instance" "test6" {
  ami           = data.aws_ami.latest-ubuntu.id
  instance_type = "t2.micro"
  associate_public_ip_address = true
  key_name      = aws_key_pair.ssh-key.key_name
  tags = {
    Name = "test6"
  }
}


resource "aws_default_vpc" "default" {
  tags = {
    Name = "Default VPC"
  }
}

resource "aws_default_security_group" "default" {
  vpc_id = "${aws_default_vpc.default.id}"
  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Le fichier requirements.yml pour installer les rôles :

roles:
- name: geerlingguy.mysql
- name: geerlingguy.apache

On installe les rôles avec la commande ansible-galaxy :

ansible-galaxy install -r requirements.yml

Le playbook qui servira de test :

- name: Installation de mysql avec le role de geerlinguy
  hosts: all
  gather_facts: true
  become: true
  roles:
    - geerlingguy.apache
    - geerlingguy.mysql

Pour la configuration d'ansible, j'ai généré celle par défaut et que j'ai obtenue avec la commande suivante :

ansible-config init --disabled -t all > ansible.cfg

Le fichier de configuration ansible.cfg obtenue contient tous les paramètres en commentaires. Pour chaque run, je ne donnerai que ceux que j'ai activés. Donc voici celui de la configuration du premier run, intégrant que les modifications nécessaires au fonctionnement du lab et donc sans optimisations :

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False

Prise de mesures initiales

Pour mesurer le temps pris par chacune des taches, je vais activer les callbacks suivants :

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles

Pour rappel les callbacks permettent de configurer la sortie des commandes ansible-playbook.

Pour ce premier run, je prends les valeurs par défaut de la configuration ansible. Ce run va nous servir d'étalon pour valider les différentes modifications de paramètres. Pour info, je l'ai lancé 5 fois et j'ai pris le run avec le temps moyen.

Pour chacun des runs suivants, je vais détruire et reconstruire les machines pour que nous nous retrouvions à chaque fois dans les conditions initiales. Pour cela, je vais utiliser l'option -replace de terraform qui remplace désormais la commande taint:

terraform apply -replace=aws_instance.test1 -replace=aws_instance.test2 -replace=aws_instance.test3 -replace=aws_instance.test4 -replace=aws_instance.test5 -replace=aws_instance.test6 --auto-approve

Le premier lancement utilisera une version 3.9.15 de python. Sur les machines cibles, qui tournent sur Ubuntu 22.04, nous aurons droit à une 3.10.8 !

On installe python 3.9.15 avec la dernière version d'ansible avec pyenv sur le contrôleur ansible.

pyenv install 3.9.15
pyenv virtualenv 3.9.15 ansible-python3.9.15
pyenv shell ansible-python3.9.15
pip install ansible boto3

On lance le premier run :

ansible-playbook -u vagrant test-performance.yml

Playbook run took 0 days, 0 hours, 2 minutes, 56 seconds
lundi 27 mars 2023  12:40:49 +0200 (0:00:05.627)       0:02:56.819 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  55.39s
geerlingguy.apache : Update apt cache. :  23.82s
geerlingguy.apache : Ensure Apache is installed on Debian. :  21.20s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  9.03s
Gathering Facts :  5.65s
geerlingguy.mysql : restart mysql :  5.63s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  4.52s
geerlingguy.apache : Get installed version of Apache. :  4.28s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.78s
geerlingguy.apache : Add apache vhosts configuration. :  3.32s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  3.00s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  2.75s
geerlingguy.apache : Enable Apache mods. :  2.70s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  2.55s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  2.27s
geerlingguy.apache : restart apache :  2.08s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.82s
geerlingguy.mysql : Remove MySQL test database. :  1.75s
geerlingguy.mysql : Disallow root login remotely :  1.71s
geerlingguy.mysql : Update MySQL root password for localhost root account (5.7.x). :  1.54s
lundi 27 mars 2023  12:40:49 +0200 (0:00:05.628)       0:02:56.820 ************
===============================================================================
geerlingguy.mysql :  107.06s
geerlingguy.apache :  64.09s
gather_facts :  5.65s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  176.80s

Optimisation des temps d'exécution du playbook

Utiliser des connexions persistantes

L'établissement d'une connexion SSH est un processus relativement lent qui s'exécute en arrière-plan. Le temps d'exécution global augmente considérablement lorsque vous avez plus de tâches dans un playbook et plus de nœuds.

Par défaut, Ansible utilise OpenSSH pour les connexions SSH car il supporte la persistance de connexion via le paramètre ControlPersist. Si votre contrôleur utilise une ancienne version d'OpenSSH Ansible utilisera ‘paramiko’.

Vous pouvez activer les paramètres ControlMaster et ControlPersist pour améliorer les performances.

  • ControlMaster permet à plusieurs sessions SSH avec un hôte distant d'utiliser une seule connexion réseau.
  • ControlPersist indique combien de temps le SSH maintient une connexion inactive ouverte en arrière-plan.

Notre nouveau fichier de configuration ansible.cfg :

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles
[ssh_connection]
# -C permit compression
ssh_args=-o ControlMaster=auto -o ControlPersist=600s

Le résultat du run :

Playbook run took 0 days, 0 hours, 3 minutes, 2 seconds
lundi 27 mars 2023  12:49:38 +0200 (0:00:05.764)       0:03:02.314 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  59.27s
geerlingguy.apache : Update apt cache. :  25.66s
geerlingguy.apache : Ensure Apache is installed on Debian. :  24.58s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  9.23s
Gathering Facts :  6.17s
geerlingguy.mysql : restart mysql :  5.76s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  4.45s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.61s
geerlingguy.apache : Add apache vhosts configuration. :  3.12s
geerlingguy.apache : Enable Apache mods. :  2.79s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  2.78s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  2.73s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  2.72s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  2.08s
geerlingguy.apache : restart apache :  2.01s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.76s
geerlingguy.apache : Get installed version of Apache. :  1.58s
geerlingguy.apache : Configure Apache. :  1.52s
geerlingguy.mysql : Remove MySQL test database. :  1.50s
geerlingguy.mysql : Update MySQL root password for localhost root account (5.7.x). :  1.48s
lundi 27 mars 2023  12:49:38 +0200 (0:00:05.766)       0:03:02.316 ************
===============================================================================
geerlingguy.mysql :  109.77s
geerlingguy.apache :  66.35s
gather_facts :  6.17s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  182.30s

Pas de gain. Mon pipeline est peut-être trop court ?

Activer le pipelining

Lorsque Ansible utilise des connexions SSH, plusieurs opérations se produisent en arrière-plan pour copier les fichiers, les scripts et les autres commandes d'exécution. Vous pouvez réduire le nombre de connexions SSH en activant le paramètre pipelining (il est désactivé par défaut) dans ansible.cfg :

Mais attention lors de l'utilisation des opérations "sudo:", vous devez d'abord désactiver "requiretty" dans /etc/sudoers sur tous les hôtes gérés (cf doc Ansible.)

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles
[ssh_connection]
pipelining = True

Le resultat du run :

Playbook run took 0 days, 0 hours, 2 minutes, 42 seconds
lundi 27 mars 2023  13:00:49 +0200 (0:00:05.583)       0:02:42.610 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  58.68s
geerlingguy.apache : Update apt cache. :  23.88s
geerlingguy.apache : Ensure Apache is installed on Debian. :  23.13s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  8.26s
Gathering Facts :  5.89s
geerlingguy.mysql : restart mysql :  5.58s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  3.80s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.54s
geerlingguy.apache : Add apache vhosts configuration. :  2.36s
geerlingguy.apache : Configure Apache. :  1.95s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  1.92s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  1.91s
geerlingguy.apache : Get installed version of Apache. :  1.48s
geerlingguy.apache : Enable Apache mods. :  1.38s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  1.35s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  1.22s
geerlingguy.apache : restart apache :  1.18s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.15s
geerlingguy.mysql : Remove MySQL test database. :  0.80s
geerlingguy.mysql : Update MySQL root password for localhost root account (5.7.x). :  0.71s
lundi 27 mars 2023  13:00:49 +0200 (0:00:05.583)       0:02:42.610 ************
===============================================================================
geerlingguy.mysql :  97.75s
geerlingguy.apache :  58.95s
gather_facts :  5.89s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  162.59s

Cette fois, nous avons des gains significatifs :)

Configurer le parallélisme

Dans la configuration par défaut d'ansible, le paramètre forks est défini à 5. On peut jouer avec sa valeur surtout si on utilise de gros inventaires.

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles
forks = 20

Le résultat du run :

Playbook run took 0 days, 0 hours, 1 minutes, 44 seconds
lundi 27 mars 2023  13:43:02 +0200 (0:00:03.470)       0:01:44.616 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  33.39s
geerlingguy.apache : Update apt cache. :  13.84s
geerlingguy.apache : Ensure Apache is installed on Debian. :  13.32s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  4.62s
geerlingguy.mysql : restart mysql :  3.47s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  3.33s
Gathering Facts :  3.30s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  2.06s
geerlingguy.apache : Add apache vhosts configuration. :  1.65s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  1.47s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  1.44s
geerlingguy.apache : Enable Apache mods. :  1.43s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  1.36s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  1.28s
geerlingguy.apache : Get installed version of Apache. :  1.10s
geerlingguy.apache : restart apache :  1.04s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.03s
geerlingguy.mysql : Create datadir if it does not exist :  0.82s
geerlingguy.mysql : Remove MySQL test database. :  0.81s
geerlingguy.mysql : Check if MySQL is already installed. :  0.80s
lundi 27 mars 2023  13:43:02 +0200 (0:00:03.471)       0:01:44.616 ************
===============================================================================
geerlingguy.mysql :  64.48s
geerlingguy.apache :  36.81s
gather_facts :  3.30s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  104.59s

Gros gain via ce paramètre !

Il y a quelques années, j'ai utilisé Ansible pour peupler une CMDB. Le principe nmap scrutait le port 22 sur tous les réseaux, en ressortait une liste de 8000 serveurs. Ensuite via Ansible, je testais la connexion, puis un gather_facts pour les machines qui répondaient. Pour ce besoin, j'ai poussé ce paramètre à 500, mais j'ai dû ajouter des ressources CPU et Mémoire pour que cela passe. Donc faites bien attention !

Utiliser une version de python récente

Lors de la sortie de python 3.10, il a été annoncé que cette version apporterait des gains de performances. Voyons son influence en l'utilisant sur le contrôleur (la machine d'où est lancé ansible). En partant d'une ubuntu 22.04 sur les cibles la version 3.10.8 de python est déjà installé.

pyenv install 3.10.8
pyenv virtualenv 3.10.8 ansible-python3.10.8
pyenv shell ansible-python3.10.8
pip install ansible boto3

On lance le run :


Playbook run took 0 days, 0 hours, 2 minutes, 57 seconds
lundi 27 mars 2023  13:29:30 +0200 (0:00:07.267)       0:02:57.820 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  58.41s
geerlingguy.apache : Update apt cache. :  24.32s
geerlingguy.apache : Ensure Apache is installed on Debian. :  23.35s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  8.80s
geerlingguy.mysql : restart mysql :  7.27s
Gathering Facts :  5.90s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  4.49s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.74s
geerlingguy.apache : Add apache vhosts configuration. :  2.93s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  2.58s
geerlingguy.apache : Enable Apache mods. :  2.51s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  2.48s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  2.44s
geerlingguy.apache : restart apache :  2.11s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  2.00s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.73s
geerlingguy.apache : Get installed version of Apache. :  1.67s
geerlingguy.mysql : Remove MySQL test database. :  1.47s
geerlingguy.mysql : Update MySQL root password for localhost root account (5.7.x). :  1.38s
geerlingguy.apache : Configure Apache. :  1.33s
lundi 27 mars 2023  13:29:30 +0200 (0:00:07.267)       0:02:57.819 ************
===============================================================================
geerlingguy.mysql :  108.77s
geerlingguy.apache :  63.12s
gather_facts :  5.90s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  177.79s

Peu ou pas de gains !

Désactiver ou améliorer la collecte des facts ansible

La collecte des facts peut être consommatrice, on peut la désactiver en mettant gather_facts à false. Mais dans notre exemple le playbook plante lamentablement. Eh oui, il teste la distribution pour configurer les machines en fonction de celles-ci. On va donc ajouter une pre-task permettant de collecter juste les facts distribution. Le playbook test-performance.yml devient :

- name: Installation de mysql avec le role de geerlinguy
  hosts: all
  gather_facts: false
  become: true
  pre_tasks:
    - name: Get minimal facts
      ansible.builtin.setup:
        gather_subset:
          - '!all'
          - distribution

  roles:
    - geerlingguy.mysql

Plus d'infos sur les gather_subset ici

Playbook run took 0 days, 0 hours, 2 minutes, 50 seconds
lundi 27 mars 2023  13:53:42 +0200 (0:00:06.061)       0:02:50.820 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  54.84s
geerlingguy.apache : Update apt cache. :  23.32s
geerlingguy.apache : Ensure Apache is installed on Debian. :  21.76s
geerlingguy.mysql : Ensure MySQL Python libraries are installed. :  9.01s
geerlingguy.mysql : restart mysql :  6.06s
Get minimal facts :  4.77s
geerlingguy.mysql : Update apt cache if MySQL is not yet installed. :  4.62s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.68s
geerlingguy.apache : Add apache vhosts configuration. :  3.09s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  2.72s
geerlingguy.apache : Enable Apache mods. :  2.59s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  2.55s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  2.54s
geerlingguy.apache : restart apache :  2.02s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  2.00s
geerlingguy.apache : Configure Apache. :  1.72s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  1.72s
geerlingguy.mysql : Get MySQL version. :  1.52s
geerlingguy.mysql : Create datadir if it does not exist :  1.51s
geerlingguy.apache : Add vhost symlink in sites-enabled. :  1.48s
lundi 27 mars 2023  13:53:42 +0200 (0:00:06.061)       0:02:50.819 ************
===============================================================================
geerlingguy.mysql :  105.29s
geerlingguy.apache :  60.73s
ansible.builtin.setup :  4.77s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  170.79s

Le gain est peu significatif, mais il peut le devenir si l'inventaire cible est conséquent !

De plus, dans le cas ou les gather_facts sont lancés à plusieurs reprises, on peut mettre en œuvre leur mise en cache. Cela se passe au niveau du fichier de config ansible.cfg.

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts

Dans notre exemple cela n'apportera rien, car les facts ne sont collectées qu'une seule fois (cloud = nouvelles ressources) !

Mais dans le cas où vous lancez plusieurs playbooks sur un nombre conséquent de machines ce paramétrage trouve tout son sens !

Regrouper si possible les taches

On voit que les taches les plus longues sont celles demandant l'installation des packages. En regardant le rôle de gueerlinguy, on peut voir que dans l'installation des packages sur une debian family, il utilise plusieurs taches apt qui pourraient être regroupées en une seule :

Dans le fichier roles/geerlingguy.mysql/tasks/setup-Debian.yml on peut lire :

- name: Update apt cache if MySQL is not yet installed.
  ansible.builtin.apt: update_cache=yes
  changed_when: False
  when: not mysql_installed.stat.exists

- name: Ensure MySQL Python libraries are installed.
  ansible.builtin.apt:
    name: "{{ mysql_python_package_debian }}"
    state: present

- name: Ensure MySQL packages are installed.
  ansible.builtin.apt:
    name: "{{ mysql_packages }}"
    state: present
    policy_rc_d: 101
  register: deb_mysql_install_packages

Deviennent :

- name: Ensure MySQL packages are installed.
  ansible.builtin.apt:
    update_cache: true
    name: "{{ mysql_packages + mysql_python_package_debian }}"
    state: present
    policy_rc_d: 101
  register: deb_mysql_install_packages
  when: not mysql_installed.stat.exists

Pour que cela fonctionne, j'ai aussi modifié la variable mysql_python_package_debian en une liste dans le fichier roles/geerlingguy.mysql/defaults/main.yml:

Playbook run took 0 days, 0 hours, 2 minutes, 46 seconds
lundi 27 mars 2023  14:05:35 +0200 (0:00:05.144)       0:02:46.575 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  62.22s
geerlingguy.apache : Update apt cache. :  24.69s
geerlingguy.apache : Ensure Apache is installed on Debian. :  22.79s
Gathering Facts :  5.71s
geerlingguy.mysql : restart mysql :  5.14s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  3.66s
geerlingguy.apache : Add apache vhosts configuration. :  3.02s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  2.62s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  2.52s
geerlingguy.apache : Enable Apache mods. :  2.52s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  2.44s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  2.03s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  1.99s
geerlingguy.apache : restart apache :  1.94s
geerlingguy.mysql : Remove MySQL test database. :  1.45s
geerlingguy.apache : Get installed version of Apache. :  1.42s
geerlingguy.mysql : Update MySQL root password for localhost root account (5.7.x). :  1.40s
geerlingguy.apache : Configure Apache. :  1.36s
geerlingguy.mysql : Get MySQL version. :  1.29s
geerlingguy.mysql : Get list of hosts for the root user. :  1.27s
lundi 27 mars 2023  14:05:35 +0200 (0:00:05.144)       0:02:46.575 ************
===============================================================================
geerlingguy.mysql :  98.03s
geerlingguy.apache :  62.82s
gather_facts :  5.71s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  166.55s

Même constat que précédemment, le gain peut devenir significatif dès lors où vous faites attention à regrouper des taches identiques. Et là se pose la question de l'utilisation et du codage des rôles. Il existe plusieurs stratégies pour améliorer les temps d'exécution en utilisant des rôles. La première que j'utilise est de ne faire que de la configuration dans les rôles, l'installation des packages étant regroupée dans le playbook en pre_tasks ou de construire des goldens images par middlewares.

Pour le plaisir toutes les optimisations ensemble

Attention, je ne le fais que pour jouer, mais avant de généraliser ce genre de pratique, il faut bien étudier le fonctionnement de vos playbooks pour voir s'ils sont compliants avec ce genre de pratique.

Le fichier ansible.cfg :

[defaults]
roles_path = ./roles
inventory=aws_ec2.yml
remote_user=ubuntu
host_key_checking = False
callbacks_enabled = timer, profile_tasks, profile_roles
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible-facts
forks = 20

[ssh_connection]
# -C permit compression
ssh_args=-o ControlMaster=auto -o ControlPersist=600s
pipelining = True

Playbook run took 0 days, 0 hours, 1 minutes, 35 seconds
lundi 27 mars 2023  14:16:38 +0200 (0:00:03.075)       0:01:35.738 ************
===============================================================================
geerlingguy.mysql : Ensure MySQL packages are installed. :  35.49s
geerlingguy.apache : Update apt cache. :  15.13s
geerlingguy.apache : Ensure Apache is installed on Debian. :  13.41s
Get minimal facts :  7.56s
geerlingguy.mysql : restart mysql :  3.08s
geerlingguy.mysql : Ensure MySQL is started and enabled on boot. :  1.57s
geerlingguy.apache : Add apache vhosts configuration. :  1.31s
geerlingguy.mysql : Copy my.cnf global MySQL configuration. :  1.06s
geerlingguy.mysql : Delete innodb log files created by apt package after initial install. :  1.00s
geerlingguy.apache : Ensure Apache has selected state and enabled on boot. :  0.93s
geerlingguy.apache : restart apache :  0.91s
geerlingguy.mysql : Copy .my.cnf file with root password credentials. :  0.89s
geerlingguy.apache : Get installed version of Apache. :  0.84s
geerlingguy.mysql : Ensure MySQL is stopped after initial install. :  0.83s
geerlingguy.apache : Enable Apache mods. :  0.77s
geerlingguy.mysql : Disallow root login remotely :  0.57s
geerlingguy.mysql : Remove MySQL test database. :  0.50s
geerlingguy.apache : Configure Apache. :  0.50s
geerlingguy.mysql : Create datadir if it does not exist :  0.42s
geerlingguy.mysql : Check if MySQL is already installed. :  0.42s
lundi 27 mars 2023  14:16:38 +0200 (0:00:03.075)       0:01:35.737 ************
===============================================================================
geerlingguy.mysql :  52.70s
geerlingguy.apache :  35.45s
ansible.builtin.setup :  7.56s
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
total :  95.72s

Gros gain :)

Autres pistes

Utiliser les taches asynchrones et les blocks

Parmi les autres gains possibles, c'est d'utiliser les taches asynchrones et les blocks. Le principe des taches asynchrones reprend celui de la strategy free sauf que l'on choisit quelle(s) tache(s) peu(ven)t tourner en arrière-plan et définir une tâche de resynchronisation.

Utiliser la meilleure des stratégies

Ansible par défaut utilise la stratégie linear pour ordonnancer les taches. En fait, il attend que chaque tâche soit totalement terminée pour passer à la suivante. Il existe une autre stratégie, appelée free qui elle enchaine les taches sur les machines sans attendre la fin de l'exécution des taches. Cela fonctionne très bien si vous n'avez pas de dépendances entre les différentes machines, par contre dans le cas contraire cela peut être désastreux. Donc à utiliser avec précaution. A ne surtout pas utiliser avec run_once !!!!!

[defaults]
inventory = .vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
callbacks_enabled = timer, profile_tasks, profile_roles
roles_path = ./roles

Pourquoi pas de changement dans le fichier ansible.cfg ? Car je préfère le voir directement dans le playbook !

- name: Installation de mysql avec le role de geerlinguy
  hosts: all
  gather_facts: true
  become: true
  strategy: free
  roles:
    - geerlingguy.apache
    - geerlingguy.mysql

Je ne mets pas de résultat pour ce paramètre, parce qu'il n'est pas assez généralisable.

Les erreurs communes allongeant le temps d'exécution des playbooks

Le recours aux modules shell et command au lieu des modules existant

Très souvent quand je reprends du code existant, même écrit par moi, je trouve des pistes d'améliorations. La plus courante étant le recours aux modules shell et command au lieu des modules. Souvent par méconnaissance et parfois parce qu'au moment de l'écriture du code ansible ce module n'existait pas. Une autre piste est la méconnaissance des nouvelles options. Combien de fois, on utilise plusieurs tâches pour contrer l'absence du paramètre d'un module et moi le premier. Le plus gros des risques en utilisant les modules shell et command est de rendre votre playbook non idempotant.

Utiliser des boucles sur les modules gérant les listes

Une autre des plus grosses erreurs est de mettre des boucles : loop, with_items, ... alors que le module prend en charge une liste de paramètres.

Le plus courant le module package ou apt ou yum .... :

- name: Installation de plusieurs packages
  ansible.builtin.apt:
    name: "{{item}}"
    state: present
  with_items:
    - htop
    - net-tools

qui devrait être écrit :

- name: Installation de plusieurs packages
  ansible.builtin.apt:
    name:
      - htop
      - net-tools
    state: present

Ne pas utiliser les blocks.

Regrouper des taches sur une seule condition avec les blocks !!!!

Ne pas utiliser le module synchronize

Pour copier plusieurs fichiers ou répertoires, on peut utiliser le module synchronize qui est un wrapper de la commande rsync:

- name: Synchronization using rsync protocol (pull)
  ansible.posix.synchronize:
    mode: pull
    src: rsync://somehost.com/path/
    dest: /some/absolute/path/

Plus loin

Si vous avez d'autres tips, n'hésitez pas en m'en faire part.