Tutoriel pour Apprendre à utiliser Ansible

Ansible vous donne la possibilité d'automatiser l'installation, le déploiement et la gestion de vos serveurs.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum. 2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Automatiser la gestion des serveurs avec Ansible

Ansible est un outil qui permet - entre autres choses - d'automatiser l'installation, le déploiement et la gestion de vos serveurs. Vous utilisez certainement ssh pour installer les programmes dont vous avez besoin et configurer vos serveurs. Peut-être même avez-vous créé des scripts pour que tout ça aille plus vite. Ansible permet de créer des « Playbooks », qui ne sont autres que des scripts à la sauce Ansible, et permettent de configurer vos serveurs.

Sa grande force est qu'il est agentless, autrement dit, rien n'est à placer sur vos serveurs. Vous installez Ansible sur votre laptop par exemple, et le tour est joué. Vous pouvez ensuite lancer l'install de vos 40 serveurs de base de données en une seule commande ! Ça vous émoustille ? Alors allons-y !

I-A. L'install

Vous vous doutez bien qu'il faut avant tout installer Ansible sur votre control machine. Rien de bien compliqué, ça tourne sur à peu près tout sauf Windows et il y a plusieurs procédures au choix : du git clone au apt-get. Elles sont pour la plupart détaillées sur la page d'install de Ansible.

En ce qui me concerne, j'ai voulu tenter de l'installer sur mon Mac via Macports, pour plus de facilité et des mises à jour en toute souplesse. L'install se passe assez simplement via un sudo port install ansible. Sur Mac, et c'est valable aussi pour l'install via Homebrew, au lieu d'avoir la config dans /etc/ansible/ tout se passe dans /opt/local/etc/ansible/, ça vous évitera de chercher.

Dans le cas où vous suivriez les instructions d'installation de la doc Ansible - laquelle suggère de procéder à l'installation via PIP -, il n'y a pas de fichier de config par défaut. Pourtant, Ansible le cherchera dans /etc/ansible, de la même manière que le fichier hosts.

Deux solutions sont possibles. Soit vous créez un fichier .ansible.cfg dans votre HOME, et vous précisez bien dans celui-ci où se trouve le fichier hosts, soit vous créez /etc/ansible et vous y placez le fichier hosts.

I-B. Configuration des hôtes

Avant tout, il faut bien comprendre que Ansible repose sur le protocole ssh. Ainsi, c'est via ce protocole qu'il se connectera à vos serveurs. Par défaut sur le port 22 évidemment. Il tentera aussi par défaut de se connecter via une clef ssh de ~/.ssh/. De plus, l'utilisation d'OpenSSH permet de lire le fichier de configuration ~/.ssh/conf. Néanmoins, l'usage d'OpenSSH est conditionné par une version récente de ce dernier. Dans le cas contraire, Ansible fallback sur une bibliothèque en Phyton.

Les hôtes se configurent dans /etc/ansible/hosts :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
# les crochets permettent de définir des groupes
# on pourra ainsi appliquer la même conf à tous les serveurs de backup
# un serveur peut appartenir à plusieurs groupes 
[backup]
# on fait ici référence à un serveur présent dans le fichier conf de .ssh
jarjar

# définir un port non standard
www.buzut.fr:5309

# il est possible d'adresser plusieurs serveurs qui suivent un nommage spécifique
# en utilisant la notation intervalle des REGEX
www[01:50].buzut.fr

Beaucoup d'autres options sont disponibles, vous trouverez toutes les possibilités dans l'Inventory.

Une fois les hôtes configurés, vous pouvez tester une première commande afin de voir que la configuration fonctionne. Le module ping affiche simplement « pong », il n'a rien à voir avec le ping ICMP :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
ansible all -m ping
buzut | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

# il se peut que vous ayez un problème 
buzut | UNREACHABLE! => {
    "changed": false, 
    "msg": "SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue", 
    "unreachable": true
} 

# après le -vvv et quelques recherches, Ansible crée un dossier .ansible dans votre home
# il se peut que ce répertoire n'appartienne pas au bon utilisateur
# pour remédier à cela
sudo chown -R votre_user ~/.ansible

Dernier petit détail, nous avons entraperçu dans les commentaires qu'il y a de puissants moyens de sélectionner les hôtes. On peut en sélectionner un unique ou tout un groupe, appliquer des REGEX pour en exclure ou préciser un intervalle. Pour tirer toute la souplesse et la puissance de cette notation, lisez rapidement la page patterns de la doc.

I-C. La ligne de commande

Ansible possède une ligne de commande. Pourquoi donc ? Eh bien ! c'est la même chose que pour les scripts. Parfois vous faites un script parce que vous savez que vous aurez à refaire cette manipulation, parfois, c'est juste du one shot - ou c'est super court - donc vous tapez directement ce que vous voulez faire. Par exemple, admettons que vous vouliez redémarrer tous les serveurs de base de données, vous pourriez faire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
# database est notre groupe de serveur [database]
# l'option -a permet de préciser l'argument de la commande
ansible database -a "/sbin/reboot"

# par défaut, ansible lance la commande sur cinq serveurs en parallèle en faisant des forks de son processus
# si votre groupe contient beaucoup de serveurs - front[1-500] par exemple - il peut être judicieux de l'augmenter
# vous pouvez gérer cela avec le paramètre -f
ansible database -a "/sbin/reboot" -f 25

Vous pouvez utiliser l'option -u username pour exécuter une commande depuis un autre utilisateur, -k pour passer en root et entrer le mot de passe root.

La CLI permet d'appeler des modules pour exécuter les commandes que nous voulons. Dans l'exemple ci-dessus, nous n'avons rien précisé, car le module par défaut est command et c'est ce que nous voulions. Admettons maintenant que nous voulions copier un fichier sur l'ensemble de nos serveurs web :

 
Sélectionnez
# le module copy permet de transférer un fichier
ansible web -m copy -a "src=/Users/Buzut/Desktop/super-site.conf dest=/etc/apache2/sites-available/super-site.conf"

Vous pouvez en savoir plus sur les modules disponibles directement sur la page de docs dédiée.

I-D. Les playbooks

J'en ai rapidement parlé en introduction, les playbooks sont des fichiers en YAML qui décrivent les tâches qu'Ansible doit accomplir sur vos serveurs. Les playbooks utilisent une syntaxe très simple : on définit les hôtes, les variables éventuelles, puis on crée des tâches. Chaque tâche possède un nom et appelle des modules (les mêmes qu'en CLI).

Sachez que vous pouvez lancer vos playbooks avec l'option --syntax-check, qui comme son nom l'indique, s'assure qu'il n'y a pas d'erreur dans la syntaxe de vos playbooks. Ensuite, vous pouvez également utiliser --check afin de simuler un play sans effectuer aucun changement. Dans cette dernière option, Ansible va se connecter au(x) serveur(s) et lancer les modules en leur demandant de ne pas effectuer de modifications (tous les modules ne sont pas compatibles, auquel cas, ils ne retourneront pas les changements potentiels).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
---
# les documents YAML commencent toujours par "---"

# le nom de l'hôte ou groupe concerné
- hosts: webserver

  # on déclare les éventuelles variables
  vars:
    http_port: 80
    domain: buzut.fr

  # nom de l'utilisateur du compte (lance celui du .ssh/conf par défaut)
  remote_user: root

  # ici débute la liste des tâches
  tasks:

  # nom d'une tâche
  - name: ensure server is up to date
    # nom du module à utiliser
    apt:
      update_cache: yes
      upgrade: full

  # installons apache
  - name: install apache2
    # il existe une syntaxe alternative, plus condensée
    # apt: name=apache2 update_cache=yes state=latest

    apt:
      name: apache2
      update_cache: yes
      state: latest

  # s'assurer que le mod rewrite est actif (l'activer sinon)
  - name: enabled mod_rewrite
    apache2_module:
      name: rewrite
      state: present

  # le module template fonctionne de manière similaire à copy (vu plus haut)
  # mais il injecte dynamiquement les variables nécessaires
  - name: write the apache config file
    template:
      src: /Users/Buzut/.ansible/templates/vhost.conf
      dest: /etc/apache2/sites-available/buzut.conf

  - name: enable vhost
    command: a2ensite buzut

  - name: restart apache
    service:
      name: httpd
      state: restarted

Pour que vous compreniez bien le fonctionnement des variables, voici à quoi ressemble le fichier de template vhost.conf :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
<VirtualHost *:{{ http_port }}>
    ServerAdmin webmaster@{{ domain }}
    ServerName {{ domain }}
    ServerAlias www.{{ domain }}
    DocumentRoot /var/www/{{ domain }}

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Directory /var/www/{{ domain }}/>
       Options -Indexes +FollowSymLinks
       AllowOverride All
    </Directory>
</VirtualHost>

Comme expliqué en commentaire, le module template charge le fichier de template et pour chaque {{ variable }}, injecte la valeur correspondante depuis les variables définies au début. Vous rendez-vous compte de la puissance de la chose ?!

I-D-1. Les variables

Nous avons rapidement pu voir les variables dans la partie précédente. Néanmoins, Ansible fournit de nombreuses variables sur l'environnement serveur. Le module setup nous renseigne sur ces dernières :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
ansible buzut -m setup
buzut | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "37.59.21.45"
        ], 
        "ansible_all_ipv6_addresses": [
            "2001:41d0:8:852d::", 
            "fe80::225:90ff:fe7c:fa36"
        ], 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "01/03/2014", 
        "ansible_bios_version": "3.0a", 
        […]
        "ansible_lsb": {
            "codename": "trusty", 
            "description": "Ubuntu 14.04.4 LTS", 
            "id": "Ubuntu", 
            "major_release": "14", 
            "release": "14.04"
        }, 
       […]

Toutes ces variables peuvent être utilisées dans vos playbooks. On y accède de la manière suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
# variable simple
{{ ansible_architecture }}

# pour accéder à une propriété
{{ ansible_lsb.major_release }}

# pour accéder à un tableau (première propriété)
{{ ansible_all_ipv4_addresses[0] }}

Vous commencez à percevoir la puissance des variables. Mieux, on peut alimenter ces dernières directement depuis une commande exécutée sur le serveur ! Voyons donc cela :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
---
- hosts: buzut
  tasks:
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts

vhosts contient maintenant la liste des éléments présents dans /etc/apache2/sites-enabled/ telle que l'affiche la commande ls.

Il y a un type de variable un peu particulier qui peut s'avérer très utile : ce sont les variables prompt. Au moment de l'exécution, l'utilisateur qui lance le script se voit demander la valeur qu'il veut attribuer à la variable. Par exemple dans le cas d'un playbook permettant d'installer un nouveau serveur, inutile - peut-être même dangereux - de l'appliquer à all s'il n'y a qu'un seul nouveau serveur à paramétrer. Par ailleurs, c'est fastidieux d'éditer le playbook avant exécution. Il suffit donc de procéder comme ceci :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
    ---
    - hosts: "{{ servernames }}"
      vars_prompt:
        - name: "servernames"
          prompt: "Which hosts would you like to setup?"
          private: no
      tasks:
        []

Vous noterez que je précise ici private: no. En effet, par défaut, Ansible considère le prompt comme password-sensitive et n'affiche donc pas les caractères.

Enfin, sachez qu'il est également possible de passer des variables au playbook directement depuis la CLI au moment de l'invocation de ce dernier. Reprenons le même exemple que ci-dessus en enlevant le vars_prompt :

 
Sélectionnez
1.
2.
3.
4.
    ---
    - hosts: "{{ servernames }}"
      tasks:
        []

Il suffit d'appeler le playbook de cette manière : ansible-playbook baseServer.yml --extra-vars "servernames=db6". Sachez également que les variables prennent des guillemets lorsqu'elles débutent une ligne. Petite illustration :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
    # ici la variable doit être entourée de guillemets
    - hosts: "{{ servernames }}"
      tasks:
        []

    # mais pas dans ce cas 
    - name: Enable VirtualHost
      file:
        src: /etc/nginx/sites-available/{{ domain }}
        dest: /etc/nginx/sites-enabled/{{ domain }}
        state: link

I-D-1-a. Debug, le var_dump d'Ansible

Récupérer des variables c'est bien, savoir ce qu'il y a dedans, c'est mieux. C'est justement l'objet de debug. Reprenons l'exemple précédent :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
---
- hosts: buzut
  tasks:
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts
    - debug: msg="{{ vhosts }}"
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
---
# on lance le playbook
ansible-playbook test.yml 

PLAY ***************************************************************************

TASK [setup] *******************************************************************
ok: [buzut]

TASK [determine vhosts] ********************************************************
changed: [buzut]

TASK [debug] *******************************************************************
ok: [buzut] => {
    "msg": {
        "changed": true, 
        "cmd": [
            "/bin/ls", 
            "/etc/apache2/sites-enabled"
        ], 
        "delta": "0:00:00.014652", 
        "end": "2016-03-24 23:21:32.612755", 
        "rc": 0, 
        "start": "2016-03-24 23:21:32.598103", 
        "stderr": "", 
        "stdout": "buzut.conf\default.conf", 
        "stdout_lines": [
            "buzut.conf", 
            "default.conf"
        ], 
        "warnings": []
    }
}

PLAY RECAP *********************************************************************
cloud                      : ok=3    changed=1    unreachable=0    failed=0

Nous voyons ici que si nous voulons accéder à la valeur du premier vhost, il faut faire {{ vhosts.stdout_lines[0] }}, tout simplement.

Les variables présentent bien d'autres possibilités, on peut par exemple récupérer des informations sur un autre hôte. Je vous laisse découvrir toute cette magie directement dans la doc.

I-D-2. Les boucles

Vous avez pu constater que les variables peuvent avoir des propriétés et contenir des tableaux. Qui dit tableau dit boucle. Nous allons reprendre notre exemple précédent et effacer tous les vhosts déjà présents avant d'ajouter le nôtre. C'est parti :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
---
- hosts: webserver
  vars:
    http_port: 80
    domain: buzut.fr
  remote_user: root
  tasks:
    []
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts

    # on enlève le (ou les) vhosts par défaut
    - name: deregister default vhosts
      command: a2dissite {{ item }}
      with_items: 
        - "{{ vhosts.stdout_lines }}"

    # maintenant on peut rajouter notre vhost
    - name: enable vhost
      command: a2ensite buzut
    []

Bien entendu, les boucles recèlent encore bien d'autres secrets, et comme à mon habitude, je vous laisse avec la doc si vous souhaitez creuser le sujet.

I-D-3. Le notify pattern

Qu'est-ce donc que cela me direz-vous ? On peut dire que le notify pattern est la programmation événementielle Ansible flavoured en quelque sorte. OK, je m'explique. Plutôt que de lancer une action plusieurs fois , comme de redémarrer Apache - après l'activation d'un module ou la modification d'un fichier de config, les handlers ne sont lancés que si le fichier de conf change réellement. En outre, bien que plusieurs tâches puissent nécessiter une même action, l'action en question ne sera lancée qu'après l'exécution de tous les blocs tâches.

Illustrons ce comportement en reprenant notre exemple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
---
- hosts: webserver

  vars:
    http_port: 80
    domain: buzut.fr
  remote_user: root

  tasks:
    []
    - name: determine vhosts
      command: /bin/ls /etc/apache2/sites-enabled/
      register: vhosts

    - name: deregister default vhosts
      command: a2dissite {{ item }}
      with_items: 
        - "{{ vhosts.stdout_lines }}"
      notify:
        - restart apache2

    - name: enabled mod_rewrite
      apache2_module:
        name: rewrite
        state: present
      notify:
        - restart apache2

    - name: enable vhost
      command: a2ensite buzut
      notify:
        - restart apache2
    []

    handlers:
    - name: restart apache2
      service: 
        name: apache2 
        state: restarted

Ainsi, dès lors que nos anciens vhosts sont effacés, que le nouveau est installé et que le mod_rewrite est activé, Apache sera redémarré. Si nous avions référencé directement le module apache2 avec l'instruction de redémarrer dans chacun des blocs de tâche, nous aurions redémarré Apache trois fois…

À ce stade, comme tout notre code est dans le même playbook, il est vrai que l'intérêt de notify s'avère plutôt limité. Dans un cas comme celui-ci, il nous suffit en effet de redémarrer Apache une fois en fin de fichier et le tour est joué. Cependant, lorsque nos playbooks sont plus complexes, on les sépare en plusieurs morceaux logiques, qui peuvent ainsi être réutilisables dans d'autres playbooks. Là, tout de suite vous percevez sans doute bien mieux l'intérêt de notify.

Puisque l'on parle de séparer nos playbooks en différentes parties, il est temps d'introduire les includes !

I-E. Meilleure organisation avec include

L'include dans Ansible, c'est exactement comme le include de PHP. Cela vous permet de scinder vos tâches en différents fichiers et de les importer au besoin dans vos playbooks. Imaginez par exemple que vous ayez une tâche qui se charge d'installer un dæmon de monitoring, vous voudrez certainement qu'elle s'exécute aussi bien sur vos serveurs de base de données, que sur vos serveurs front, etc.

Sans l'include, vous devriez répéter ça dans tous vos playbooks, tandis qu'avec cette petite magie, une simple référence suffit.

On définit donc nos tâches dans un fichier dédié, on l'appelle pour l'exemple setupMonitoring.yml.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
---
- name: Install monitoring agent
  apt:
    name: blabla

# on ajoute toutes les tâches que l'on veut

Comme ce fichier sera intégré directement dans un playbook, nous n'avons pas à référencer tasks:, nous plaçons directement notre liste de tâches.

Une fois ce fichier créé et enregistré, admettons pour l'exemple que nous enregistrions toutes nos tâches dans tasks et que les playbooks soient à la racine, voici à quoi ressemblerait notre playbook :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
---
- hosts: dbservers
  vars:
    email: mon@email.fr

  vars_prompt:
    - name: "dbrootpasswd"
      prompt: "Database root password"

  tasks:
    - include: tasks/commonSetup.yml
    - include: tasks/setupMonitoring.yml
    - include: tasks/installMySQL.yml

    # on peut bien entendu mélanger des includes et des tâches classiques
    - name: Install htop
      apt:
        name: htop

Il y a quelque chose que je ne vous ai pas dit concernant les includes. Nous avons vu comment inclure des tâches, mais l'on peut aussi inclure des playbooks. Tout dépend de l'endroit où l'include est utilisé.

Dans l'exemple précédent, nous l'avons inséré après tasks:, il est donc évident qu'il ne peut inclure que des tâches. Cependant, s'il est inséré au premier niveau du playbook, il insèrera un playbook. Il est ainsi possible de créer des meta-playbooks. Attention cependant, car la syntaxe de ces fichiers devra être celle d'un playbook !

C'est simple, mais redoutable de puissance ! Depuis sa version 1.2, Ansible a mis en place un mécanisme qui pousse encore plus loin cette logique afin de rendre les playbooks plus organisés, plus clairs et plus réutilisables, j'ai nommé : les rôles.

Les rôles constituent un moyen d'automatiser le chargement des variables, des tâches et des handlers grâce à une convention d'arborescence de fichiers. C'est une automatisation des includes qui permet une grande souplesse et une très bonne organisation des tâches complexes. J'y consacre un article entierTirer toute la puissance d'Ansible avec les rôles !

I-F. Les objets de valeur au coffre

C'est une bonne pratique de versionner ses scripts Ansible. Cependant, qui dit versionning, dit souvent dépôt distant. Il va sans dire que certains éléments de configurations ne sont pas à laisser en clair sur n'importe quel dépôt : clef ssh, fichiers de conf avec mot de passe, etc.

D'une part, tous les collaborateurs n'ont pas forcement à y avoir accès, d'autre part, un Gitlab ou Github, même avec un dépôt privé et même s'il est installé sur vos serveurs, peut toujours être compromis.

Bien entendu, séparer les fichiers sensibles est envisageable. Mais vous n'aurez pas le confort d'avoir l'intégralité de vos éléments d'install ou deploy d'un simple petit coup de git clone, ou équivalent avec SNV ou Mercurial.

Pour répondre à cette problématique, Ansible propose un coffre - Vault dans la langue de Shakespeare. Vous spécifiez un mot de passe et Ansible chiffre le fichier (par défaut en AES). Lors de l'édition de fichiers, Ansible ouvrira votre l'éditeur défini dans la variable $EDITOR. Veillez à en définir un si ce n'est pas fait. Le cas échéant, votre fichier sera ouvert avec vi.

L'usage du vault est très simple. Vous n'aurez que quatre commandes à retenir :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
# chiffrer des fichiers
ansible-vault encrypt fichierA [fichierB …]

# afficher un fichier
ansible-vault view fichierA [fichierB …]

# éditer un fichier déjà chiffré
ansible-vault edit fichierA

# si jamais vous avez envie de déchiffrer un fichier précédemment chiffré
ansible-vault decrypt ficherA [fichierB …]

Lorsque vous désirez lancer un playbook qui nécessitera d'utiliser des fichiers présents dans le vault, il faudra passer l'option --ask-vault-pass.

Enfin, il est possible d'utiliser des mots de passe différents pour différents fichiers. Cependant, tous les fichiers utilisés au sein d'un même Playbook doivent partager le même mot de passe.

I-G. Performances

Négligeable quand vous n'avez que quelques serveurs à traiter, les réglages qui influent sur les performances peuvent avoir un impact important en termes de temps d'exécution si vous gérez un parc de serveurs important. Passons en revue les optimisations qui permettent de doper les performances d'Ansible. Tout va se passer dans le fichier de config /etc/ansible/ansible.cfg ou /opt/local/etc/ansible/ansible.cfg.default (qu'il faudra d'ailleurs renommer pour lui enlever .default) sur OS X.

I-G-1. Forks

Nous en avons déjà rapidement parlé, Ansible gère par défaut les hôtes cinq par cinq. Ce qui veut dire qu'il attendra que l'exécution de vos instructions soit terminée sur vos cinq serveurs avant de poursuivre sur d'autres. Vous pouvez évidemment, c'est ce que nous avons vu, préciser autre chose avec l'option -f. Néanmoins, si vous souhaitez toujours vous adresser à plus de serveurs en parallèle, autant modifier ce paramètre dans le fichier de configuration et ne plus avoir à la spécifier manuellement à chaque fois. Nous sommes là pour automatiser nos tâches après tout ! Il n'y a pas de règle spécifique, les deux principaux facteurs limitants seront la charge CPU et la charge réseau engendrées. Le réglage de cinq par défaut est extrêmement conservateur, si votre machine est décemment récente et que vous avez autre chose qu'un modem 56K, vous pouvez tout à fait passer cette variable à 20 et ajuster après avoir testé.

 
Sélectionnez
forks=20

I-G-2. Pipelining

Le pipelining permet de réduire le nombre de connexions ssh nécessaires. Par conséquent, la vitesse d'exécution des playbooks s'en trouvera grandement améliorée. Il est désactivé par défaut, car certaines configurations nécessitant requiretty ne sont pas compatibles, auquel cas il est possible d'utiliser le mode accéléré. Pour activer le pipelining :

 
Sélectionnez
pipelining=True

I-G-3. Exécution asynchrone et polling

Ansible se connecte à vos serveurs en ssh et ne rend la connexion qu'une fois toutes les actions effectuées. Ainsi, le comportement par défaut est bloquant, et maintenir de nombreuses connections ssh ouvertes « pour rien » impacte négativement les performances. Il est cependant possible de lancer des opérations et de faire du polling afin de contrôler à intervalles réguliers l'état des processus ainsi lancés. De cette manière, vous pourrez lancer vos tâches sur plus de serveurs en parallèle.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
# compilation du codec video x264, qui peut être assez longue
- name: compile x264
  environment: ffmpeg_env
  command: "{{ item }}"
  args:
    chdir: "{{ ffmpeg_source_dir }}/x264"
    creates: "{{ ffmpeg_bin_dir }}/x264"
  with_items:
    - ./configure --prefix={{ ffmpeg_build_dir }} --bindir={{ ffmpeg_bin_dir }} --enable-static
    - make
    - make install
    - make distclean
  # on déclare le temps maximum d'exécution
  async: 120
  # et intervalle de temps auquel vérifier l'état de l'opération
  poll: 10

Il est également possible de spécifier poll à 0. Auquel cas on lance l'opération sans en vérifier le statut, en présupposant que le résultat sera celui auquel on s'attend.

Avant de conclure, puisque nous visons à automatiser au maximum la gestion de nos systèmes, j'ai écrit un petit script pre-ansible, dispo sur Github. Ce dernier permet de configurer l'ajout d'un nouveau serveur au système : ajout dans la config ssh, ajout au fichier hosts d'Ansible et paramétrage du ssh du serveur pour une connexion automatique par clef ssh.

En guise de conclusion, je vous laisse avec un exemple de playbook assez fourni. Il s'agit d'un article de Digital Ocean sur la configuration d'un serveur Apache avec Ansible.

Alors, est-ce qu'Ansible va révolutionner votre vie ? Quel est selon vous son plus gros atout ?

II. Tirer toute la puissance d'Ansible avec les rôles

Ansible est absolument génial. Il permet d'automatiser l'installation et la maintenance de machines et d'infrastructures complètes. J'ai déjà consacré un article à la prise en main d'AnsibleAutomatiser la gestion des serveurs avec Ansible, nous nous concentrerons ici sur les rôles.

Les rôles représentent une manière d'abstraire les directives includes. C'est en quelque sorte une couche d'abstraction. Grâce aux rôles, il n'est plus utile de préciser les divers includes dans le playbook, ni les paths des fichiers de variables, etc. Le playbook n'a qu'à lister les différents rôles à appliquer et le tour est joué !

En outre, depuis les tasks du rôle, l'ensemble des chemins sont relatifs. Inutile donc de préciser l'intégralité du path lors d'un copy, template ou d'une tâche. Le nom du fichier suffit, Ansible s'occupe du reste.

On peut faire beaucoup avec les playbooks et les includes, cependant, lorsque nous commençons à gérer des infrastructures complexes, on a beaucoup de tâches et les rôles s'avèrent salvateurs dans l'organisation et l'abstraction qu'ils apportent.

Par ailleurs, Ansible met à disposition une plate-forme permettant de télécharger et de partager des rôles utilisateurs : Ansible galaxy. Un bon moyen de ne pas réinventer la roue.

Pour illustrer cet article, nous allons préparer différents rôles et les assembler dans un playbook afin d'installer un simple serveur LAMP (testé sur Ubuntu 16.04 uniquement, mais l'idée est là). Pour faciliter le suivi, j'ai mis l'ensemble sur un repo Github. C'est parti !

En CLI, on peut utiliser ansible-galaxy afin de préparer l'arborescence d'un rôle vide. Nos playbooks seront à la racine de notre dossier tandis que les rôles seront dans roles. Ainsi, lorsque nous appellerons un rôle depuis un playbook, sans avoir besoin de préciser autre chose que son nom, Ansible saura où chercher.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
# on initialise un role vide depuis le dossier roles
ansible-galaxy init common

cd common && ls -R
ls -R
README.md    handlers    tasks        vars
defaults    meta        tests

./defaults:
main.yml

./handlers:
main.yml

./meta:
main.yml

./tasks:
main.yml

./tests:
inventory    test.yml

./vars:
main.yml

On voit ici qu'Ansible nous a créé plusieurs dossiers avec un fichier main.yml dans chacun d'eux. Par défaut, Ansible ira chercher les informations dans les main.yml de chaque répertoire. Cependant, vous pouvez créer d'autres fichiers et les référencer dans vos instructions. Par exemple, il est tout à fait possible de créer une autre tâche dans tasks et de l'inclure depuis tasks/main.yml. Passons rapidement en revue la fonction de chaque répertoire.

La commande ansible-galaxy est normalement destinée à être utilisée avec Galaxy, nous en tirons cependant profit pour facilement obtenir un boilerplate de rôle vide. Par défaut, elle ne créée pas les répertoires files et templates dont nous allons tout de même parler.

defaults

  • Ce sont ici les variables par défaut qui seront à disposition du rôle.

vars

  • De la même manière que defaults, il s'agit ici de variables qui seront à disposition du rôle, cependant, celles-ci ont en général vocation à être modifiées par l'utilisateur et elles prennent le dessus sur celles de defaults si elles sont renseignées.

tasks

  • Sans grande surprise, c'est ici que vous référencerez vos tâches.

files

  • Tous les fichiers étant destinés à être traités par le module copy seront placés ici.

templates

  • Idem que copy, mais cela concerne les fichiers du module template.

meta

  • Il y a ici plusieurs usages, notamment dans le cas de rôles publiés sur Galaxy. Dans notre cas, on référencera ici les dépendances à d'autres rôles.

tests

  • Pas utile pour nous, c'est seulement pour Galaxy et la doc n'est pas très loquace à ce propos…

En plus des répertoires susmentionnés, il y a le README qu'il est de bon ton de renseigner afin d'expliquer comment utiliser le rôle, quelles sont les variables à définir, etc.

Aucun des répertoires n'est impératif - quoique sans tasks, notre rôle ne sert pas à grand-chose. On effacera donc les répertoires dont on n'a pas l'utilité.

On est parti pour la création des différents rôles utiles à notre serveur LAMP. On commence avec le rôle common (créé précédemment), qui sert de base à l'ensemble de nos serveurs. Cette tâche est assez fournie, ça permet d'avoir un exemple de diverses choses qu'on peut faire, vous pouvez néanmoins la lire en diagonale, le but n'est pas de configurer un serveur, c'est de découvrir les rôles Ansible !

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
# tasks/main.yml
---
# maj du système qui est fraichement installé
- name: Update & upgrade system
  apt:
    update_cache: yes
    upgrade: dist

# souvent livré avec un mdp root envoyé par mail
# on en génère un aléatoire avant de le remplacer
# on va configurer ssh pour n'accepter que les clefs
- name: Generate password
  command: openssl rand -base64 32
  register: randomPass

- name: Change root passwd
  shell: echo "root:{{ randomPass }}" | chpasswd

# diverses choses pouvant servir sur tous les serveurs
- name: Install base soft (transport-https, fail2ban)
  apt:
    name: "{{ item }}"
  with_items:
    - apt-transport-https
    - software-properties-common
    - fail2ban
    - exim4
    - htop
    - glances
    - iotop
    - unattended-upgrades


# on configure les maj auto de secu
# on pourrait ici très bien utiliser un fichier de template au lieu de remplacer à coup de regexp
- name: Configure unattended-upgrades
  replace:
    dest: /etc/apt/apt.conf.d/50unattended-upgrades
    regexp: "{{ item.regexp }}"
    replace: "{{ item.replace }}"
  with_items:
    - { regexp: '//Unattended-Upgrade::Mail "root";', replace: 'Unattended-Upgrade::Mail "{{ email }}";' }
    - { regexp: '//Unattended-Upgrade::MailOnlyOnError "true";', replace: 'Unattended-Upgrade::MailOnlyOnError "true";' }
    - { regexp: '//Unattended-Upgrade::Remove-Unused-Dependencies "false";', replace: 'Unattended-Upgrade::Remove-Unused-Dependencies "true";' }

# ici, on upload directement le fichier de config qui va bien
- name: Enable unattended-upgrades
  copy:
    src: 20auto-upgrades
    dest: /etc/apt/apt.conf.d/20auto-upgrades

# on remplace tout bonnement la config ssh par défaut
- name: Upload ssh server config file
  copy:
    src: sshServerConfig
    dest: /etc/ssh/sshd_config
    mode: 0644

# on ajoute un script d'init iptables
- name: Install iptables.sh
  copy:
    src: iptables.sh
    dest: /usr/local/sbin/iptables.sh
    mode: 0744

# qui se lancera au boot
- name: Make iptable start on boot
  copy:
    src: rc.local
    dest: /etc/rc.local
    mode: 0755

# fail2ban c'est bien aussi, mangez-en
- name: Upload fail2ban config file
  copy:
    src: fail2ban.conf
    dest: /etc/fail2ban/jail.d/defaults-debian.conf
    mode: 0644

# les alias pour récupérer les emails
- name: Configure /etc/aliases
  lineinfile:
    dest: /etc/aliases
    regexp: ^root:.*
    line: "root: {{ email }}"

# on configure exim4
- name: Configure exim4
  replace:
    dest: /etc/exim4/update-exim4.conf.conf
    regexp: "dc_eximconfig_configtype='local'"
    replace: "dc_eximconfig_configtype='internet'"

On voit déjà que là, on fait pas mal de choses (j'ai bien allégé par rapport à ce que j'utilise réellement) qui seront communes à absolument tous nos serveurs. Un rôle qui sera donc utilisé absolument partout.

Vous notez certainement qu'il y a dans cette tâche des variables et des fichiers importés. Il faut ajouter tout cela, sinon Ansible balancera une erreur.

Concernant les variables, il s'agit simplement de l'email, pas de defaults ici, car il faut que l'email soit renseigné, il ne peut pas être « par défaut ». Nous n'avons donc pas l'utilité de defaults et nous mettons cette variable directement dans vars/main.yml :

 
Sélectionnez
---
email: mon@email.fr

Point de vars:, Ansible sait qu'il s'agit de variable, car c'est dans le dossier vars, what else?

Il y aurait pas mal de choses à dire sur cette conf, concernant ssh, on ne pourra se connecter qu'avec une clef ed25519. Pour en savoir plus sur la config ssh, je vous invite à lire mon article dédié, idem pour fail2ban. Quant à iptables, il ferme tout sauf le web et ssh et quelques services de base (DNS, DHCP, NTP…).

files contient naturellement l'ensemble des fichiers que nous avons besoin d'uploader. Vous trouverez leur contenu sur le git de l'article.

 
Sélectionnez
# voici quand même la liste
ls files/
20auto-upgrades    fail2ban.conf    iptables.sh    rc.local    sshServerConfig

Nous n'allons pas détailler tous les rôles un à un, car vous les retrouverez sur le git, nous allons créer un rôle nommé lampserver, qui se chargera d'installer PHP, Apache2, MySQL et letsencrypt. Comme vous vous en doutez, chacune des différentes briques constitue un rôle en elle-même. Nous pourrions vouloir installer letsencrypt avec Nginx en reverse d'un Node.js. Il serait un peu bête de devoir réécrire des tâches alors que nous avons déjà fait le boulot… Vous voyez la logique ?

On commence donc par créer notre meta/main.yml dans lequel on référence nos dépendances :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
dependencies:
  - { role: letsencrypt }
  - { role: mariadb }
  - { role: apache }

# notez qu'il est possible de passer des paramètres directement aux rôles
  - { role: letsencrypt, withCerts: true }

# très intéressant également, il est possible de lister des rôles depuis git
# le tag est optionnel, mais représente une option à connaître également
- { role: 'git+https://gitlab.com/user/rolename,v1.0.0'}

Eh oui, tout simplement. On précise ici que ce rôle dépend des rôles letsencrypt, mariadb et apache qui devront donc être exécutés avant.

Ces rôles sont bien entendu présents dans roles au même niveau que les autres (sauf s'ils sont référencés via une URL).

Notre tâche est ensuite tout ce qu'il y a de plus classique :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
# tasks/main.yml
---
# à ce stade, apache est déjà installé, car ce rôle précise ses dépendances
- name: Install php and common php ext
  apt:
    name: "{{ item }}"
  with_items:
    - libapache2-mod-php
    - php
    - php-curl
    - php-gd
    - php-json
    - php-mbstring
    - php-mysql
    - php-xml

- name: Enable required apache modules
  apache2_module:
    name: "{{ item }}"
    state: present
  with_items:
    - expires
    - headers
    - http2
    - rewrite
    - ssl

Enfin, il ne nous reste plus qu'à créer notre playbook et à donner les instructions pour l'exécution des rôles.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
---
- hosts: webserver
  roles:
     - common
     - lampserver

     # il est possible de passer des variables aux rôles ici
     - { role: foo, myvar: 'blabla' }

  # ou d'en définir au niveau global
  # elles seront prises en compte dans les rôles
  # vars:
    # blurp: test

  # ici, on demande directement à l'exécution de renseigner une variable
  vars_prompt:
    - name: "mysqlRootPass"
      prompt: "password for MySQL root"

Attention cependant à la priorité des variables. Dans le cas d'une variable référencée à la fois dans le playbook et dans vars du rôle avec deux valeurs différentes, c'est vars qui l'emporte. Néanmoins, la référence dans le playbook l'emporte sur defaults. En outre, il peut être très pratique de référencer une variable dans le playbook. Par exemple, l'email sera certainement requis dans de nombreux rôles, autant ne le déclarer qu'une seule fois.

Nous avons à peu près fait le tour des rôles. J'espère que vous y voyez un peu plus clair, et surtout, que vous êtes convaincu d'en user et d'en abuser !

III. Note de la rédaction de Developpez.com

Nous tenons à remercier Quentin Busuttil qui nous a aimablement autorisés à publier ses tutoriels : Automatiser la gestion des serveurs avec Ansible et Tirer toute la puissance d'Ansible avec les rôles. Nous remercions également Winjerome pour la mise au gabarit et Claude Leloup pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Quentin Busuttil. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.