Écrire un module Ansible sur mesure
Vincent Bernat
Ansible dispose de nombreux modules que vous pouvez combiner pour vos besoins. Toutefois, la qualité de ces modules peut varier considérablement. Parfois, il peut être plus simple et plus robuste d’écrire votre propre module au lieu de chercher et d’assembler des modules existants1.
À mon avis, un module robuste répond aux caractéristiques suivantes :
- idempotence,
- affichage des différences (
--diff
), - compatibilité avec le mode simulation (
--check
), - indication s’il y a eu des changements,
- gestion du cycle de vie des objets.
En bref, cela signifie que le module est capable de fonctionner avec
--diff --check
et d’afficher les changements qu’il appliquerait.
Quand il est lancé deux fois de suite, aucun changement n’est appliqué
ni signalé lors de la seconde exécution. Le dernier point suggère que
le module devrait être capable de supprimer les objets obsolètes mis
en place lors des exécutions précédentes2.
Le code du module doit être minimal et adapté à vos besoins. Rendre le module générique pour qu’il puisse être utilisé par d’autres utilisateurs n’est pas un objectif. Moins de code signifie généralement moins de problèmes et plus de facilité à relire.
Je n’aborde pas la problématique des tests ici. Il s’agit indéniablement d’une bonne pratique, mais cela nécessite un effort important. À mon sens, il est préférable d’avoir un module bien écrit répondant aux caractéristiques ci-dessus plutôt qu’un module bien testé mais sans celles-ci ou un module nécessitant un assemblage supplémentaire (sans tests) pour correspondre à vos besoins.
Squelette du module#
La documentation d’Ansible contient des instructions pour construire un module, ainsi que quelques bonnes pratiques. Comme la distribution du module ne fait pas partie de nos objectifs, nous avons choisi de prendre quelques raccourcis. Supposons que nous construisions un module avec la signature suivante :
custom_module: user: "quelqu'un" password: quelque chose data: "une chaîne au hasard"
Il y a plusieurs endroits où vous pouvez placer un module dans
Ansible. Une possibilité courante est de l’inclure dans un rôle. Dans
un sous-répertoire library/
, créez un fichier vide __init__.py
et
un fichier custom_module.py
avec le code suivant3 :
#!/usr/bin/python import yaml from ansible.module_utils.basic import AnsibleModule def main(): # Définissez les options acceptées par le module. ❶ module_args = dict( user=dict(type='str', required=True), password=dict(type='str', required=True, no_log=True), data=dict(type='str', required=True), ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) result = dict( changed=False ) got = {} wanted = {} # Renseignez les variables `got` et `wanted`. ❷ # […] if got != wanted: result['changed'] = True result['diff'] = dict( before=yaml.safe_dump(got), after=yaml.safe_dump(wanted) ) if module.check_mode or not result['changed']: module.exit_json(**result) # Appliquez les changements. ❸ # […] module.exit_json(**result) if __name__ == '__main__': main()
En ❶, nous définissons le module avec les options acceptées.
Reportez-vous à la documentation de argument_spec
pour plus de détails.
En ❷, nous construisons deux variables: got
and wanted
. got
représente l’état actuel tandis que wanted
est l’état cible.
Par exemple, si vous avez besoin de modifier des enregistrements dans
un serveur de base de données, got
contient les lignes actuelles
tandis que wanted
contient celles après modification. Ensuite, nous
comparons got
et wanted
. S’il y a une différence, changed
passe à
True
et nous créons l’objet diff
. Ansible l’utilise pour afficher
les différences entre les deux états. Si nous sommes en mode
simulation ou si aucun changement n’est détecté, nous nous arrêtons
là.
En ❸, on applique les changements. En général, cela signifie qu’il faut itérer sur les deux structures pour détecter les différences et créer les éléments manquants, supprimer ceux non désirés et mettre à jour ceux existants.
Documentation#
Ansible fournit une page assez complète sur la façon de documenter un module. Je vous conseille une approche plus minimale en ne documentant chaque option qu’avec parcimonie4, en sautant les exemples et en ne documentant les valeurs de retour que si cela s’avère nécessaire. Je me limite généralement à quelque chose de ce genre :
DOCUMENTATION = """ --- module: custom_module.py short_description: Pass provided data to remote service description: - Mention anything useful for your workmate. - Also mention anything you want to remember in 6 months. options: user: description: - user to identify to remote service password: description: - password for authentication to remote service data: description: - data to send to remote service """
Gestion des erreurs#
En cas d’erreur, vous pouvez arrêter l’exécution avec
module.fail_json()
:
module.fail_json( msg=f"remote service answered with {code}: {message}", **result )
Il n’y a pas d’obligation à intercepter toutes les erreurs. Parfois, ne pas neutraliser une exception fournit une meilleure information que le remplacement par un message générique.
Retourner des valeurs supplémentaires#
Un module peut renvoyer des informations supplémentaires qui peuvent
être enregistrées pour être utilisées dans une autre tâche par le
biais de la directive register
. À cette fin, vous pouvez
ajouter des champs arbitraires au dictionnaire result
. Consultez la
documentation à propos des valeurs standards de retour. Vous devez essayer d’ajouter ces champs avant de quitter le
module en mode simulation. Les valeurs de retour peuvent être
documentées.
Exemples#
Voici plusieurs exemples de modules sur mesure suivant l’approche documentée ci-dessus. Chaque exemple indique la raison pour laquelle on préfère écrire un nouveau module plutôt que de combiner des modules existants. ⚙️
- Synchroniser les clefs SSH sur Cisco IOS XR
- Synchroniser des tables MySQL
- Synchroniser NetBox
- Synchroniser des objets RIPE, ARIN et APNIC
- Synchroniser des tunnels IPsec GCP (non réalisé)
-
De plus, en utilisant les modules d’Ansible Galaxy, vous introduisez une dépendance à un tiers. Ce n’est pas quelque chose qui doit être décidé à la légère : cela peut casser plus tard, cela peut ne répondre qu’à 80% des besoins, cela peut ajouter des bugs. ↩︎
-
Certains systèmes déclaratifs, comme Terraform, sont fondés sur ces comportements. ↩︎
-
Ne vous inquiétez pas pour le shebang. Il est codé en dur avec
/usr/bin/python
. Ansible le modifiera pour qu’il corresponde à l’interpréteur choisi sur l’hôte distant. Vous pouvez écrire du code Python 3 siansible_python_interpreter
correspond à Python 3. ↩︎ -
Le principal inconvénient de cette approche non programmatique de la documentation est qu’elle répète les informations contenues dans
argument_spec
. Je pense qu’une structure auto-documentatée permettrait d’éviter cela. ↩︎