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 :
Le fichier requirements.yml pour installer les rôles :
On installe les rôles avec la commande ansible-galaxy :
Le playbook qui servira de test :
Pour la configuration d’ansible, j’ai généré celle par défaut et que j’ai obtenue avec la
commande suivante :
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 :
Prise de mesures initiales
Pour mesurer le temps pris par chacune des taches, je vais activer les
callbacks suivants :
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:
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.
On lance le premier run :
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 :
Le résultat du run :
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.)
Le resultat du run :
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.
Le résultat du run :
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é.
On lance le run :
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 :
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.
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 :
Deviennent :
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:
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 :
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 !!!!!
Pourquoi pas de changement dans le fichier ansible.cfg ? Car je préfère le voir
directement dans le playbook !
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 … :
qui devrait être écrit :
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:
Plus loin
Si vous avez d’autres tips, n’hésitez pas en m’en faire part.