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’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.

# on initialise un role vide depuis le dossier roles
ansible-galaxy init common

cd common && 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 sus-mentionné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 !

# 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

# divers 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 :

---
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. Quand à 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.

# 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 con de devoir réécrire des tâches alors que nous avons déjà fait le boulot… Vous voyez la logique ?

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

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 :

# 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.

---
- 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êt 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 !

Déjà 2 réponses, rejoignez la discussion !

  • Seuf

    dit :

    Bonne intro à Ansible :D
    T’as un repo github ou on peut consulter les confs sshd / iptables que tu mets en place ?
    Je fait quasiment la même chose à la maison (mon role s’appele prepare au lieu de common), et ensuite j’installe docker et je build toutes les images à partir de mes dockerfiles.

    • Buzut

      dit :

      Hello,
      Yes dans tu trouveras ça dans le repo d’exemple. La conf iptables est un tout petit peu allégée par rapport à ce que j’ai en prod. J’ai dans ma TODO « écrire un article sur iptables ».

      En revanche, celle de ssh est exactement la même et le pourquoi des réglages est expliqué dans mon article sur ssh.

      Si t’as des remarques, surtout, n’hésites pas !

      Et je confesse ne pas être un grand utilisateur de Docker… je résiste à la mode.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *