Ansible: Say "hello" to my little friend!

Dans l'article d'iperf j'exposais l'outil comme un élément de torture. Ici avec l'outil que je vais vous présenter, on est un peu plus pool party, piscine à boules, pistolet à eau et t-shirts blancs mouillés en plein cagnard au mois d'août dans une villa à Cannes. Ou comment mesurer le niveau de délire du sysadmin !

Et c'est donc d'Ansible que je vais vous parler, qui est un outil de déploiement de configurations et d'automatisation. Qui peut être comparé à Chef ou Puppet.

Le but ici n'est pas de vous montrer comment il faut utiliser Ansible, mais de vous montrer un scénario d'utilisation et un exemple de projet. J'utilise Ansible depuis environ trois ans principalement pour des déploiements qui sont amenés à être répétés et réutiliser (ex: configurer un frontal web), mais aussi pour des re-configurations sur des groupes de machines et mise en conformité de configurations voir même pour le déploiement de mises à jour de paquets.

On le verra dans cet article, mais si je devais résumer Ansible je dirais qu'il est simple et rapide à prendre en main, offre une très grande flexibilité et est agentless. Le projet de présentation sera utilisé depuis une machine sous Ubuntu avec deux serveurs cible, un sous Debian l'autre sous Centos, mais il est possible d'utiliser Ansible sous d'autres systèmes Unix/Linux, FreeBSD, MacOSX et Windows (cible uniquement).

Dans chaque partie il y aura des exemples d'utilisations et différentes possibilités, mais il y a tellement de façon de faire, de possibilités et de modules dans Ansible qu'un article ne pourra jamais tout couvrir. C'est pourquoi je vous invite vivement à consulter la documentation d'Ansible. Le projet présenté est compatible Ansible 2.4.

T'es chiant avec tes tools, moi j'veux un install.sh

Un script Bash pourra faire ce que fait Ansible,mais moins efficacement et moins rapidement dans le cas de configurations et d'automatisation de déploiement

Imaginons un instant un scénario de déploiement:

  • Installation de plusieurs paquets de base: rsync, vim, unzip
  • Déploiement d'un paquet spécifique: mysql-server
  • Déploiement de configurations de base: hostname, locales, timezone, motd
  • Déploiement de configurations spécifiques: cron, ajout d'users SQL, création de bases SQL.
    Le tout sur un serveur Debian et un autre CentOS.

Dans le cas d'un script bash (ou perl, Ruby, ...), ça va vite devenir long, source d'erreurs, sans contrôle d'erreurs ou de gestion de retours (ou alors ça va devenir encore plus long, avec encore plus de source d'erreurs).
Mais aussi:

  • On a autre chose à faire que de taper du bash pour faire des déploiements.
  • Les notions d'inventaire et de variables dynamiques, sont inexistantes.
  • Déployer le script de déploiement partout...
  • Parce que rm -rf /$mavariable c'est tellement la loose
  • et que travailler comme ça c'est tellement " So 90' " (mais après tout qui suis-je pour le dire ;))

    Photo by David Kovalenko / Unsplash

Et quand bien même vous arrivez à passer tous ces points, vous serez le seul à réellement comprendre votre script et serez le seul capable(ou pas) de le maintenir dans le temps (personne ne veut toucher a de vieux scripts bash de déploiement).

Dans le cas d'Ansible, ce sera très rapide, facile à maintenir, à améliorer et à comprendre. Rapide à (ré)utiliser sur des groupes entiers de serveurs avec des OS différents (exemple: Debian + CentOS).
On aura une gestion d'erreurs, des retours d'exécution, le déploiement se fera rapidement depuis un point central, avec un inventaire de serveurs et une configuration par serveur, entre autre.

Photo by Daniela Cuevas / Unsplash

Le projet

Ici le but n'est pas de vous donner LE projet à faire ni de vous donner LA bonne façon de faire, mais de vous montrer un exemple de projet Ansible possible et utilisable. Le projet aura pour but d'effectuer les tâches lister dans le scénario de déploiement imaginé juste avant.

L'arborescence

Ansible à pour bon goût de nous offrir une page de Best Practice nous allons nous inspirer de cette page pour définir notre arborescence, en faite nous allons même la copier puisqu'elle fait partie des bestpractice et qu'elle est très bien pensée, mais libre à vous de l'adapter.

Pour cette présentation j'ai crée l'arborescence suivante:

.
├── configuration.yml
├── group_vars
│   ├── all
│   ├── centos
│   ├── debian
│   └── mysql-server
├── host_vars
│   ├── hostcentos
│   └── hostdebian
├── LICENSE
├── mon_inventaire
├── README.md
└── roles
    ├── common
    │   ├── files
    │   ├── handlers
    │   │   └── main.yml
    │   ├── tasks
    │   │   ├── centosbase.yml
    │   │   ├── debianbase.yml
    │   │   └── main.yml
    │   └── templates
    │       └── motd.j2
    └── mysql
        ├── files
        └── tasks
            └── main.yml
  • configuration.yml: correspond à un des playbooks du projet (ici on en a qu'un pour l'instant)
  • group_vars: contient un fichier de variables d'un group spécifique, ici mysqlserver
  • host_vars: contient des fichiers de variables de différents serveurs
  • mon_inventaire: notre inventaire ansible
  • roles: contient les différents rôles (ré)utilisables pour nos playbooks. Nous reviendrons sur les différents répertoires d'un rôle plus tard. C'est dans nos rôles que nous allons créer nos différentes tâches de configuration et déploiement (ex: installation d'un paquet).

Le playbook

Le playbook va organiser notre déploiement et nos différents rôles. Ce fichier peut contenir des tâches, des variables, des rôles, etc.

Ici notre playbook va récupérer des Gather Facts, déployer le rôle common pour tous les serveurs et déployer le rôle mysql pour le groupe de serveur mysql-server.

Le playbook (fichier: configuration.yml):

---
# playbook de presentation https://noskillbrain.fr/2017/10/18/ansible/

- name: Récupération des gather facts
  hosts: all
  user: root
  gather_facts: yes

- hosts: all
  roles:
    - common

-  hosts: mysql-server
   roles:
    - mysql

L'inventaire

Le fichier d'inventaire va contenir la liste de nos serveurs sur lesquels nous effectuerons des déploiements. Chaque serveur sera organisé dans des groupes qui peuvent être organisés dans des sous-groupes et ainsi de suite.

Notre inventaire restera très simple, mais donne déjà un bon exemple de ce qui est possible de faire.

L'inventaire (fichier: mon_inventaire):

[linux-server]
hostcentos ansible_host=192.168.2.87
hostdebian ansible_host=192.168.2.109

[debian]
hostdebian

[centos]
hostcentos

[mysql-server]
hostdebian

Comme on le voit ici, j'ai mes deux serveurs hostcentos et hostdebian déclarés dans le groupe linux-server. Chaque serveur est également déclaré dans un autre groupe: debian pour le serveur sous Debian, centos pour le serveur sous CentOS.

Et il y a également un dernier groupe, mysql-server dans lequel est déclaré uniquement le serveur sous Debian. A ce stade ça devient déjà plus clair, on peut déjà se spoiler en se disant que c'est le serveur Debian qui va recevoir le déploiement et la configuration du rôle mysql.

A noter qu'un inventaire n'est pas unique. Nous pouvons en avoir plusieurs, selon les projets, selon les besoins (ex: Production vs Pré-production, Cluster LAMP vs Serveurs dédiés). Mais gardez également à l'esprit qu'avoir un inventaire complet centralisé, contenant toutes vos machines organisées dans des groupes, va vous faire gagner en souplesse et en efficacité dans vos déploiements.

Premier point

A ce stade, en quelques lignes très simples, claires et facilement compréhensibles, on a déjà rangé nos serveurs dans différents groupes de notre inventaire et nous allons obtenir tout un tas d'informations avec les gather facts sur chaque serveur (version de l'OS, hostname, informations sur le CPU, ...) et surtout réutiliser ces informations.

Photo by NEX6ki / Unsplash

Du côté de notre script install.sh, on a également bien avancé, voyez par vous même:

#!/bin/bash

mon_serveur=$1
type=$2

echo "Le serveur ${mon_serveur} est de type ${type}."


Photo by Justine DePaolis / Unsplash

Les variables

Nos variables, au même titre que des variables dans un script bash, vont nous permettre de définir des valeurs de configurations pour les différentes tâches.

Dans Ansible les variables peuvent être définies à différents endroits: dans les playbooks, dans les rôles, dans l'inventaire, dans des fichiers séparés, en ligne de commande, etc. Concernant les variables et leur organisation, il faut savoir qu'il y a une priorité, selon où se trouve la variable, cette notion de priorité est très bien détaillée dans la documentation d'Ansible: http://docs.ansible.com/ansible/latest/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable.

Dans un premier temps nous allons analyser les variables du serveur, contenu dans fichier host_vars/hostdebian de notre projet:

---
# fichier de variables de configuration pour le serveur hostdebian
# https://noskillbrain.fr/2017/10/18/ansible/

hostname: hostdebian
ip_address: 192.168.2.109

Comme on peut le voir, nous avons défini deux variables pour notre serveur, qui sont hostname et ip_address. Nous pouvons donc maintenant utiliser ces variables de configuration dans nos différents rôles et tâches.

Le fichier pour le serveur sous CentOS se présente de la même manière, seules les valeurs changent:

---
# fichier de variables de configuration pour le serveur hostcentos
# https://noskillbrain.fr/2017/10/18/ansible/

hostname: hostcentos
ip_address: 192.168.2.87

Maintenant prenons notre fichier group_vars/all, ce fichier est spécifique dans Ansible puisque chaque variable définie dans ce fichier sera définie pour tous les serveurs de notre inventaire.

---
locales: 
  - fr_FR.UTF-8
  - en_US.UTF-8

default_locale: fr_FR.UTF-8
timezone: Europe/Paris
common_packages:
  - vim
  - rsync
  - unzip

packages:
  - "{{ common_packages }}"
  - "{{ more_packages }}"

Dans ce fichier nous avons définie plusieurs variables, dont certaines sont des listes, comme la variable locales. Ansible gère différents types comme les listes, les dictionnaires, qui peuvent être très utiles pour effectuer des boucles sur des tâches.

Pour finir, prenons notre fichier group_vars/debian:

---
more_packages:
  - locales

Et notre fichier group_vars/centos:

---
more_packages:
  - epel-release
  - MySQL-python

Chaque fichier à la même variable more_packages mais avec des valeurs différentes. Ainsi le serveur Debian aura des paquets différents du serveur CentOS.

A ce stade nous avons donc nos deux serveurs présents dans l'inventaire et avec leur configuration définie dans les différents fichiers de variable. Il nous reste plus qu'à exploiter toutes ces informations pour exécuter des tâches sur nos serveurs. C'est là qu'interviennent les rôles.

Photo by Geran de Klerk / Unsplash

Les rôles

Les rôles sont composés (le plus souvent) de plusieurs tâches à exécuter sur le serveur et peuvent également contenir des fichiers ou des templates de fichier de configuration qui peuvent être envoyés sur le serveur.

Nous allons analyser notre rôle common qui va exécuter les tâches suivantes:

  • Installation de paquet (selon l'OS)
  • Génération des locales (selon l'OS)
  • Configuration de la locale par défaut (selon l'OS)
  • Configuration de la timezone
  • Déploiement d'un motd templatisé
  • Création d'une tâche cron
    avec une configuration et des informations différentes par serveur, rendu possible avec les variables et les informations récupérées du serveur(gather facts).

Notre fichier roles/common/task/main.yml se compose ainsi et effectue les tâches indiquées plus haut:

---

# Tâches pour Debian 9 ou supérieur
- import_tasks: debianbase.yml
  when: ansible_os_family == 'Debian' and ansible_distribution_major_version | int >= 9

# Tâches pour CentOS 7 ou supérieur
- import_tasks: centosbase.yml
  when: ansible_os_family == 'RedHat' and ansible_distribution_major_version | int >= 7

# Module TIMEZONE: http://docs.ansible.com/ansible/latest/timezone_module.html
- name: Configuration de la timezone
  timezone:
    name: "{{ timezone }}"

- name: Déploiement du motd
  template:
    src: motd.j2
    dest: /etc/motd
  tags: motd
  notify: restart ssh

- name: Déploiement d'une tâche cron 
  cron:
    name: "Tache cron de test"
    minute: "12"
    hour: "12"
    job: "echo 'Hello World'"

Dans notre rôle, il y a une différenciation entre Debian et OS possible par le biais des Gather Facts représentées par les variables ansible_xxxx. Ces variables nous permettent dans ce cas présent définir une condition selon l'OS.

Si nous revenons quelques secondes sur l'arborescence du rôle common, nous verrons qu'il contient plusieurs sous répertoires qui sont liés au fonctionnement d'Ansible.

Arborescence du rôle common:

    ├── common
    │   ├── files
    │   ├── handlers
    │   │   └── main.yml
    │   ├── tasks
    │   │   ├── centosbase.yml
    │   │   ├── debianbase.yml
    │   │   └── main.yml
    │   └── templates
    │       └── motd.j2

Quelques explications sur ces dossiers:

  • files: peut contenir des fichiers à deployer sur le serveur cible (avec le module copy par exemple), contrairement au dossier templates les fichiers dans ce dossier ne peuvent pas contenir de code Jinja2 interprétable par Ansible.
  • handlers: ce dossier contient les tâches de déclenchement d'action, par exemple restart le service SSH selon une condition. Si on regarde la tâche "Déploiement du motd" on peut voir qu'elle contient la ligne: notify: restart ssh. Ainsi quand cette tâche est exécute et engendre des modifications, Ansible va notifier le handler restart ssh qui exécutera la tâche définie pour ce handler.
  • tasks: ce dossier contient toutes les tâches du rôle.
  • templates: contient les des fichiers qui peuvent être templatisés, nous allons aborder les fichiers templates dans la partie suivante.

Voici un exemple de tâche handler:

---
- name: restart ssh
  service: name=ssh state=restarted

Ici on utilise le module service de Ansible, qui va exécuter l'action restart sur le service ssh.

Les templates

Avec les rôles nous pouvons déployer des fichiers de configurations, et surtout des fichiers qui peuvent être générés avec les informations des variables. Ce sont les fichiers templates, au format Jinja2.

En regardant la tâche de déploiement du motd on observe que notre source est le fichier motd.j2 correspondant au fichier template/motd.j2 dans notre arborescence.

Notre fichier motd.j2 au format Jinja2:


Hostname: {{ ansible_fqdn }}
OS: {{ansible_distribution}} {{ansible_distribution_major_version}} ({{ansible_distribution_version}}) 
IP: {% for ip in ansible_all_ipv4_addresses %}{{ip}} {% endfor %}

CPU: {% for cpu in ansible_processor %} {{cpu}} {% endfor %}

Note: les variables sont au format {{ }} , ce qui est entre {% %} correspond à du Jinja2.

Notre fichier motd une fois généré sur le serveur via Ansible:

Hostname: hostdebian.in
OS: Debian 9 (9.2) 
IP: 192.168.2.109 
CPU:  0  GenuineIntel  Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz 

Nous avons nos premières tâches ainsi qu'un premier exemple de fichier de template. Il est temps de réunir tout ce beau monde et de balancer tout ça sur un ou deux serveurs !

Le déploiement

Pour le déploiement nous allons utiliser ansible-playbook pour ça il faut avoir Ansible d'installer, je n'expliquerai pas ici l'installation et la configuration d'Ansible, je vous invite à consulter la documentation d'installation: http://docs.ansible.com/ansible/latest/intro_installation.html

Pour déployer notre projet, rien de plus simple, il suffit de se placer à la racine du projet et d'exécuter la commande:

ansible-playbook -i mon_inventaire configuration.yml

On indique à ansible-playbook que nous allons utiliser l'inventaire mon_inventaire et le playbook configuration.yml

Voici l'output de l'exécution des tâches:

PLAY [Récupération des gather facts] *****************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *******************************************************************************************************************************************************************************************************************************************************************
ok: [hostdebian]
ok: [hostcentos]

PLAY [all] *******************************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *******************************************************************************************************************************************************************************************************************************************************************
ok: [hostdebian]
ok: [hostcentos]

TASK [common : Installation des paquets] *************************************************************************************************************************************************************************************************************************************************
skipping: [hostcentos] => (item=[]) 
changed: [hostdebian] => (item=[u'vim', u'rsync', u'unzip', u'mysql-server', u'python-mysqldb'])

TASK [common : Génération des locales] ***************************************************************************************************************************************************************************************************************************************************
skipping: [hostcentos] => (item=fr_FR.UTF-8) 
skipping: [hostcentos] => (item=en_US.UTF-8) 
ok: [hostdebian] => (item=fr_FR.UTF-8)
changed: [hostdebian] => (item=en_US.UTF-8)
changed: [hostdebian] => (item=en_GB.UTF-8)

TASK [common : Configuration de la locale par défaut] ************************************************************************************************************************************************************************************************************************************
skipping: [hostcentos]
changed: [hostdebian]

TASK [common : Installation des paquets] *************************************************************************************************************************************************************************************************************************************************
skipping: [hostdebian] => (item=[]) 
changed: [hostcentos] => (item=[u'vim', u'rsync', u'unzip', u'epel-release', u'MySQL-python'])

TASK [common : Configuration de la locale par défaut] ************************************************************************************************************************************************************************************************************************************
skipping: [hostdebian]
changed: [hostcentos]

TASK [common : Configuration de la timezone] *********************************************************************************************************************************************************************************************************************************************
ok: [hostdebian]
ok: [hostcentos]

TASK [common : Déploiement du motd] ******************************************************************************************************************************************************************************************************************************************************
changed: [hostdebian]
ok: [hostcentos]

TASK [common : Déploiement d'une tâche cron] *********************************************************************************************************************************************************************************************************************************************
changed: [hostdebian]
changed: [hostcentos]

RUNNING HANDLER [common : restart ssh] ***************************************************************************************************************************************************************************************************************************************************
changed: [hostdebian]

PLAY [mysql-server] **********************************************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *******************************************************************************************************************************************************************************************************************************************************************
ok: [hostdebian]

TASK [mysql : Création des utilisateur SQL] **********************************************************************************************************************************************************************************************************************************************
changed: [hostdebian] => (item={'key': u'noskillbrain1', 'value': {u'privs': [u'noskillbrain1_db_1.*:ALL'], u'password': u'i0eFuprAYZQQ', u'databases': [u'noskillbrain1_db_1']}})
changed: [hostdebian] => (item={'key': u'noskillbrain2', 'value': {u'privs': [u'noskillbrain2_db_1.*:ALL', u'noskillbrain2_db_2.*:SELECT'], u'password': u'V17pPyOsUO6Q', u'databases': [u'noskillbrain2_db_1', u'noskillbrain2_db_2']}})

TASK [mysql : Création des bases de données SQL] *****************************************************************************************************************************************************************************************************************************************
changed: [hostdebian] => (item=({u'password': u'i0eFuprAYZQQ', u'privs': [u'noskillbrain1_db_1.*:ALL']}, u'noskillbrain1_db_1'))
changed: [hostdebian] => (item=({u'password': u'V17pPyOsUO6Q', u'privs': [u'noskillbrain2_db_1.*:ALL', u'noskillbrain2_db_2.*:SELECT']}, u'noskillbrain2_db_1'))
changed: [hostdebian] => (item=({u'password': u'V17pPyOsUO6Q', u'privs': [u'noskillbrain2_db_1.*:ALL', u'noskillbrain2_db_2.*:SELECT']}, u'noskillbrain2_db_2'))

PLAY RECAP *******************************************************************************************************************************************************************************************************************************************************************************
hostcentos                 : ok=7    changed=3    unreachable=0    failed=0   
hostdebian                 : ok=12   changed=8    unreachable=0    failed=0   

On retrouve nos différentes tâches exécutées sur nos deux serveurs de tests. Il y a également des tâches mysql supplémentaires non détaillées ici mais qui étaient dans le scénario imaginé. Pour les découvrir je vous laisse récupérer les sources complètes du projet, de changer les deux adresses IP attribuées à la variable ansible_host= dans le fichier d'inventaire pour renseigner celles de vos deux serveurs de test et exécuter le playbook.


Photo by Emile Séguin / Unsplash

Les sources

Les sources de ce projet de présentation sont disponibles sur le Gogs de mon ami et collègue Victor Héry: https://git.lecygnenoir.info/Guillaume/noskillbrain_ansible_prez

Pour récupérer le projet:
git clone https://git.lecygnenoir.info/Guillaume/noskillbrain_ansible_prez.git

Il tient également un blog qui contient quelques articles techniques très intéressants: https://blog.héry.com , n'hésitez pas à y faire un tour ;).

Aller plus loin

Ansible contient des dizaines de modules cependant, l'utilisation d'un module spécifique pour chaque tâche n'est pas possible et peut-être pas la meilleure façon de faire. Faites toujours des actions simples et qui fonctionnent pour vous.

Faites toujours des tests de vos playbooks et validez les sur des machines qui ne sont pas en production, une erreur de module ou de manipulation peut arriver. L'option --dry-run peut également être une bonne pratique à avoir.

Le développement d'Ansible bouge beaucoup et rapidement, les versions changent régulièrement avec des changements qui peuvent rendre deprecated un module ou la façon dont une variable peut être utilisée. Utilisez toujours une version la plus récente possible, mais pour ça utilisez des virtualenv python pour avoir plusieurs versions d'Ansible utilisables pour vos projets.

Pour finir voici quelques commandes Ansible utiles.

Obtenir les gather facts d'un serveur:

ansible hostdebian -i mon_inventaire -m setup

Exécuter un module Ansible (lineinfile):

ansible hostdebian -i mon_inventaire -m lineinfile -a 'dest=/etc/motd isertafter=EOF line="Come Get Some!"

Exécuter une commande spécifique sur un groupe de serveur:

ansible hostdebian -i mon_inventaire -a "ls /var/log/"

Ah j'oubliais ! Notre script install.sh... non je plaisante :)