--- categories: automation développement title: Howto Ansible ... * Documentation : * Statut de cette page : prod / bullseye [Ansible](https://www.ansible.com/) 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](https://fr.wikipedia.org/wiki/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](https://docs.ansible.com/ansible/2.7/modules/raw_module.html) d'Ansible * *OpenBSD* : voir **[pré-requis pour OpenBSD](#pré-requis-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 module `apt`). ## 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 -bK --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 ~~~ ## Les éléments d'Ansible L'élément de base d'Ansible est le [module](#modules) : 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](#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](#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](#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](#inventory) : c'est la liste des serveurs potentiellement concernés. ### modules 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html), des [conditions](https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html), des [boucles](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html), 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/command_module.html) : ~~~{.yaml} - ansible.builtin.command: cmd: date ~~~ 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/shell_module.html) : ~~~{.yaml} - ansible.builtin.shell: cmd: "cat foo.txt | grep bar" ~~~ 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 : ~~~{.yaml} - 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/file_module.html) : ~~~{.yaml} - ansible.builtin.file: path: /etc/cron.daily/apticron state: absent ~~~ * Module [ansible.builtin.copy](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/copy_module.html) : ~~~{.yaml} - ansible.builtin.copy: src: files/foo dest: /etc/bar owner: root group: root mode: "0644" ~~~ * Module [ansible.builtin.replace](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/replace_module.html) : ~~~{.yaml} - ansible.builtin.replace: dest: /etc/ssh/sshd_config regexp: '^(Match User ((?!{{ name }}).)*)$' replace: '\1,{{ name }}' ~~~ * Module [ansible.builtin.lineinfile](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/lineinfile_module.html) : ~~~{.yaml} - 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 _replace_ * avec _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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/blockinfile_module.html) : ~~~{.yaml} - 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](https://docs.ansible.com/ansible/2.10/collections/community/general/ini_file_module.html) : ~~~{.yaml} - 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/user_module.html) : ~~~{.yaml} - 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` : ~~~{.bash} mkpasswd --method=sha-512 ~~~ * Module [ansible.builtin.group](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/group_module.html) : ~~~{.yaml} - ansible.builtin.group: state: present name: "foobarcorp" gid: "1042" ~~~ * Module [ansible.builtin.stat](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/stat_module.html) : ~~~{.yaml} - ansible.builtin.stat: path: /etc/sudoers.d/foo register: foo_sudoers_file ~~~ * Module [ansible.builtin.apt](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/apt_module.html) : ~~~{.yaml} - 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/apt_repository_module.html) : ~~~{.yaml} - 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/.list`. * Module [community.mysql.mysql_user](https://docs.ansible.com/ansible/2.10/collections/community/mysql/mysql_user_module.html) : ~~~{.yaml} - 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`. * module [community.mysql.mysql_variables](https://docs.ansible.com/ansible/2.10/collections/community/mysql/mysql_variables_module.html) ~~~{.yaml} - community.mysql.mysql_variables: variable: read_only value: 1 ~~~ Cela permet d'exécuter une commande du type "SET GLOBAL read_only = 1;" de manière idempotente. * module [community.general.htpasswd](https://docs.ansible.com/ansible/2.10/collections/community/general/htpasswd_module.html) ~~~{.yaml} - 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](https://docs.ansible.com/ansible/2.10/collections/ansible/posix/sysctl_module.html) : ~~~{.yaml} - name: exemple ansible.posix.sysctl: name: vm.max_map_count value: 262144 sysctl_file: /etc/sysctl.d/elasticsearch.conf ~~~ * Module [community.general.alternatives](https://docs.ansible.com/ansible/2.10/collections/community/general/alternatives_module.html) : ~~~{.yaml} - community.general.alternatives: name: editor path: /usr/bin/vim.basic ~~~ * Module [ansible.builtin.service](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/service_module.html) : ~~~{.yaml} - name: exemple pour redémarrer un service (compatible avec sysvinit, systemd…) ansible.builtin.service: nginx state: restarted ~~~ * Module [community.general.openbsd_pkg](https://docs.ansible.com/ansible/2.10/collections/community/general/openbsd_pkg_module.html) : ~~~{.yaml} - community.general.openbsd_pkg: name: "{{ item }}" state: present with_items: - wget - vim--no_x11 ~~~ * module [community.general.timezone](https://docs.ansible.com/ansible/2.10/collections/community/general/timezone_module.html) : ~~~{.yaml} - community.general.timezone: name: Europe/Paris ~~~ 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](https://docs.ansible.com/ansible/2.10/collections/ansible/builtin/git_module.html) : ~~~{.yaml} - 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 Un playbook va ensuite dérouler des actions qui seront organisées en _tasks_, [roles](#roles) et [handlers](#handlers). Exemple de playbook simple : ~~~{.yaml} --- - hosts: all tasks: - ansible.builtin.shell: echo hello World # vim:ft=ansible: ~~~ Un playbook plus évolué : ~~~{.yaml} --- - 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](https://manpages.debian.org/cgi-bin/man.cgi?query=ansible-playbook&apropos=0&sektion=0&manpath=Debian+unstable+sid&format=html&locale=en) : * `-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 de `su` 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 : ~~~{.yaml} 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 : ~~~{.yaml} - ansible.builtin.meta: flush_handlers ~~~ > *Note* : n'importe quel module peut être utilisé comme handler. ### roles 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 : ### inventory 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 groupe * `machine[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.com_ * `HOSTNAME: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ées * `machine50.example.com` : cette machine est déjà présente dans le groupe _httpservers_, mais sera aussi accessible à partir du groupe _dbservers_ * `alias 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 aussi `ansible_connection` (local ou ssh) ou `ansible_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 : Les variables propres à Ansible : ### variables Les variables sont un élément clé de la configuration des playbooks et roles. Exemple : ~~~{.yaml} 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable)) : * 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](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html). ### Tags Les tags permettent de ranger/trier chaque tâche ou rôle dans une catégorie. ~~~{.yaml} - name: Coucou ansible.builtin.debug: msg: "Saloute!" tags: message ~~~ 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` : ~~~{.yaml} - 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é. ~~~{.yaml} - ansible.builtin.copy: src: my_file dst: /path/to/file register: _file_copy ~~~ 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 ~~~{.yaml} - 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 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 ~~~{.yaml} - 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` : ~~~{.yaml} - 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` : ~~~{.yaml} - 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 `==`, `!=` : ~~~{.yaml} when: - ansible_distribution == "Debian" - ansible_distribution != "Ubuntu ~~~ 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 entier * `bool` pour la transformation en booléen ~~~{.yaml} when: redis_port | int == 6379 ~~~ Il est possible d'inverser une condition (quelle qu'elle soit) en la précédant de `not` : ~~~{.yaml} - 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` : ~~~{.yaml} - ansible.builtin.file: path: /path/to/file state: "{{ feature_enabled | bool | ternary('present','absent') }}" ~~~ ## Configuration 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 _stdout_ * `display_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 couleur * `forks` : le nombre de processus en parallèle possible lors déploiement du script Ansible sur nombreux _hosts_ * `hosts` : accès vers les _hosts_ par défaut (`all`) * `private_key_file` : le chemin pour la clé pem * `remote_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](https://jinja.palletsprojects.com/en/2.11.x/templates/) 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#selecting-from-sets-or-lists-set-theory) ; `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 * 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 ~~~{.yaml} - ansible.builtin.debug: var: foo - command: /bin/false ~~~ ou ~~~{.yaml} - ansible.builtin.debug: var: foo - ansible.builtin.fail: msg: "FAIL" ~~~ ou ~~~{.yaml} - ansible.builtin.debug: var: foo - ansible.builtin.pause: ~~~ ### 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. ~~~{.yaml} - hosts: all (…) strategy: free ~~~ > *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 ~~~ {% if python_is_installed is defined %} Ansible devrait marcher -pardi! {% endif %} ~~~ Voir la doc pour plus de détails : ### Boucles dans fichier jinja2 ~~~ location / { {% for ip in allowed_ips %} allow {{ ip }}; {% endfor %} deny all; ~~~ ### Lire une entrée au clavier 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 : ~~~{.yaml} 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) : ~~~{.yaml} - include: './tasks/autre.yml' vars: prenom_de_autre: prenom ~~~ ### Exécuter un playbook en mode interactif ~~~ $ 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) ~~~{.yaml} - 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 ~~~{.yaml} - name: Début installation, envoie email run_once: true (…) ~~~ 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 ~~~{.yaml} - 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 : ~~~{.yaml} --- - 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 : ~~~{.yaml} 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 : ~~~{.yaml} 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](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#finding-first-matched-files) Permet de prendre le premier fichier trouvé : ~~~{.yaml} - 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écessaire * `changed` : 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 : ~~~{.yaml} - ansible.builtin.command: date changed_when: False ~~~ ### Voir variables disponibles ~~~ $ ansible -m ansible.builtin.setup 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 : ~~~{.yaml} --- - hosts: all gather_facts: true tasks: - ansible.builtin.debug: var: ansible_eth0.macaddress ~~~ 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](HowtoGPG). Au préalable, il faut configurer GPG pour utiliser l'[agent GPG](HowtoGPG#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 : ~~~ echo "VAULT_PASSWORD" | 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`. ### 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](https://docs.ansible.com/ansible/latest/playbooks_tests.html#version-comparison). Un cas concret : ~~~{.yaml} - 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` : ```bash ANSIBLE_FORCE_COLOR=true ansible-playbook playbook.yml 2>&1 | aha --black > output.html ``` Si on veut quand même avoir la sortie dans son terminal : ```bash 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](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_async.html). 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.: ```yaml - 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 `) : * 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 »](https://docs.ansible.com/ansible/latest/user_guide/) (voir notamment la partie [Best Practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)) * Vidéos [ansible-best-practices](https://www.ansible.com/ansible-best-practices) et [ansible-tips-and-tricks](https://www.ansible.com/ansible-tips-and-tricks) * [Ansible 101 - on a Cluster of Raspberry Pi 2s](https://www.youtube.com/watch?v=ZNB1at8mJWY) * Sysadmin Casts (épisodes [43](https://sysadmincasts.com/episodes/43-19-minutes-with-ansible-part-1-4), [45](https://sysadmincasts.com/episodes/45-learning-ansible-with-vagrant-part-2-4), [46](https://sysadmincasts.com/episodes/46-configuration-management-with-ansible-part-3-4) et [47](https://sysadmincasts.com/episodes/47-zero-downtime-deployments-with-ansible-part-4-4)) * [How Twitter uses Ansible](https://www.youtube.com/watch?v=fwGrKXzocg4) (AnsibleFest 2014) * [Orchestration with Ansible at Fedora Project](http://fr.slideshare.net/AdityaPatawari/ansible-33223245)