Howto Ansible
- Installation
- Utilisation de base
- Les éléments d’Ansible
- Configuration
- Jinja
- Erreurs
- Astuces
- Vérifier un playbook
- Stopper l’éxecution du code
- Lancement tâches hosts asynchrone
- Fréquence des hosts
- Cowsay
- Conditions dans fichier jinja2
- Boucles dans fichier jinja2
- Lire une entrée au clavier
- Exécuter un playbook en mode interactif
- Ne pas lancer une commande shell si le fichier existe
- Lancer une tâche sur machine précise (voire localhost)
- Ne lancer une tâche qu’une seule fois
- Appliquer une tâche à une liste (tableau) -> boucle
- Se connecter sous un autre utilisateur UNIX
- Éviter que la commande shell n’indique d’élement ‘changed’
- Voir variables disponibles
- Pré-requis OpenBSD
- Ansible Vault via GPG
- Git diff pour fichier vault
- Comparer des versions
- Erreur : /usr/local/bin/python: not found
- Export HTML d’un playbook
- Action en « fire and forget »
- Migration
- Uiliser les modules AWS
- Exemples
- Ressources utiles
- Documentation : http://docs.ansible.com/
- Statut de cette page : prod / bullseye
Ansible est un outil d’automatisation de configuration et gestion de serveurs : il permet le déploiement de logiciels et l’exécution de tâches via une connexion SSH.
Ansible fonctionne sans agent sur les serveurs (agent-less) et selon le concept d’idempotence : on décrit l’état d’un serveur et des actions seront exécutées dans le but de rendre le serveur conforme à cette description. On pourra relancer Ansible plusieurs fois, l’état final reste le même : seules les actions nécessaires seront exécutées.
Installation
En local
Nous utilisons actuellement Ansible 2.10 (version proposée en Debian 11) :
# apt install ansible sshpass
$ ansible --version
/usr/lib/python3/dist-packages/paramiko/transport.py:219: CryptographyDeprecationWarning: Blowfish has been deprecated
"class": algorithms.Blowfish,
ansible 2.10.8
config file = /home/gcolpart/.ansible.cfg
configured module search path = ['/home/gcolpart/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/dist-packages/ansible
executable location = /usr/bin/ansible
python version = 3.9.2 (default, Feb 28 2021, 17:03:44) [GCC 10.2.1 20210110]
Sur les serveurs distants
Ansible peut exécuter des actions sur des machines distantes. Mais certains logiciels sont requis :
- Debian 11 et 12 :
# apt-get install --no-install-recommends python3 dbus sudo
- Debian 6 à Debian 10 :
# apt-get install --no-install-recommends python dbus sudo
- Debian 4 / 5 : utiliser le module raw d’Ansible
- OpenBSD : voir pré-requis pour OpenBSD
- FreeBSD :
# pkg install python
Pour s’exécuter sur un serveur en Debian 11 et 12, nous forçons
ansible_python_interpreter=/usr/bin/python3
. Cela peut se
faire en ligne de commande
--extra-vars "ansible_python_interpreter=/usr/bin/python3"
ou dans votre inventaire.
Note : pour des anciens serveurs Debian, vous devrez parfois installer le paquet
python-apt
(mais normalement il s’installe désormais tout seul quand vous utiliser le moduleapt
).
Utilisation de base
Configuration minimale :
$ cat ~/.ansible.cfg
[defaults]
inventory = $HOME/.ansible/hosts
[ssh_connection]
#ssh_args = -o ControlMaster=no -o ControlPersist=no
ssh_args = -o ControlMaster=auto -o ControlPersist=300s
pipelining = True
Exemples d’utilisation basique sur sa machine en local :
$ ansible localhost -m ansible.builtin.ping
localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}
Ou sur une machine distante avec l’IP 192.0.2.42 :
$ ssh-copy-id mon-serveur
$ ansible mon-serveur -i 192.0.2.42, -m ansible.builtin.ping --one-line --forks 1
mon-serveur | SUCCESS => {"changed": false, "ping": "pong"}
Ou sur plusieurs machines distantes présentes dans un inventaire :
$ echo mon-serveur1 >> ~/.ansible/hosts
$ echo mon-serveur2 >> ~/.ansible/hosts
$ ansible "mon-*" -i $HOME/.ansible/hosts -m ansible.builtin.command --args "date"
mon-serveur1 | SUCCESS | rc=0 >>
jeudi 26 mai 2016, 23:16:01 (UTC+0200)
mon-serveur2 | SUCCESS | rc=0 >>
jeudi 26 mai 2016, 23:16:01 (UTC+0200)
Par exemple, pour lister les serveurs qui ont le package
lxc
installé :
$ ansible stretch,buster,bullseye,bookworm -bK --one-line --forks 42 -m ansible.builtin.shell --args 'dpkg -l | grep lxc | grep -q ii' | grep CHANGED
Autres exemples pratiques :
$ ansible stretch,buster --become --ask-become-pass --one-line --forks 42 -m ansible.builtin.command --args "grep -r SFTPEngine /etc/proftpd"
$ ansible "*" -bK --one-line --forks 42 -m ansible.builtin.command --args "grep foo /etc/passwd"
$ ansible "*" -bK --one-line --forks 42 -m ansible.builtin.command --args "lxc-ls" | grep CHANGED
$ ansible "*" --one-line --forks 42 -m ansible.builtin.copy --args "src=recap-auth.sh dest=~/"
$ ansible "*" -bK --one-line --forks 42 -m ansible.builtin.apt --args "name=needrestart"
$ ansible "*" -bK --one-line --forks 42 -m ansible.builtin.shell --args "cd /etc && git add . && git commit -av -m 'commit balai' ; sed -i 's/^DPkg/#DPkg/' /etc/apt/apt.conf.d/99needrestart ; git add . && git commit -av -m 'fix needrestart' ; exit 0"
Les éléments d’Ansible
L’élément de base d’Ansible est le module : on
peut exécuter une tâche (installation de paquets, copie de fichiers,
etc.) en exécutant simplement ansible -m mon_module
. Pour
regrouper plusieurs tâches, on utilise un playbook : un fichier en syntaxe YAML qui va lister
une succession de modules avec des arguments. Au sein d’un playbook, on
dispose d’options pratiques comme les handlers :
ils permettent le déclenchement d’une commande sous certaines conditions
(redémarrage d’un service par exemple). Si l’on veut organiser de façon
poussée les différentes tâches, on utilisera des roles : il s’agit simplement d’inclure dans un
playbook des fichiers YAML respectant une structure conventionnelle.
Enfin, pour s’exécuter sur un ensemble de machines, Ansible a besoin
d’un inventory : c’est la liste des serveurs
potentiellement concernés.
modules
https://docs.ansible.com/ansible/2.10/collections/index_module.html
Un module est comme une bibliothèque. Il constitue une couche d’abstraction par rapport au shell et commandes sous-jacentes. C’est cette couche qui permet l’idempotence et le fonctionnement sur plusieurs plateformes.
Les modules disposent de certains comportements et fonctionnalités communs : indication de succès/erreurs/changements, gestion des variables, des conditions, des boucles, des états de sortie…
Pour avoir la liste des modules utilisables :
ansible-doc -l
Voici quelques exemples de modules que nous utilisons :
- Module ansible.builtin.command :
Ce module ne permet que l’exécution de commandes simple (pas de pipe…) mais en échange il vérifie les commandes et les assainit pour limiter les injections.
- Module ansible.builtin.shell :
Ce module permet en revanche d’exécuter arbitrairement et sans contrôle toute commande, au sein d’un shell lancé pour l’occasion.
Pour forcr un shell en particulier :
- ansible.builtin.shell:
cmd: "set -o pipefail && dpkg -l cron 2>/dev/null | grep -q -E '^(i|h)i'"
executable: /bin/bash
- Module ansible.builtin.file :
- Module ansible.builtin.copy :
- Module ansible.builtin.replace :
- ansible.builtin.replace:
dest: /etc/ssh/sshd_config
regexp: '^(Match User ((?!{{ name }}).)*)$'
replace: '\1,{{ name }}'
- Module ansible.builtin.lineinfile :
- ansible.builtin.lineinfile:
dest: /etc/evocheck.cf
insertafter: EOF
line: "IS_APTICRON=0"
regexp: "^IS_APTICRON="
Note: replace vs lineinfile ? Le fonctionnement exact de replace et de lineinfile peut être déroutant. Voici quelques constatations :
avec lineinfile, si l’argument regexp n’est pas matché… il insère quand même la ligne ! regexp n’est pas une condition pour l’insertion mais pour remplacer au lieu d’insérer !
avec lineinfile, sauf cas tordus, l’argument regexp doit matcher l’argument line (sinon il va insérer la valeur de line à chaque exécution !)
lineinfile va d’abord évaluer si regexp matche et remplacer la dernière occurrence ; si regexp ne matche pas, il ajoute alors line (sans d’autre condition… même si elle existe déjà)
replace va remplacer uniquement si regex est matché, comme la commande sed
avec lineinfile, si l’on veut utiliser une référence (
\1
) dans line, ça donne une erreur, il faut utiliser replaceavec lineinfile, l’argument
backrefs: yes
sert à utiliser une référence au sein de l’argument regexp (et non pas au sein de l’argument line).Module ansible.builtin.blockinfile :
- ansible.builtin.blockinfile:
dest: /etc/apache2/envvars
block: |
## Set umask for writing by Apache user.
## Set rights on files and directories written by Apache
- Module community.general.ini_file :
- community.general.ini_file:
dest: /root/.my.cnf
section: client
option: user
value: root
mode: "0640"
Ce module permet de facilement d’ajouter/modifier/supprimer des valeurs dans des fichiers INI, dans la bonne section, sans se soucier de la syntaxe.
- Module ansible.builtin.user :
- ansible.builtin.user:
state: present
name: "jdoe"
comment: 'John Doe'
shell: /bin/bash
groups: adm
append: yes
password: '$6$k/Fg76xH'
Pour générer le hash du mot de passe à mettre dans la variable
password
:
- Module ansible.builtin.group :
- Module ansible.builtin.stat :
- Module ansible.builtin.apt :
- ansible.builtin.apt:
name: '{{ item }}'
state: latest
update_cache: yes
cache_valid_time: 3600
with_items:
- vim
- htop
Ce module fait partie d’une courte liste de modules pour lesquels
l’utilisation d’une boucle (avec with_items
par exemple) ne
provoque pas l’exécution séquentielle et répétée du module. Dans
l’exemple ci-dessus le module utilisera “apt” intelligemment.
- Module ansible.builtin.apt_repository :
- name: exemple
ansible.builtin.apt_repository:
repo: "deb https://artifacts.elastic.co/packages/5.x/apt stable main"
filename: elastic
state: present
L’indication “filename” permet de référencer le dépôt dans
/etc/apt/sources.list.d/<filename>.list
.
- Module community.mysql.mysql_user :
- community.mysql.mysql_user:
name: mysqladmin
password: my_password
priv: "*.*:ALL,GRANT"
state: present
config_file: /root/.my.cnf
update_password: on_create
Lorsqu’une réplication est en place, on peut choisir de ne pas
propager l’action dans les binlogs, avec l’option
sql_log_bin: no
.
Cela permet d’exécuter une commande du type “SET GLOBAL read_only = 1;” de manière idempotente.
- module community.general.htpasswd
- community.general.htpasswd:
path: /etc/nginx/htpasswd_phpmyadmin
name: jdoe
password: 'PASSWORD'
owner: root
group: www-data
mode: "0640"
Il nécessite la bibliothèque Python “passlib”, installable sous Debian grace au paquet “python-passlib” (“python3-passlib” sur les versions récentes).
- Module ansible.posix.sysctl :
- name: exemple
ansible.posix.sysctl:
name: vm.max_map_count
value: 262144
sysctl_file: /etc/sysctl.d/elasticsearch.conf
- Module community.general.alternatives :
- Module ansible.builtin.service :
- name: exemple pour redémarrer un service (compatible avec sysvinit, systemd…)
ansible.builtin.service: nginx
state: restarted
- Module community.general.openbsd_pkg :
- module community.general.timezone :
Si systemd est présent, le module utilise timedatectl
.
Sinon, sur Debian il utilise “/etc/timezone” et reconfigure le paquet
“tzdata”.
- module ansible.builtin.git :
- ansible.builtin.git:
repo: https://gitea.evolix.org/evolix/evoadmin-web.git
dest: /home/evoadmin/www
version: master
update: yes
Pour avoir plus d’infos sur un module :
# ansible-doc shell
> SHELL
The [shell] module takes the command name followed by a li
space-delimited arguments. It is almost exactly like the
(…)
Note : c’est pratique pour avoir la documentation exacte pour votre version d’Ansible. En effet, celle du site correspond à la dernière version et n’indique pas toujours toutes les différences.
playbook
https://docs.ansible.com/ansible/2.10/user_guide/playbooks.html
Un playbook va ensuite dérouler des actions qui seront organisées en tasks, roles et handlers.
Exemple de playbook simple :
Un playbook plus évolué :
---
- hosts: all
gather_facts: yes
become: yes
vars_files:
- 'vars/main.yml'
vars:
external_roles: ~/GIT/ansible-roles
external_tasks: ~/GIT/ansible-public/tasks
pre_tasks:
- name: Minifirewall is stopped (temporary)
ansible.builtin.service:
name: minifirewall
state: stopped
roles:
- "{{ external_roles }}/minifirewall"
post_tasks:
- include: "{{ external_tasks }}/commit_etc_git.yml"
vars:
commit_message: "Ansible run firewall.yml"
handlers:
- name: restart minifirewall
service:
name: minifirewall
state: restarted
# vim:ft=ansible:
On lance des playbooks ainsi :
$ ansible-playbook PLAYBOOK.yml --limit HOSTNAME --forks 1
$ ansible-playbook PLAYBOOK_WITH_SUDO.yml --limit HOSTNAME --ask-become-pass
Options utiles pour ansible-playbook :
-vvvv
: très verbeux (utile notamment pour debug SSH quand on a une erreur unreachable)-k
/--ask-pass
: demande le mot de passe pour la connexion SSH-K
/--ask-become-pass
: demande le mot de passe pour l’escalade (via sudo, su, doas…)-l
/--limit HOSTNAME
: limite la connexion à un ou plusieurs serveurs (attention, par défaut c’est all, cf/etc/ansible/hosts
)-f
/--forks N
: nombre de process lancés en parallèle (par défaut 5)… peut être utile de mettre à 1 pour ne pas paralléliser-i
/--inventory FILENAME/DIRNAME
: utiliser le fichier ou le dossier d’inventaire fournit en paramètre-i
/--inventory "example.com,"
: utilise un inventaire dynamique défini en paramètre (doit être un tableau)-D
/--diff
: montre un diff des changements effectués par les templates-K --become-method=su
: force l’utilisation desu
pour passer en root
Limiter l’exécution à certaines machines
Quelques exemples d’utilisation de l’option --limit
(ou
l
) :
- limiter aux groupes www et sql (qui peuvent être indifféremment des groupes ou des serveurs) :
$ ansible-playbook -l "www:sql" playbook.yml
- limiter aux serveurs foo-www01, foo-lb01, foo-filer… :
$ ansible-playbook -l "foo-*" playbook.yml
- limiter aux 10 premiers serveurs de l’inventaire (utile pour faire par paquets) :
$ ansible-playbook -l "*[0:9]" playbook.yml
- puis à ceux restants :
$ ansible-playbook -l "*[10:]" playbook.yml
Il est de toute façon préférable de ne pas mettre all
dans le champs hosts
dans le playbook pour éviter un
oubli.
Il est possible de lister les hôtes concernés par un inventaire (et
d’éventuels filtres) grace à l’option --list-hosts
.
$ ansible-playbook playbook.yml --list-hosts
playbook: playbook.yml
play #1 (all): all TAGS: []
pattern: ['all']
hosts (3):
server1
server2
server3
handlers
Les handlers sont des actions définies dans un
playbook, qui ne sont exécutées que dans certains cas. On utilise
l’option notify
au sein d’un module pour évoquer un
handler. Celui-ci ne sera exécuté que si un module a effectivement
provoqué un changement. L’usage classique est de recharger un service
après une modification de configuration : si la modification est
réalisée => le service est rechargé, si la modification est déjà
effectuée => aucune action.
Par défaut, l’exécution effective des handlers se fait une seule fois à la fin du playbook, quel que soit le nombre de fois où il a été demandé pendant l’exécution.
Exemple :
tasks:
- name: copy Apache configuration
ansible.builtin.copy: (…)
notify: Restart Apache
handlers:
- name: Restart Apache
ansible.builtin.service:
name: apache2
state: restarted
Dans des rôles longs, nous conseillons de purger les handlers de temps en temps (en fin de groupe d’action). En effet, si un playbook est interrompu les handlers ne sont pas forcément exécutés alors que l’action qui les a déclenchés a bien eu lieu. On insère alors l’action suivante :
Note : n’importe quel module peut être utilisé comme handler.
roles
https://docs.ansible.com/ansible/latest/user_guide/playbooks_roles.html
Lorsqu’on a besoin d’utiliser des fichiers ou templates à copier, des variables avec des valeurs par défaut, des handlers… on peut organiser tout cela dans un role en respectant la structure conventionnelle suivante :
foo
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
Cette structure permet à Ansible de retrouver automatiquement les fichiers et de les rendre disponibles dans l’exécution du rôle.
À titre d’exemple, voici des rôles Ansible que nous utilisons : https://gitea.evolix.org/evolix/ansible-roles
inventory
https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
La partie inventory correspond à la description de l’inventaire des serveurs à configurer et inclus un mécanisme de configuration individuelle et par groupe.
Il permet d’indiquer la liste des machines concernées par Ansible
(peut être limité lors de l’exécution de la commande par l’option
-l
) et de pouvoir les ranger dans des groupes.
Exemple:
hostname.internal
[httpservers]
machine[01:57].example.com
http.example.com:2222
[dbservers]
machine12.example.com
machine50.example.com
m[a:o]chine52.example.com
alias ansible_port=2222 ansible_host=192.0.2.42
[client]
host1 http_port=80 maxRequestsPerChild=808 # des variables qui seront automatiquement auto-completées liées à cet host
[commercant]
mercerie
chapeautier
[commercant:vars]
ntp_server=ntp.mercerie.example.com
proxy=proxy.mercerie.example.com
hostname.internal
: serveur présent dans aucun groupe[httpservers]
: le nom du groupe (pour les serveurs http). Les noms de hosts qui suivent appartiendront à ce groupemachine[01:57].example.com
: on peut indiquer une [pseudo-]expression régulière - ici ajoutera les machines machine01.example.com, machine02.example.com, machine03.example.com… machine57.example.comHOSTNAME:2222
: ansible se connecte par ssh, et HOSTNAME a un port SSH d’écoute différent qui est 2222[dbservers]
: groupe pour les serveurs de base de donnéesmachine50.example.com
: cette machine est déjà présente dans le groupe httpservers, mais sera aussi accessible à partir du groupe dbserversalias ansible_port=2222 ansible_host=192.0.2.42
: la machine alias n’a pas un vrai FQDN mais pointera vers 192.0.2.42 car on a indiqué des variables propres à Ansible. Il existe aussiansible_connection
(local ou ssh) ouansible_user
(le nom de l’utilisateur de la machine distante avec lequel Ansible se connectera en ssh)host1 http_port=80 maxRequestsPerChild=808
: des variables qui seront automatiquement disponibles pour les actions sur host1[commercant:vars]
: des variables qui seront liées au groupe commercant.
On peut aussi créer des groupes de groupes en utilisant
:children
On peut aussi découper le fichier “inventory” selon les groupes et les variables : https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#splitting-out-host-and-group-specific-data
Les variables propres à Ansible : https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#list-of-behavioral-inventory-parameters
variables
Les variables sont un élément clé de la configuration des playbooks et roles. Exemple :
vars:
ip: 192.0.2.42
conf_file: /etc/foo.conf
tasks:
- ansible.builtin.command: echo {{ ip }} >> {{ conf_file }}
Les variables peuvent être définies à de multiples niveaux, chacun ayant une certaine précédence (extrait de la documentation) :
- role defaults
- inventory vars
- inventory group_vars
- inventory host_vars
- playbook group_vars
- playbook host_vars
- host facts
- play vars
- play vars_prompt
- play vars_files
- registered vars
- set_facts
- role and include vars
- block vars (only for tasks in block)
- task vars (only for the task)
- extra vars (always win precedence)
Attention : les variables définies avec l’option
-e
ou --extra-vars
de la forme
var=valeur
sont toujours passées comme des chaînes de
caractères. Par exemple, écrire -e var=False
affectera la chaîne de caractères False
dans
var
, donc var
sera interpretée comme vraie !
Pour définir une variable booléenne, il faut utiliser du JSON :
-e '{ "var": false }'
.
Pour gérer de nombreuses variables dans un projet, on peut stocker toutes celles qui correspondent à un groupe de serveur dans un fichier portant le nom du groupe, ainsi que toutes celles d’un serveur en particulier dans un fichier du nom du serveur. Voici l’arborescence conseillée :
└── inventory
├── hosts # fichier d'inventaire
├── group_vars # dossier regrouppant …
│ └── group1.yml # … les variables du groupe "group1"
│ └── group2.yml # … les variables du groupe "group2"
└── host_vars # dossier regrouppant …
└── hostname1.yml # … les variables du serveur "hostname1"
└── hostname2.yml # … les variables du serveur "hostname2"
Les groupes sont définis dans le fichier d’inventaire.
Tags
https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html
Les tags permettent de ranger/trier chaque tâche ou rôle dans une catégorie.
On peut également utiliser les tags pour limiter/exclure des tâches :
$ ansible-playbook (…) --skip-tags "message"
On peut aussi n’exécuter que certains tags :
$ ansible-playbook (…) --tags "configuration,packages"
On peut également spécifier des tags quand on utilise un rôle :
Note : les charactères -
ne doivent pas être utilisés
dans les noms de tags, utiliser _
à la place.
Register
register
est un attribut d’action que l’on peut rajouter
pour tout type de tâche et qui initialisera la variable (par le nom
donné) avec les valeurs retournées par le module. Pour
shell
, on a le droit à .stdout
,
.stderr
, .rc
… mais cela dépend des valeurs de
retour du module.
Il est possible de consulter le contenu détaillé de la variable avec
debug
:
- ansible.builtin.stat:
path: /etc/passwd
register: st
- ansible.builtin.debug:
var: st
- ansible.builtin.fail:
msg: "Whoops! file ownership has changed"
when: st.stat.pw_name != 'root'
Pour certains modules, register
est presque un passage
obligatoire pour une utilisation cohérente des éléments (stat…).
Le contenu d’une variable issue d’un register
peut être
utilisé dans des conditions pour des tasks ultérieures. Dans
l’exemple ci-dessus on utilise une valeur intrinsèque de l’objet
stat
généré par le module, mais il y a des valeurs qui sont
utilisables quel que soit le module utilisé.
Cette autre task s’exécute si la précédente s’est terminée avec succès, en échec, a été sautée ou bien a produit un changement
- other_task: options
when: _file_copy is succeeded
- other_task: options
when: _file_copy is failed
- other_task: options
when: _file_copy is skipped
- other_task: options
when: _file_copy is changed
Vault
https://docs.ansible.com/ansible/latest/user_guide/latest/playbooks_vault.html
Un Vault permet d’avoir un fichier protégé par un mot de passe.
Pour éditer un Vault nommé foo.yml
(utilise l’éditeur
configuré) :
# ansible-vault edit foo.yml
Pour consulter un Vault (sortie standard) :
# ansible-vault view foo.yml
Pour modifier le mot de passe d’un vault :
# ansible-vault rekey foo.yml
Pour créer un vault vide :
# ansible-vault create bar.yml
Pour créer un vault sur un fichier clair :
# ansible-vault encrypt baz.yml
Pour retirer le chiffrement d’un fichier chiffré :
# ansible-vault decrypt baz.yml
Pour utiliser vault, il faut préciser l’option
--ask-vault-pass
avec les commandes ansible
ou
ansible-playbook
.
Conditions
Les tasks et les roles peuvent être soumis à
conditions. Cela repose sur la directive when
dont le
résultat est évalué de manière booléenne (true
ou
false
).
Exemple pour installer un paquet seulement si la distribution est en version 9 ou plus
- ansible.builtin.apt:
name: libapache2-mpm-itk
state: present
when: ansible_distribution_major_version is version('9', '>=')
Combinaisons
Il est possible de combiner plusieurs conditions sur la même ligne,
en combinant des and
ou des or
:
- ansible.builtin.apt:
name: libapache2-mpm-itk
state: present
when: ansible_distribution_major_version is version('9', '=') or ansible_distribution_major_version is version('10', '=')
Il est possible de séparer les conditions sur plusieurs lignes, elle
doivent alors être toutes respectées, comme jointe spar des
and
:
- ansible.builtin.apt:
name: libapache2-mpm-itk
state: present
when:
- ansible_distribution == "Debian"
- ansible_distribution_major_version is version('9', '>=')
Types de conditions
Pour des comparaisons de chaînes de caractères on peut utiliser
==
, !=
:
Pour des comparaisons de valeurs numériques on peut utiliser
==
, !=
, <=
,
>=
, <
, >
On peut changer le type d’une donnée en utilisant un filtre :
int
pour la transformation en entierbool
pour la transformation en booléen
Il est possible d’inverser une condition (quelle qu’elle soit) en la
précédant de not
:
- name: remove file is feature is disabled
ansible.builtin.file:
path: /path/to/file
state: absent
when: not (feature_enabled | bool)
Il est possible d’avoir 2 valeurs non booléennes en fonction d’une
variable booléenne. Exemple pour qu’un fichier soit présent ou absent
selon la variable feature_enabled
:
- ansible.builtin.file:
path: /path/to/file
state: "{{ feature_enabled | bool | ternary('present','absent') }}"
Configuration
https://docs.ansible.com/ansible/latest/installation_guide/intro_configuration.html
La configuration est lue dans l’ordre suivant :
ANSIBLE_CONFIG
(variable d’environnement)./ansible.cfg
~/.ansible.cfg
/etc/ansible/ansible.cfg
ansible.cfg
Quelques options qui peuvent être utiles :
display_args_to_stdout
: mettre àTrue
si on veut voir tout le contenu des tasks exécutées pour chaque étape écrit sur stdoutdisplay_skipped_hosts
: mettre àFalse
si on ne veut pas voir affichée sur stdout l’information d’une task qui n’est pas exécutée (le nom de variable est confu - mais il s’agit bien de l’affichage de la task)error_on_undefined_vars
: mettre àTrue
pour s’assurer que le script Ansible s’arrête si une variable n’est pas définie (alors qu’il y a utilisation de cette dernière dans une task)force_color
: mettre à1
pour forcer la couleurforks
: le nombre de processus en parallèle possible lors déploiement du script Ansible sur nombreux hostshosts
: accès vers les hosts par défaut (all
)private_key_file
: le chemin pour la clé pemremote_port
: le port SSH par défaut (22
)remote_user
: l’utilisateur pour la connexion SSH par défaut (root
)retry_files_enabled
: mettre àTrue
pour la création de fichier.retry
après un échec d’Ansible, pour reprendre le travail précédent - ajouté en argument dans l’appel de la commande
Jinja
Ansible utilise la bibliothèque Jinja2 pour ses templates, ses filtres, ses conditions…
- fusionner et dédoublonner 2 listes :
a: [1, 2, 3]
b: [3, 4, 5]
c: a | union(b) | unique
Il existe plein de filtres
sur les listes ; union
, intersect
,
difference
, unique
, sort
…
- liste avec valeur par défaut (variable vide, indéfinie ou liste vide)
a: []
c: a | default([1, 2], true)
C’est le second paramètre (true
) qui permet à
default()
d’agir lorsque la variable a
n’est
pas seulement nulle ou indéfinie, mais aussi en cas de chaîne vide,
tableau vide…
- boucler sur un attribut d’un dictionnaire
On veut par exemple créer les groupes des utilisateurs du dictionnaire suivant :
users:
user1:
name: user1
groups: group1
user2:
name: user2
groups: group2
user3:
name: user3
groups: group1
On va donc faire une boucle avec la liste des groupes définit dans l’attribut “groups” :
- name: "Create secondary groups"
ansible.builtin.group:
name: "{{ item }}"
with_items: "{{ users.values() | map(attribute='groups') | list | unique }}"
Erreurs
Les messages d’erreurs ne sont pas le point fort d’Ansible. Il n’est pas toujours clair de savoir si c’est un soucis de syntaxe YAML, un problème de sémantique d’Ansible ou une erreur dans l’utilisation de Jinja2. De plus, Ansible tente de faire des recommandations, mais elles sont des fois plus déroutantes qu’éclairantes. En voici quelques unes que nous avons rencontrées.
unbalanced jinja2 block or quotes
fatal: [HOSTNAME]: FAILED! => {"failed": true, "reason": "error while splitting arguments, either an unbalanced jinja2 block or quotes"}
Bien vérifier la syntaxe : cela peut être un guillemet mal fermé (ou mélange simples/doubles guillemets), ou encore histoire de crochet devenant une parenthèse…
Missing required arguments
fatal: [HOSTNAME]: FAILED! => {"changed": false, "failed": true, "msg": "missing required arguments: section"}
Le message est assez clair, donc bien relire la doc du module sur Ansible pour ajouter les arguments obligatoires pour ce module.
Requires stdlib json or simplejson module
fatal: [HOSTNAME]: FAILED! => {"changed": false, "failed": true, "msg": "Error: ansible requires the stdlib json or simplejson module, neither was found!"}
# apt install python-simplejson
Unable to install package
Si l’erreur
Unable to install package: There is no member named 'control'
se produit à l’installation d’un paquet, il suffit en général
d’installer le paquet Debian xz-utils
.
Astuces
Vérifier un playbook
- Vérifier la syntaxe :
$ ansible-playbook --syntax-check my-experimental-playbook.yml
- Voir toutes les tâches qui seront jouées (sans rien exécuter ni même simuler l’exécution) :
$ ansible-playbook --list-tasks my-experimental-playbook.yml
- Voir sur quels hôtes le playbook agira :
$ ansible-playbook --list-hosts my-experimental-playbook.yml
- Vérifier les actions qui vont être faites (mode
dry-run
) sans rien exécuter :
$ ansible-playbook --check my-experimental-playbook.yml
Note : certaines actions ne sont pas exécutées en mode “check”, cela peut donc perturber celles qui sont basées dessus.
- Avoir le diff des fichiers modifiés (ne fonctionne pas avec les
modules
replace
/lineinfile
à priori) :
$ ansible-playbook --check --diff my-experimental-playbook.yml
Stopper l’éxecution du code
Pour par exemple, stopper le code à un moment pour lire les valeurs d’une variables
ou
ou
Lancement tâches hosts asynchrone
Pour éviter que les différentes tâches s’appliquent une par une sur
tout les hosts impliqués par l’exécution du playbook, on peut utiliser
l’option strategy
à la valeur free
pour que
chaques tâches sur un host puisse continuer dès la fin de son exécution
sans attendre l’état des autres hosts concernés en cours.
Note: ne plus se fier au texte
host changed
après texte de la tâche, car il pourrait s’agir d’une autre tâche affichée plus en haut dans le texte de l’historique.
Fréquence des hosts
Lors de l’exécution d’un play, on peut indiquer une fréquence sur le nombre d’hôtes concernés par l’éxecution du playbook.
Fork
pour le nombre d’hôtes simultanés (modifiable dans le fichier ansible.cfg - mettre une valeur importante > centaine).serial
en en-tête contenant une valeur numérique qui représente le nombre de machines pour chaque tour d’éxecution de playbook, ou un pourcentage par rapport à la liste inventory concerné.
Cowsay
Si la commande cowsay
est disponible sur votre machine,
vous verrez un message à la fin :
____________________
< NO MORE HOSTS LEFT >
--------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
____________
< PLAY RECAP >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Pour le désactiver : export ANSIBLE_NOCOWS=1
Disponible aussi en définissant nocows = 1
dans le
fichier de configuration (/etc/ansible/ansible.cfg
par
exemple).
Conditions dans fichier jinja2
https://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-tests
{% if python_is_installed is defined %}
Ansible devrait marcher -pardi!
{% endif %}
Voir la doc pour plus de détails : https://jinja.palletsprojects.com/en/2.11.x/
Boucles dans fichier jinja2
location / {
{% for ip in allowed_ips %}
allow {{ ip }};
{% endfor %}
deny all;
Lire une entrée au clavier
https://docs.ansible.com/ansible/latest/user_guide/playbooks_prompts.html
S’il manque une valeur pour la suite du script, soit on le gère en mettant une erreur, ou une valeur par défaut, mais sinon on peut aussi demander une saisie clavier :
vars_prompt:
- name: 'prenom'
prompt: 'Quel est votre prénom ?'
private: no
tasks:
- ansible.builtin.debug:
var: prenom
Malheureusement pour le moment, cela doit se situer avant
tasks
.
Si on veut utiliser cette variable dans une tâche, il faut simplement utiliser le nom de la variable, et si on veut l’utiliser (explicitement) pour un play ne se trouvant pas dans le même fichier (donc ici la variable dans autre.yml s’appelera prenom_de_autre et non prenom) :
Exécuter un playbook en mode interactif
https://docs.ansible.com/ansible/latest/user_guide/playbooks_startnstep.html
$ ansible-playbook playbook.yml --step
Ne pas lancer une commande shell si le fichier existe
Avec l’argument creates
indiquant le chemin de fichier
lors de l’utilisation du module shell, cette tâche ne s’exécutera que si
le fichier indiqué par creates
n’existe pas. Le corollaire
est possible avec l’argument removes
qui empêche
l’exécution si le fichier n’existe pas.
Ces arguments sont disponibles pour certains modules (comme
shell
).
C’est beaucoup plus simple et rapide que de tester le fichier par le
module stat
juste avant.
Lancer une tâche sur machine précise (voire localhost)
https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html#delegation
- name: /etc/hosts
ansible.builtin.command:
cmd: cat /etc/hosts
register: tmp
delegate_to: localhost
- ansible.builtin.debug:
var: tmp.stdout
Pour une exécution locale, on peut aussi utiliser l’attribut
local_action
.
Ne lancer une tâche qu’une seule fois
https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html#run-once
Si cet attribut est utilisé avec delegate_to
, alors
cette machine sera la seule à exécuter cette tâche. Sinon, c’est la
première dans la liste de l’inventaire.
Appliquer une tâche à une liste (tableau) -> boucle
with_items
- name: Manger les fruits
ansible.builtin.command:
cmd: eat '{{ item }}'
with_items:
- Apple
- Orange
- Strawberry
- Mango
Par exemple pour l’installation de plusieurs nouveaux paquets :
---
- hosts: localhost
tasks:
- ansible.builtin.apt:
name: '{{ item }}'
state: present
with_items:
- cmatrix
- tetrinet-server
- tetrinet-client
- xtel
Même si il y aura plusieurs paquets installés, cela ne comptera que
pour un changement (changed=1
). Cette tâche
appellera un par un les éléments de la liste (présents dans
with_items
) pour le module.
with_nested
Pour croiser les éléments des items :
tasks:
- include: "./ajout_utilisateur_sur_machine.yml"
vars:
user: "{{ item[0] }}"
server: "{{ item[1] }}"
with_nested:
- [ 'alice', 'bob' ]
- [ 'machine1', 'machine2', 'machine-backup' ]
Cela a pour effet d’exécuter l’inclusion pour alice
pour
chacune des 3 machines, puis pour bob
pour chacune des 3
machines.
with_dict
Avec hash :
users:
bob:
name: Bob
uid: 1000
home: /home/bob
alice:
name: Alice
uid: 1001
home:
tasks:
- user:
name: "{{ item.key }}"
comment: "{{ item.value.name }}"
uid: "{{ item.value.uid }}"
home: "{{ item.value.home }}"
with_dict: "{{ users }}"
with_first_found
Permet de prendre le premier fichier trouvé :
- name: Copy HAProxy configuration
ansible.builtin.template:
src: "{{ item }}"
dest: /etc/haproxy/haproxy.cfg
force: yes
with_first_found:
- "haproxy.cfg/{{ inventory_hostname }}"
- "haproxy.cfg/{{ host_group }}"
- "aproxy.cfg/default"
De cette manière, si un fichier portant le nom du serveur en cours existe, il sera utilisé, sinon on cherche un fichier du nom du groupe du serveur et enfin on cherche un fichier par défaut, valable pour tous les serveurs qui n’ont pas de configuration spécifique ou de groupe.
Se connecter sous un autre utilisateur UNIX
Par défaut, l’utilisateur se connectant sur le serveur distant est
l’utilisateur UNIX courant. On peut soit le préciser dans le fichier de
conf principal d’Ansible avec remote_user: michu
, dans
l’inventaire pour un groupe ou un serveur précis ou encore en
l’indiquant en argument lors de l’éxecution du playbook.
$ ansible-playbook -u michu -k play.yml
Éviter que la commande shell n’indique d’élement ‘changed’
Sur tous les modules, chaque tâche retourne un statut sur son résultat :
ok
: aucune modification n’a été nécessairechanged
: une modification a eu lieu par rapport à l’état précédent (droits fichiers…)failed
: une erreur s’est produite
Pour des modules comme shell
, command
…
Ansible ne peut savoir si un changement a eu lieu ou pas. Il indique
alors toujours changed
.
Il est possible de forcer le statut du changement :
Voir variables disponibles
$ ansible -m ansible.builtin.setup <hostname>
HOSTNAME | SUCCESS => {
"ansible_facts": {
(…)
"ansible_architecture": "x86_64",
"ansible_bios_date": "12/01/2006",
"ansible_bios_version": "VirtualBox",
"ansible_cmdline": {
"BOOT_IMAGE": "/boot/vmlinuz-3.16.0-4-amd64",
"quiet": true,
"ro": true,
"root": "UUID=37de3cbb-3f28-48d2-a4eb-c893a2f2fbc3"
},
"ansible_date_time": {
"date": "2016-05-06",
"day": "06",
"epoch": "1462546886",
"hour": "17",
(…)
},
"ansible_default_ipv4": {
(…)
}
$ ansible -m ansible.builtin.debug -a "var=hostvars['hostname']" localhost
Pour récupérer toutes les adresses MAC des machines :
que l’on pourra combiner par exemple avec un pipe en ligne de commande :
$ ansible-playbook mac_address.yml | grep ansible_eth0.macaddress | sed 's/^\s*"ansible_eth0.macaddress": "\(.*\)"/\1/'
Il est possible aussi d’accéder aux variables d’environnement shell :
"{{ lookup('env','HOME') }}"
Pré-requis OpenBSD
Voici les étapes nécessaires à l’utilisation d’Ansible sur des serveurs OpenBSD.
Installer Python et sudo :
# pkg_add -z python sudo
Faire un lien symbolique de “python” vers le python le plus récent sur la machine, pour pallier le problème d’hétérogénéité de versions sur plusieurs machines OpenBSD différentes :
# ls -l /usr/local/bin/python*
# ln -s /usr/local/bin/pythonX.X /usr/local/bin/python
Et surcharger la variable ansible_python_interpreter
dans le fichier inventory :
[openbsd]
serveur.example.com
[openbsd:vars]
ansible_python_interpreter=/usr/local/bin/python
Ansible Vault via GPG
Afin de ne pas avoir à taper son mot de passe Vault à chaque utilisation, on peut stocker son mot de passe Vault dans un fichier chiffré par GPG. Au préalable, il faut configurer GPG pour utiliser l’agent GPG.
Ensuite, créer le script suivant dans
~/bin/open_vault.sh
#!/bin/sh
gpg --quiet --batch --use-agent --decrypt ~/.ansible/vault.gpg
Rendre ce script exécutable :
chmod +x ~/bin/open_vault.sh
Configurer Ansible pour utiliser ce script comme source du mot de
passe Ansible Vault dans ~/.ansible.cfg
:
[defaults]
vault_password_file= ~/bin/open_vault.sh
Stocker le mot de passe Ansible Vault dans un fichier chiffré via GPG :
cat | gpg -e -o ~/.ansible/vault.gpg
Ansible va maintenant automatiquement déchiffrer les fichiers Vault
via votre agent GPG et le fichier ~/.ansible/vault.gpg
.
Il existe une option similaire pour le mot de passe de “become”
appelée become_password_file
.
Git diff pour fichier vault
Les diff de fichier chiffrés avec ansible-vault ne sont pas lisibles par défaut car ils s’appliquent sur le contenu chiffré des fichiers et non pas sur le contenu réel.
On peux modifier cela, en modifiant sa config GIT dans son fichier ~/.gitconfig :
[diff "ansible-vault"]
textconv = ansible-vault view
cachetextconv = false
Et en appliquant cette config au fichier vault dans ses dépôts Git dans le fichier .gitattributes :
vars/evolinux-secrets.yml diff=ansible-vault
Comparer des versions
Dans le cas où on ne veut pas faire la même chose suivant la version sur lequelle on exécute la tâche, on peut utiliser version_compare.
Un cas concret :
- name: Install monitoring-plugins on OpenBSD 5.6 and later
community.general.openbsd_pkg:
name: monitoring-plugins
state: present
when: ansible_distribution_version is version('5.6', '>=')
- name: Install nagios-plugins on OpenBSD before 5.6
community.general.openbsd_pkg:
name: nagios-plugins
state: present
when: ansible_distribution_version is version("5.6",'<')
Erreur : /usr/local/bin/python: not found
Si vous obtenez une erreur du type :
$ ansible -m ansible.builtin.ping foo
foo | FAILED! => {
"changed": false,
"failed": true,
"module_stderr": "/bin/sh: 1: /usr/local/bin/python: not found\n",
"module_stdout": "",
"msg": "MODULE FAILURE"
}
Pour une raison inconnue, Ansible détecte mal le chemin vers Python.
Vous pouvez le forcer en utilisant l’option
-e 'ansible_python_interpreter=/usr/bin/python'
.
Export HTML d’un playbook
Pour enregistrer la sortie d’exécution d’un playbook dans un fichier
HTML (en gardant les couleurs et les warnings qui vont normalement sur
la sortie d’erreur), on peut utiliser le paquet aha
:
Si on veut quand même avoir la sortie dans son terminal :
ANSIBLE_FORCE_COLOR=true ansible-playbook playbook.yml 2>&1 | tee /dev/fd/2 | aha > playbook_output.html
Action en « fire and forget »
Il est possible d’exécuter un module ou une commande et continuer l’exécution du playbook sans attendre sa completion, avec le mode asynchrone.
C’est utile par exemple pour une action potentiellement très lente, ou bloquante.
Avant l’existence du module natif reboot
c’était un bon
moyen de ne pas bloquer l’exécution du playbook.
Exemple pour la commande sleep 15
à qui on donne 45
secondes pour s’exécuter, sans bloquer le playbook.:
- name: Simulate long running op, allow to run for 45 sec, fire and forget
ansible.builtin.command: /bin/sleep 15
async: 45
poll: 0
Migration
Ansible 2.7 vers 2.10
vérification version Debian
ansible_distribution_major_version | version_compare('11', '=')
devient :
- ansible_distribution_major_version is version('11', '=')
Uiliser les modules AWS
Sur Debian 11 (et probablement les versions inférieures) les
bibliothèques python3-boto3
et
python3-botocore
sont dans des versions trop anciennes pour
les modules ec2_eni
, ec2_eni_info
…
On peut aisément télécharger les versions de Bookworm, puis les
installer manuellement (dpkg -i <pkg.deb>
) : *
https://packages.debian.org/bookworm/all/python3-boto3/download *
https://packages.debian.org/bookworm/all/python3-botocore/download
Exemples
Voir /HowtoAnsible/Exemples.
Ressources utiles
- Le « User Guide » (voir notamment la partie Best Practices)
- Vidéos ansible-best-practices et ansible-tips-and-tricks
- Ansible 101 - on a Cluster of Raspberry Pi 2s
- Sysadmin Casts (épisodes 43, 45, 46 et 47)
- How Twitter uses Ansible (AnsibleFest 2014)
- Orchestration with Ansible at Fedora Project