Ansible par la pratique - Première partie: les bases

deploiement serveur ansible

Ansible® est un outil Open Source d'automatisation informatique qui automatise le provisionnement, la gestion des configurations, le déploiement des applications, l'orchestration et bien d'autres processus informatiques manuels.

Ici, on a seulement notre hôte physique (localhost) faisant fonctionner une machine virtuelle (ipv4: 192.168.122.243). Bien entendu, notre hôte physique Fedora a le paquet RPM ansible installé.

dnf install ansible

Notre machine virtuelle est une CentOS fraichement installée (minimal), sans utilisateur (pour l'instant).

Lancer des commandes manuellement

On va, dans un premier temps essayé de contacter notre machine virtuelle avec un simple ping.

ansible 192.168.122.243 -m ping
[WARNING]: Could not match supplied host pattern, ignoring: all
[WARNING]: provided hosts list is empty, only localhost is available
[WARNING]: Could not match supplied host pattern, ignoring: 192.168.122.243
[WARNING]: No hosts matched, nothing to do

Cela ne fonctionne pas.

En effet, Ansible ne connait pas notre cible et refuse de communiquer avec elle. De plus notre utilisateur n'existe pas sur la cible

Il nous faudrait déclarer l'hôte distant dans le fichier /etc/ansible/hosts pour que ça fonctionne. Mais comme on n'est pas le super-utilisateur, on va faire autrement, et en suivant le guide ansible des bonnes pratiques petit à petit.

On va créer deux répertoires, le nom peut-être ce que vous voulez, il faudra juste adapter les commandes pour utiliser ces répertoires:

  • production
  • staging (recettage ou pré-production)

et dans chaque répertoire on va créer un fichier nommé hosts, où l'on définira des groupes dans lesquels on déclarera nos hôtes. Ce fichier est au format INI, mais il peut aussi être au format YAML.

Dans l'exemple suivant, on déclare notre machine virtuelle dans le groupe server et notre hôte local dans le groupe ansible.

Fichier staging/hosts

[server]
192.168.122.243

[ansible]
localhost

Et au lancement de la commande, on va spécifier le répertoire de notre environnement avec l'option -i, et pour utiliser le super-utilisateur sur notre cible, on le spéficie (avec demande de mot de passe)

ansible -i staging 192.168.122.243 --user root --ask-pass -m ping
192.168.122.243 | SUCCESS => {
"changed": false,
"failed": false,
"ping": "pong"
}

Cette fois ça fonctionne. Essayons maintenant une commande

ansible -i staging 192.168.122.243 --user root --ask-pass -a whoami
192.168.122.243 | SUCCESS | rc=0 >>
root

Par défaut Ansible utilise le même utilisateur sur l'hôte distant. On s'assure donc que celui existe sur notre machine virtuelle ou que l'on spécifie l'utilisateur à utiliser dans la ligne de commande.

Lancer une série de commandes

Le fichier permettant de lister les commande à lancer s'appelle un playbook. Il peut être monolithique ou divisé en plusieurs parties. Ansible recommande la séparation par rôle, ce qui permet de réutiliser les commandes d'un playbook et de les partager facilement avec la communauté.

Les rôles

Il est important de bien réfléchir à la séparation des différents rôles que devra comporter notre serveur, afin d'avoir la meilleur modularité possible.

On va prendre l'exemple d'un serveur personnel (vps), qui comporterait un blog (wordpress), un outil d'analyse du trafic web (piwik alias matomo) et tout le monitoring nécessaire à tous cela. On arrive à ce stade à 3 rôles. Mais sachant que le blog et piwik ont tous les deux besoin d'un serveur web et d'une base de données, que le système à besoin d'être mis à jour, que certaines actions seront nécessaires à plusieurs rôles (ex: redémarrer le service httpd après un changement), on arrive à

Rôles de notre serveur:

  • system: concernera la post-installation du système
    • mettre le système à jour
    • Définition des pools ntp (fr)
    • Définition du nom d'hôte
    • Définition basique du firewall: SSH (22)
    • Redirection des mails locaux vers une adresse externe
    • etc...
  • pki: Concernera l'obtention et la mise à jour des certificats SSL
    • Copie de nos clés privées
    • Copies de nos fichiers et scripts gérant la pki (systemd)
  • webserver: Concernera l'installation et la configuration d'apache
    • Installation et configuration d'apache
    • Mise à jour des extensions (ex modsecurity), hors RPM (les paquets RPMs seront mis à jour avec le rôle system)
    • Ajout des ports HTTP (80) et HTTPS (443) au firewall
  • database: Concernera l'installation et la configuration de mariadb
    • Installation et configuration de mariadb
  • blog: Concernera l'installation et la configuration de wordpress
    • Installation et configuration du blog
    • définition de l'alias ou du vhost pour le webserver
    • import de la sauvegarde si elle existe
    • Mise à jour des extensions, hors RPM (les paquets RPMs seront mis à jour avec le rôle system)
  • webstat: Concernera l'installation et la configuration de piwik
    • Installation et configuration de piwik
    • définition de l'alias ou du vhost pour le webserver
    • import de la sauvegarde si elle existe
  • monitoring: Concernera l'installation et la configuration du monitoring
    • Installation et configuration des outils
  • ids: Concernera l'installation et la configuration des outils de détection et de vérification d'intrusion
    • Installation et configuration des outils

On remarque que le changement d'un élément ne perturbe pas (ou peu) les autres rôles. Ainsi le changement de produit n'est pas une tâche trop difficile. Le changement de piwik par awstats sera transparent pour les autres rôles, mais le changement du serveur web ou de la base de données impactera les rôles du blog et de l'outil d'analyse web.

Les playbooks

Un playbook est un fichier texte au format YAML.

Il s'agit dune succession de tâches à accomplir. L'erreur sur l'une d'entre elle entraine l'arrêt du processus (par défaut). Il existe un très grand nombre de modules et la documentation officielle des modules Ansible les explique tous.

L'exemple suivant se sert de deux modules:

  • yum: pour manager les paquets RPMs de la distribution Linux
  • shell: pour lancer des commandes

et effectue les tâches suivantes:

  • Installation du dépôt EPEL
  • Mise à jour de la distribution
  • Installation d'une liste de paquets RPMs
  • Initialisation de etckeeper
  • Premier commit de etckeeper

pour chaque hôte membre du groupe server (fichier hosts du répertoire production ou staging).

Fichier site.yml

- hosts: server
  tasks:
    - name: Ensure epel repository is set
      yum:
        name: epel-release
        state: latest
      become: true

    # System update
    - name: Ensure all pkgs are up-to-date
      yum:
        name: '*'
        state: latest
      become: true
      tags: update

    # Install packages
    - name: Ensure system RPMs are installed and up-to-date
      yum:
        pkg: "{{ item }}"
        state: latest
      become: true
      with_items:
        - git
        - postfix
        - chrony
        - mlocate
        - screen
        - vim-enhanced
        - yum-utils
        - bzip2
        - unzip
        - bind-utils
        - man-pages
        - net-tools
        - etckeeper

    # Manage etckeeper
    - name: Ensure etc is versionned
      shell: "etckeeper init"
      args:
        executable: /bin/bash
        creates: /etc/.git
        chdir: /etc
      become: true

    - name: Ensure first commit is done for etc
      shell: "etckeeper commit 'First commit'"
      args:
        executable: /bin/bash
        creates: /etc/.git/refs/heads/master
        chdir: /etc
      become: true

Quelques remarques sur les tâches:

  • Toutes les tâches doivent être exécutées par le super-utilisateur, c'est le rôle de l'instruction become: true (root par défaut, mais on peut le spécifier avec _becomeuser)
  • Les tâches d'initialisation d'etckeeper ne doivent pas être exécuter plusieurs fois. On spécifie donc un nom de fichier avec l'instruction creates, qui, s'il existe, ne lancera pas la commande. On se place également dans un répertoire précis avant de lancer la commande avec l'instruction chdir

Le playbook se lance ainsi:

ansible-playbook -i staging --ask-become-pass site.yml
SUDO password:

PLAY [server] ******************************************************************

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

TASK [Ensure epel repository is set] *******************************************
changed: [192.168.122.243]

TASK [Ensure all pkgs are up-to-date] ******************************************
changed: [192.168.122.243]

TASK [Ensure system RPMs are installed and up-to-date] *************************
changed: [192.168.122.243] => (item=[u'git', u'postfix', u'chrony', u'mlocate', u'screen', u'vim-enhanced', u'yum-utils', u'bzip2', u'unzip', u'bind-utils', u'man-pages', u'net-tools', u'etckeeper'])

TASK [Ensure etc is versionned] ************************************************
changed: [192.168.122.243]

TASK [Ensure first commit is done for etc] *************************************
changed: [192.168.122.243]

PLAY RECAP *********************************************************************
192.168.122.243            : ok=6    changed=5    unreachable=0    failed=0

Si on relance le même playbook, les tâches déjà effectuées ne seront pas relancées.

Et c'est une règle d'or des playbooks, ils peuvent être relancé n'importe quand et le résultat sera toujours prévisible (idempotent), c'est à dire qu'il n'y aura pas d'effet de bord, en cas de doublon par exemple.

En effet, dans notre exemple, on ne peut pas initialiser le dépôt git d'etckeeper plusieurs fois et c'est au playbook de gérer cette situation (en posant un fichier et en testant sa présence).

ansible-playbook -i staging --ask-become-pass site.yml
SUDO password:

PLAY [server] ******************************************************************

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

TASK [Ensure epel repository is set] *******************************************
ok: [192.168.122.243]

TASK [Ensure all pkgs are up-to-date] ******************************************
ok: [192.168.122.243]

TASK [Ensure system RPMs are installed and up-to-date] *************************
ok: [192.168.122.243] => (item=[u'git', u'postfix', u'chrony', u'mlocate', u'screen', u'vim-enhanced', u'yum-utils', u'bzip2', u'unzip', u'bind-utils', u'man-pages', u'net-tools', u'etckeeper'])

TASK [Ensure etc is versionned] ************************************************
ok: [192.168.122.243]

TASK [Ensure first commit is done for etc] *************************************
ok: [192.168.122.243]

PLAY RECAP *********************************************************************
192.168.122.243            : ok=6    changed=0    unreachable=0    failed=0

Si l'utilisateur courant n'a pas besoin de mot de passe pour les commandes sudo, on ne sera pas obligé de spécifier l'option --ask-become-pass.

Protéger ses variables en les mettant dans un coffre

Si Ansible s'avère bien pratique pour gérer les hôtes distants, il apparait que certaines pratiques sont dangereuses au niveau de la sécurité. En effet comment affecter un mot de passe à l'utilisateur MySQL root et en même temps commiter les fichiers Ansibles sur un dépôt public distant, sans compromettre la sécurité. Ansible a résolu le problème avec vault (coffre).

Ansible-vault encode les données avec l’algorithme AES256

Il y a deux manière de l'utiliser:

  • Chiffrer la totalité du fichier de variables et utiliser les commandes ad-hoc pour éditer le fichier
  • Chiffrer seulement la valeur de la variable dans un fichier texte en clair

Prenons comme exemple un fichier de variable host_vars/server qui contiendrait l'entrée suivante:

mysql_root_password: "monsupermotdepasse"

Méthode 1: Encrypter le fichier

On créé notre fichier chiffré et on inscrit notre variable une fois le fichier ouvert par l'éditeur par défaut (variable EDITOR) avec la commande

ansible-vault create host_vars/server
New Vault password:
Confirm New Vault password:

On pourra éditer notre fichier chiffré, avec l'éditeur par défaut

ansible-vault edit host_vars/server

Alternativement, on peut créer un fichier en clair et le chiffrer par la suite

echo 'mysql_root_password: "monsupermotdepasse"' > host_vars/server
ansible-vault encrypt host_vars/server

Ce qui donnera le fichier suivant

Fichier host_vars/server

$ANSIBLE_VAULT;1.1;AES256
61346330393736306566356131633264323234353664653034646239326439633261643630393162
6633646264326134373230343935636361353033313262630a356566336261623261343233666135
33333635353331663434366137653934633238633561346463626130633063663636373330663031
3738363239663635380a326532386133343732306535393466393433306434323265366336323037
39356531316631366161376161633166336562366330313038343961626261333237356237333062
3830373337656561653364313064353866306132653864623834

Méthode 2: Chiffrer la valeur de la variable

On chiffre la valeur avec la commande idoine

ansible-vault encrypt_string --vault-id @prompt 'monsupermotdepasse' --name 'mysql_root_password' >> host_vars/server
New vault password (default):
Confirm vew vault password (default):
Encryption successful

Ce qui donnera le fichier suivant

Fichier host_vars/server

mysql_root_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          65343663373236316433393833616164353236303666663437613438306630336135353238326137
          3566333939353435613539613066373463323231656635320a666633366637343738653634396137
          33316530396338316638303765636165363132363934376234316430633432613632663439326661
          3530613339653038640a396436373763363332313336623061313834353238613766393662396533
          30653531653265623165333037396539396632393535636166646538646638373261

Et maintenant ?

Les bases sont posées et un exemple concret sera bientôt disponible dans la deuxième partie: Premier playbook avec les rôles.

Article précédent Article suivant