Tag Archives: ansible

Ansible par la pratique – Troisième partie: Utilisation avancée de nos playbooks

Utilisation avancée de nos playbooks

Ce tutoriel est la suite de la première partie et de la deuxième partie consacrée à ansible.

L’intégralité des fichiers est versionné sur gitlab, en prenant le tag tuto-ansible-3. En effet la branche master va évoluer avec l’avancement de ce tutoriel.

Simple mise à jour du système

Il est possible de ne lancer qu’une partie de nos playbooks, en lui définissant des étiquettes et en les précisant à l’exécution.

En ajoutant juste la ligne “tags: update“, à la tâche “Ensure all pkgs are up-to-date“, on peut maintenant lancer un de nos playbooks pour faire les mises à jours (site ou system).

Alors oui, avant aussi on pouvait mettre notre VPS à jour, mais tout le playbook était exécuté, et le temps perdu était considérable.

Fichier roles/system/tasks/packages.yml
# System update
- name: Ensure all pkgs are up-to-date
  yum:
    name: '*'
    state: latest
  tags: update

...

On lance notre playbook en précisant l’étiquette à utiliser. Seules les tâches portant l’étiquette demandée (update) seront exécutées.

ansible-playbook -i staging --user root --tags update site.yml

A contrario, on peut aussi lancer l’intégralité de notre playbook, à l’exception des tâches portant l’étiquette précisée.

ansible-playbook -i staging --user root --skip-tags update site.yml

On peut, dans les deux cas précédant, préciser plusieurs étiquettes, en les séparant avec une virgule. Les guillemets sont là par sécurité.

ansible-playbook -i staging --user root --tags "update,vhosts" site.yml

Sauvegarde et restauration

En jouant habilement avec les étiquettes, on pourrait faire en sorte que notre playbook sauvegarde nos données et les restaure si besoin est.

Commençons par effectuer la sauvegarde. Concrètement, il n’y a pas grand chose à sauvegarder:

  • La base de données mariadb de wordpress.
  • Le répertoire wp-content/uploads qui contient les fichiers téléversés.

Comme le module ansible mysql_db permet de restaurer un fichier d’export compressé, on va économiser un peu notre bande passante en le compressant avant transfert sur notre poste.

On créé un nouveau fichier de tâche pour la sauvegarde: roles/blog/tasks/backup.yml.

  1. On exporte la base de données dans un fichier d’export à plat sur le VPS.
  2. On compresse le fichier d’export à plat sur le VPS.
  3. On créé des archives à partir des répertoires donnés en paramètres sur le VPS.
  4. On télécharge le fichier d’export compressé et les archives sur notre poste de travail, directement dans le répertoire files du rôle correspondant. Le chemin est relatif à notre fichier playbook, c’est à dire la racine de notre projet.
  5. On supprime le fichier d’export compressé et les archives sur le VPS.
Fichier roles/blog/tasks/backup.yml
- name: Dump database
  mysql_db:
    name: "{{ databases.wordpress.base }}"
    state: dump
    encoding: utf8
    target: "/tmp/{{ databases.wordpress.base }}.sql"
  tags: backup
  
- name: Compress dump database
  archive:
    path: "/tmp/{{ databases.wordpress.base }}.sql"
    dest: "/tmp/{{ databases.wordpress.base }}.sql.gz"
    remove: yes
  tags: backup
   
- name: Make tarballs from selected directories
  archive:
    path: "/usr/share/wordpress/wp-content/{{ item }}"
    dest: "/tmp/wp-{{ item }}.tar.gz"
  with_items:
    - "uploads"
  tags: backup
    
- name: Fetch database dump and tarballs
  fetch:
    src: "/tmp/{{ item }}"
    dest: "roles/blog/files/{{ item }}"
    flat: yes
  with_items:
    - "{{ databases.wordpress.base }}.sql.gz"
    - "wp-uploads.tar.gz"
  tags: backup

- name: Delete db dump and tarballs
  file:
    dest: "/tmp/{{ item }}"
    state: absent
  with_items:
    - "{{ databases.wordpress.base }}.sql.gz"
    - "wp-uploads.tar.gz"
  tags: backup

On modifie le fichier de tâche principal: roles/blog/tasks/main.yml.

On modifie la tâche nommée “Ensure database exists“, en lui ajoutant deux fonctions à exécuter (handlers) une fois la tâche exécutée (si la base existe, le statut est ok mais la tâche n’est pas effectuée). C’est à dire que si le playbook créé la base de données, il vérifiera la présence d’un fichier d’export et s’il est trouvé, il importera la base de données depuis ce fichier. Si la base existe déjà, la tâche ne fera absolument rien. De même si la base de données n’existe pas et que le fichier d’export n’existe pas, la base sera créée mais elle sera vide.

On ajoute une tâche de restauration de nos archives qui n’échoue jamais (failed_when: false), c’est à dire que l’erreur est ignorée si une des archive n’est pas trouvée, et le playbook continue. Sans cela l’exécution du playbook se serait arrêté net.

En ajoutant les étiquettes backup et never à notre tâche d’inclusion dans notre fichier de tâches principal, on pourra effectuer nos sauvegardes, seulement si l’étiquette backup est précisée à l’exécution du playbook. Alors oui, on peut aussi l’exécuter si on précise l’étiquette never, Mais c’est vraiment pas le but de cette étiquette spéciale définie par ansible.

Fichier roles/blog/tasks/main.yml
...
# Replace task "Ensure database exists"
- name: Ensure database exists and import dump
  mysql_db:
    name: "{{ databases.wordpress.base }}"
    state: present
    encoding: utf8
  notify:
    - Check if db dump exists
    - Import SQL databases
  tags:
    - restore
...
- name: Ensure wordpress is restored from backup
  unarchive:
    src: "{{ item }}.tar.gz"
    dest: "/usr/share/wordpress/wp-content"
    creates: "/usr/share/wordpress/wp-content/{{ item }}"
  with_items:
    - uploads
  tags:
    - restore
  failed_when: false
    
- name: Ensure some WordPress directories are owned by apache
...
# Just before EOF
- include_tasks: backup.yml
  tags:
    - never
    - backup

Et enfin notre fichier de fonctions déclenchées: roles/blog/handlers/main.yml

  • Vérifier que le fichier d’export de la base de données est présent sur le VPS
  • Importer la base de données que si et seulement si la vérification de la présence du fichier d’export a été exécutée et sa présence confirmée:
    1. la première fonction renseigne la variable dump_exists, parmi laquelle figure le résultat de l’existence du fichier : dump_exists.stat.exists. Autrement dit la variable dump_exists est définie, ainsi que la variable binaire dump_exists.stat.exists.
    2. le fichier d’export compressé de la base de données existe: c.a.d. la variable dump_exists.stat.exists est positive.
Fichier roles/blog/handlers/main.yml
- name: Check if db dump exists
  stat:
    path: "/tmp/{{ databases.wordpress.base }}.sql.gz"
  register: dump_exists

- name: Import SQL databases
  mysql_db:
    name: "{{ databases.wordpress.base }}"
    state: import
    encoding: utf8
    target: "/tmp/{{ databases.wordpress.base }}.sql.gz"
  when: dump_exists and dump_exists.stat.exists

Voilà, si le playbook doit créer la base de données, il importera le fichier d’export compressé, s’il existe.

Lancement du playboook

On lance maintenant un playbook qui supporte des options étiquettes.

Installation complète:

ansible-playbook -i staging --user root site.yml

Effectuer selulement la mise à jour du système:

ansible-playbook -i staging --user root --tags update site.yml

Effectuer la sauvegarde:

ansible-playbook -i staging --user root --tags backup site.yml

Effectuer juste la restauration, car on a mis des étiquettes là aussi:

ansible-playbook -i staging --user root --tags restore site.yml

Conclusion

Cette facilité à mettre le système à jour ou à faire et restaurer des sauvegardes permet de rester serein, même si à trois jours de vos vacances, votre hébergeur vous annonce qu’il arrête son service. Il suffit de faire la sauvegarde, d’en trouver un autre et de lancer le déploiement du nouveau système et retournez faire vos bagages, ansible s’occupe du reste.

Ansible par la pratique – Deuxième partie: Premiers playbooks avec les rôles

Premiers playbooks utilisant les rôles

Ce tutoriel est la suite de la première partie consacrée à ansible.

On va partir de l’hypothèse que l’on veut un blog sur notre VPS CentOS 7, en l’occurrence wordpress. Le nom d’hôte pleinement qualifié sera blog.example.com.

Pour ce faire il nous faudra donc un LAMP. Nous n’allons pas parler sécurité, tout du moins pas encore, c’est notre premier jet. On n’utilisera pas le coffre comme vu précédemment et ce sera l’utilisateur root qui fera l’installation. C’est vraiment pas fait pour de la production en l’état. Dans la partie production, il faudra ajouter un utilisateur pouvant utiliser sudo, configurer finement SSH pour ne pas accepter les connexions avec le super-utilisateur root, etc… Ce sera pour plus tard.

Afin de ne pas s’exposer inutilement sur internet, on va faire l’installation d’une machine virtuelle sur notre poste de travail, qui lui a Fedora d’installé (mais ce n’est pas obligatoire). Je ne vais pas détailler l’installation de la machine,  ce serai hors sujet et alourdirai encore ce tutoriel, mais on va partir d’une installation toute neuve:

  • L’adresse de cette machine est 192.168.122.10.
  • Le partitionnement est déjà effectué à l’installation, on n’y touchera pas.
  • Le nom d’hôte a été renseigné ou pas, on le remettra de toute façon en jouant notre playbook.
  • Le mot de passe root est déjà mis à l’installation.
  • Le réseau a été activé et la machine est joignable en SSH par l’utilisateur root.

L’intégralité des fichiers est versionné sur gitlab, en prenant le tag tuto-ansible-2. En effet la branche master va évoluer avec l’avancement de ce tutoriel en plusieurs parties.

git clone https://gitlab.com/tartare-tutorial/ansible.git
cd ansible
git checkout tags/tuto-ansible-2

Mise en place de l’arborescence

Les environnements

On va créer nos deux répertoires d’environnement :

  • production
  • staging

On ne va pas s’occuper de l’environnement de production pour l’instant, ce répertoire restera vide, pour l’instant. Dans celui destiné aux tests, on placera un fichier hosts (fichier au format INI), et deux répertoires pouvant accueillir chacun un fichier de définition de variables

  • pour les hôtes: host_vars
  • pour les groupes d’hôtes: group_vars

En résumé, sur notre station de travail, dans un répertoire dédié, ça se résume à ça:

mkdir -p production staging/{group,host}_vars
touch staging/hosts staging/group_vars/vps.yml staging/host_vars/192.168.122.10.yml

On édite nos fichiers:

Fichier staging/hosts
[vps]
192.168.122.10

Les variables définies dans ce fichier seront prioritaires à celle définies dans le rôle, et même à celle définies dans le fichier de groupe (
staging/group_vars/vps.yml). La doc officielle en parle mieux que moi.

Fichier staging/host_vars/192.168.122.10.yml
hostname:
  short: blog
  domain: example.com

Le fichier staging/group_vars/vps.yml est juste un fichier vide il ne nous sert pas pour l’instant.

Les Roles

On va créer nos trois rôles, c’est à dire un fichier YAML à la racine du projet, plus un autre, site.yml, qui inclura tous nos rôles et quatre répertoires d’accueil pour nos rôles :

  • common: un pseudo rôle qui sera inclus dans tous les autres rôles. C’est ici que l’on va mettre tous ce qui peut servir dans plusieurs rôle. Par exemple une fonction déclenchée pour redémarrer le service apache
  • system: un rôle qui sera chargé de la post-installation de notre serveur:
    • Installation du dépôt additionnel EPEL.
    • Gérer les paquets: Installation, suppression ou remplacement de paquets RPM.
    • Activer SELinux s’il n’est pas encore activé.
    • Installer et configurer etckeeper pour versionner le répertoire /etc.
    • Définir le nom d’hôte.
    • Activer le pare-feu.
    • Configurer la mise à l’heure (avec des serveurs géographiquement proches).
    • Rendre la journalisation système permanente.
    • Gérer l’activation/désactivation, le démarrage ou l’arrêt de services.
  • lamp: un rôle qui sera chargé de l’installation de notre LAMP (Linux-Apache-Mariadb-PHP):
    • Installation du serveur web.
    • Installation du gestionnaire de base de données et de son initialisation.
    • Installation du module apache PHP et modification du fichier /etc/php.ini.
    • Ouverture du port HTTP sur le pare-feu.
  • blog: un rôle qui sera chargé de l’installation de wordpress:
    • Mise en place de la base de données pour wordpress.
    • Initialisation de wordpress.

Ce qui revient à faire

touch {system,lamp,blog,site}.yml
mkdir -p roles/{common,system,lamp,blog}

Jusqu’ici pas de mystère. Pour chaque rôle, on va créer l’arborescence préconisée par Ansible. Les répertoires vides peuvent tout à fait être omis. Seul celui commun à tous (common) aura une arborescence simplifiées dès sa création.

Je ne vous l’avais pas encore dit, mais ansible s’attend à trouver un fichier main.yml à l’intérieur des sous-répertoires du rôle (sauf pour les répertoires files et templates, qui sont respectivement dédiés aux fichiers à copier et aux fichiers modèles à interpréter par Jinja2).

Un rôle doit contenir au moins un des répertoires suivant:

  • tasks: Contient la liste des tâches devant être effectuées pour ce rôle
  • handlers: Contient les fonctions déclenchées par les tâches du rôle ou comme ici pour le rôle commun, dans n’importe quel rôle incluant ce pseudo rôle.
  • defaults: Contient  les variables par défaut pour ce rôle.
  • vars: Contient  les variables pour ce rôle. Celles-ci sont prioritaire sur celles du répertoire default.
  • files: Contient les fichiers devant être déployés par ce rôle.
  • templates: Contient les fichiers modèles devant être déployés par ce rôle (interprétés par jinja2)
  • meta: Contient les définitions des méta-données pour ce rôle, c’est à dire définir les dépendances du rôle.

Ce qui revient à faire

mkdir roles/common/{handlers,defaults} roles/{system,lamp,blog}/{tasks,handlers,defaults,vars,files,templates,meta}
touch roles/common/{handlers,defaults}/main.yml roles/{system,lamp,blog}/{tasks,handlers,defaults,vars,meta}/main.yml

Voici à quoi ressemble désormais notre arborescence: les répertoires en bleu et les fichiers en noir.

├── production
├── roles
│   |── common
│   |   └── handlers
|   |       └── main.yml
│   |── system
│   |   ├── defaults
|   |   |   └── main.yml
│   |   ├── files
│   |   ├── handlers
|   |   |   └── main.yml
│   |   ├── tasks
|   |   |   └── main.yml
│   |   ├── templates
│   |   └── vars
|   |   |   └── main.yml
│   |── lamp
│   |   ├── defaults
|   |   |   └── main.yml
│   |   ├── files
│   |   ├── handlers
|   |   |   └── main.yml
│   |   ├── tasks
|   |   |   └── main.yml
│   |   ├── templates
│   |   └── vars
|   |   |   └── main.yml
│   └── blog
│       ├── defaults
|       |   └── main.yml
│       ├── files
│       ├── handlers
|       |   └── main.yml
│       ├── tasks
|       |   └── main.yml
│       ├── templates
│       └── vars
|           └── main.yml
|── staging
|   ├── group_vars
|   ├── host_vars
|   |   └── 192.168.122.10.yml
|   └── hosts
├── blog.yml
├── lamp.yml
├── site.yml
└── system.yml

Définition des playbooks et des roles

Certes, les fichiers sont vides, mais on va maintenant les remplir.

Les playbooks

Commençons par le plus facile, celui qui sera joué pour l’installation de notre VPS. Ce n’est qu’une inclusion des autres playbooks, ceux définissant nos rôles.

Fichier site.yml
- import_playbook: system.yml
- import_playbook: lamp.yml
- import_playbook: blog.yml
Maintenant, pas plus compliqué, on définit nos rôles dans des playbooks:
  • Définition des hôtes cibles, ici on met soit un liste d’hôte, soit un groupe. Notre groupe s’appelle vps, on va l’utiliser.
  • Définition de la liste de rôles, ici notre pseudo rôle pour les fonctions déclenchées et notre rôle à proprement parler, c’est à dire le nom du sous-répertoire à utiliser dans le répertoire roles.
Fichier system.yml
- hosts: vps
  roles:
    - common
    - system
Fichier lamp.yml
- hosts: vps
  roles:
    - common
    - lamp
Fichier blog.yml
- hosts: vps
  roles:
    - common
    - blog

Les rôles

Le role commun

Le handler commun définit le redémarrage d’un service: le serveur web. En effet celui-ci sera utilisé par le rôle lamp et par le rôle blog.

Fichier roles/common/handlers/main.yml
- name: restart httpd
  service: name=httpd state=restarted

Le fichier de variable par défaut commun contient le fuseau horaire, qui sera utilisé par les rôles system et lamp.

Fichier roles/common/defaults/main.yml
localtime: "Europe/Paris"

Le rôle système

Pour le rôle system, on va se faire plaisir, on va le découper en petits morceaux, c’est plus digeste. C’est pourquoi le fichier roles/common/handlers/main.yml ne contiendra que des inclusions.

Fichier roles/system/tasks/main.yml
- include_tasks: repos.yml
- include_tasks: packages.yml
- include_tasks: selinux.yml
- include_tasks: etckeeper.yml
- include_tasks: hostname.yml
- include_tasks: firewall.yml
- include_tasks: chrony.yml
- include_tasks: journald.yml
- include_tasks: services.yml

Ce fichier de tâche va installer le dépôt additionnel EPEL.

Fichier roles/system/tasks/repos.yml
# Repositories
- name: Ensure epel repository is set
  yum:
    name: epel-release
    state: latest

Ce fichier de tâche va s’assurer que tous les paquets sont à jour, installer les paquets que l’on souhaite ajouter à notre VPS et désinstaller les paquets que l’on ne veut pas.

Concrètement, on veut que le serveur de courrier soit postfix et non sendmail ou ssmtp, comme dans le VPS livré par fistheberg.

La liste des paquets que l’on souhaite installer sera défini plus loin, dans le fichier roles/system/defaults/main.yml.

Comme vu précédemment, il pourra être surchargé dans le fichier roles/system/vars/main.yml (de manière globale, quelque soit l’environnement: staging ou production) ou même dans le fichier staging/host_vars/192.168.122.10.yml si c’est que pour l’environnement staging.

Il est à noter que pour rendre notre surcharge indépendante de l’environnement, on peut aussi définir la liste dans le fichier variable de groupe staging/group_vars/vps.yml

Fichier roles/system/tasks/packages.yml
# System update
- name: Ensure all pkgs are up-to-date
  yum:
    name: '*'
    state: latest

# Uninstall unwanted packages
- name: Ensure unwanted packages of services are absent
  yum: 
    list: "[u'ssmtp', u'sendmail']"
    state: absent

# Install packages
- name: Ensure packages are installed and up-to-date
  yum:  
    name: "{{ packages }}"
    state: latest
  vars:
    packages: "{{ rpms }}"

Ce fichier de tâche va s’assurer que les paquets nécessaires à SELinux soit présents.

Si ce n’est pas le cas, il les installe et créé le fichier /.autorelabel pour déclencher le ré-étiquetage de l’intégralité de notre système au prochain démarrage. Par contre le fichier n’est pas créer si SELinux est déjà installé.

Fichier roles/system/tasks/selinux.yml
# Install packages
- name: Ensure selinux packages are installed and up-to-date
  yum: 
    list: "[u'selinux-polic', u'selinux-policy-targeted']"
    state: latest
  register: selinux_install
  
- name: Ensure autorelabel is set if selinux was disabled
  file:
    path: "/.autorelabel"
    owner: root
    group: root
    mode: 0600
    state: touch  
  when: selinux_install is changed

Ce fichier de tâche va initialiser etckeeper.

Il a été installé par le fichier de tâches roles/system/tasks/packages.yml, donc pas la peine de faire des doublons.

Fichier roles/system/tasks/etckeeper.yml
# Manage etckeeper
- name: Ensure etc is versionned
  shell: "etckeeper init"
  args:
    executable: /bin/bash
    creates: /etc/.git
    chdir: /etc
  
- 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 

Ce fichier de tâche va fixer le nom d’hôte.

Il va en plus modifier le motd pour afficher le nom d’hôte et l’adresse IP de notre système à la connexion, que se soit en direct sur une console virtuelle, ou à distance par une connexion SSH.

On est d’accord, ça sert pas à grand chose mais ça permet de montrer comment interpréter un fichier modèle avec une variable définie par nos soins dans le rôle et une autre qui est prise automatiquement par ansible dans la phase Gathering Facts.

Fichier roles/system/tasks/hostname.yml
# Set Hostname
- name: Ensure hostname is set
  hostname:
    name: "{{ hostname.short }}.{{ hostname.domain }}"

# Define banners
- name: Define banners
  template:
    src: "motd"
    dest: "/etc/motd"
    owner: root
    group: root
    mode: 0644

Ce fichier de tâche va s’assurer que le pare-feu est démarré et bien activé au démarrage.

Il va aussi ouvrir, si besoin, le pare-feu pour les connexions distantes par SSH.

Fichier roles/system/tasks/firewall.yml
# Manage firewall service
- name: Ensure firewalls is started and enabled
  systemd:
    name: firewalld
    enabled: yes
    masked: no
    state: started
    daemon_reload: yes

# Manage firewall: open ssh
- name: Ensure firewall rules are set
  firewalld: 
    service: ssh
    permanent: true
    immediate: true
    state: enabled

Ce fichier de tâche va définir notre fuseau horaire avec une variable défini dans le fichier roles/system/defaults/main.yml.

Il va en plus faire utiliser prioritairement à chrony, non pas le groupe de centos, mais un groupe encore une fois défini par nos soins dans le fichier de variables.

Fichier roles/system/tasks/chrony.yml
# Set timezone
- name: Ensure localtime is CEST
  file:
    src: "/usr/share/zoneinfo/{{ localtime }}"
    dest: "/etc/localtime"
    owner: root
    group: root
    state: link

# Configure chrony
- name: Ensure chrony use our ntp server pools before centos ones
  lineinfile: 
    path: /etc/chrony.conf
    line: 'server {{ item }}.{{ ntp_pool_domain }} iburst'
    regexp: '^server {{ item }}.{{ ntp_pool_domain }} iburst'
    insertbefore: 'server 0.centos.pool.ntp.org iburst'
  with_items: [ 0, 1, 2, 3 ]
  notify:
  - restart chronyd

Ce fichier de tâche va rendre persistant les fichiers journaux de journalctl.

En effet, par défaut, ces journaux ne survivent pas à un redémarrage. Ça consiste juste à créer le répertoire, journalctl sait quoi faire si il trouve ce répertoire.

Cette action actionne deux fonctions déclenchées qui seront renseignées et expliquées plus loin.

Fichier roles/system/tasks/journald.yml
# systemd-journald
- name: Ensure persistent storage of log messages is enabled
  file:
    path: "/var/log/journal"
    state: directory
    owner: root
    group: systemd-journal
    mode: 02755
  notify:
  - create tmpfiles journald
  - restart journald

Ce fichier de tâche va s’assurer que les services de courrier, de mise à l’heure automatique et de journalisation soient démarrés et activés.

Fichier roles/system/tasks/services.yml
# Manage all services
- name: Ensure services are started and enabled
  systemd:
    name: "{{ item }}"
    enabled: yes
    masked: no
    state: started
    daemon_reload: yes
  with_items:
  - postfix
  - chronyd
  - rsyslog

Ce fichier de variable va définir notre liste de paquets RPM que l’on souhaite installer et nos groupes de serveur de temps.

Fichier roles/system/defaults/main.yml
rpms:
  - postfix
  - chrony
  - multitail
  - mlocate
  - screen
  - vim-enhanced
  - yum-utils
  - bzip2
  - unzip
  - bind-utils
  - man-pages
  - net-tools
  - tree
  - uuid
  - etckeeper
  - wget
  - yum-utils
  - NetworkManager
  - rsyslog

ntp_pool_domain: fr.pool.ntp.org

Ce fichier décrit les fonctions déclenchées:

  • S’assurer de l’existance du répertoire volatile nécesaire au redémarrage du service de journalisation
  • Redémarrer le service systemd de journalisation
  • Redémarrer le service de mise à l’heure automatique
Fichier roles/system/handlers/main.yml
- name: create tmpfiles journald
  shell: "systemd-tmpfiles --create --prefix /var/log/journal"
  args:
    executable: /bin/bash

- name: restart journald
  service: name=systemd-journald state=restarted

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

Ce fichier va remplacer le fichier /etc/motd du système après avoir été interpréter par le moteur de modèle jinja2.

Les variables hostname.short et hostname.domain sont définies par nos soins.

La variable ansible_default_ipv4.address est acquise par ansible au début de l’exécution de notre playbook, dans la partie Gathering Facts.

Fichier roles/system/templates/motd
##############################################################################

                     Hostname : {{ hostname.short }}.{{ hostname.domain }}  
                     IPv4 :     {{ ansible_default_ipv4.address }}
              
##############################################################################

Le rôle lamp

Ce fichier de définition de variables par défaut va renseigner la liste des paquets RPM à installer, définir la liste des booleans SELinux à activer et le mot de passe root pour mariadb.

Les paquets libsemanage-python et MySQL-pythonsont indispensables pour gérer les booleans SELinux et la configuration de la base de données via ansible.

Fichier roles/lamp/defaults/main.yml
rpms:
  - mariadb-server
  - httpd
  - php
  - php-mysqlnd
  - libsemanage-python
  - MySQL-python

selinux_booleans:
  - httpd_can_network_connect
  - httpd_can_sendmail
  - httpd_unified
  - httpd_enable_homedirs
  
dbrootpasswd: "monsupermotdepasse"

Ce fichier de fonctions déclenchées va définir le redémarrage du service mariadb.

Fichier roles/lamp/handlers/main.yml
- name: restart mariadb
  service: name=mariadb state=restarted

Ce fichier de tâche va:

    • Installer les paquets RPM spécifiés dans le fichier roles/lamp/defaults/main.yml.
    • Ouvrir le pare-feu pour le service HTTP (port 80).
    • Configurer un minimum PHP en mettant le fuseau horaire, ainsi que le jeu de caractères en UTF-8.
    • S’assurer que le serveur web et le SGBD sont démarrés et activés au démarrage.
    • Que le service de base de données ne soit accessible que localement.
    • Sécurise un minimum le service de base de données, aussi bien qu’avec l’exécutable mysql_secure_installation:
      • Mettre en service le mot de passe root, pour l’utilisateur root local.
      • S’assurer que l’on peut se connecter à mariadb sans mot de passe avec l’utilisateur root, en créant un fichier .my.cnf dans le HOME de l’utilisateur (ici c’est root).
      • Mettre en service le mot de passe root, pour l’utilisateur root distant.
      • Supprime les utilisateurs mariadb inutiles.
      • Supprime les bases de données inutiles.
Fichier roles/lamp/tasks/main.yml
# Install packages
- name: Ensure packages of services are installed and up-to-date
  yum: 
    name: "{{ packages }}"
    state: latest
  vars:
    packages: "{{ rpms }}"

# Manage firewall
- name: Ensure firewall rules are set
  firewalld: 
    service: http
    permanent: true
    immediate: true
    state: enabled

# Configure php
- name: Ensure php is configured
  lineinfile: 
    path: /etc/php.ini
    regexp: '^{{ item.key }}'
    line: '{{ item.key }} = {{ item.value }}'
    insertafter: '{{ item.after }}'
  with_items: 
  - key: default_charset
    value: "\"UTF-8\""
    after: ";default_charset = \"UTF-8\""
  - key: date.timezone
    value: "\"{{ localtime }}\""
    after: ";date.timezone.*"
  
- name: Ensure SELinux allow httpd to do a lot of things
  seboolean:
    name: "{{ item }}"
    state: yes
    persistent: yes
  with_items: "{{ selinux_booleans }}"

# Manage all services
- name: Ensure services are started and enabled
  systemd:
    name: "{{ item }}"
    enabled: yes
    masked: no
    state: started
    daemon_reload: yes
  with_items:
  - mariadb
  - httpd
  
# Manage mariadb service
- name: Ensure mariadb binds localhost only
  lineinfile: 
    path: /etc/my.cnf.d/server.cnf
    regexp: 'bind-address = localhost'
    line: 'bind-address = localhost'
    insertafter: '\[mysqld\]'
  notify:
  - restart mariadb
  
# Add mysql users and databases;
# Need to do this for idempotency, see
# http://ansible.cc/docs/modules.html#mysql-user
- name: Ensure mysql root password for localhost root account is updated
  mysql_user:
    name: root
    host: "localhost"
    password: "{{ dbrootpasswd }}"
    
- name: Ensure .my.cnf file with root password credentials exists
  blockinfile:
    path: /root/.my.cnf
    owner: root
    group: root
    mode: 0600
    create: yes
    marker: "# {mark} ANSIBLE MANAGED BLOCK #"
    block: |
      [client]
      user = root
      password = {{ dbrootpasswd }}

- name: Ensure mysql root password for all others root accounts is updated
  mysql_user:
    name: root
    host: "{{ item }}"
    password: "{{ dbrootpasswd }}"
  with_items:
    - "127.0.0.1"
    - "::1"

- name: Ensure default none-needed users are deleted
  mysql_user:
    name: ''
    host: "{{ item }}"
    state: absent
  with_items:
    - user: ''
      host: "localhost"
    - user: ''
      host: "{{ hostname.short }}.{{ hostname.domain }}"
    - user: "root"
      host: "{{ hostname.short }}.{{ hostname.domain }}"

- name: Remove the test database
  mysql_db:
    name: test
    state: absent

Le rôle blog

Le fichier de définition de variables par défaut var renseigner la liste des paquets à installer, les paramètres de la base de données pour wordpress: nom de la base, l’utilisateur et son mot de passe.

Ici on va montrer comment redéfinir les variables dans un tabbleau associatif (aussi appelé hash) dans un format que l’on pourra utiliser directement dans notre fichier de tâches, sans redéfinir une seconde fois le contenu des variables.

Fichier roles/blog/defaults/main.yml
rpms:
  - wordpress
  - php-pecl-imagick

databases:
  wordpress:
    user: wordpress
    passwd: wordpresspasswd
    base: wordpress

wordpress:
  settings:
    DB_NAME: "{{ databases.wordpress.base }}"
    DB_USER: "{{ databases.wordpress.user }}"
    DB_PASSWORD: "{{ databases.wordpress.passwd }}"

Le fichier de tâches va:

  • Installer les paquets RPM spécifiés dans le fichier roles/blog/defaults/main.yml.
  • Créer la base de données pour wordpress.
  • Créer l’utilisateur mariadb qui aura un accès total à cette base de données uniquement.
  • Configurer wordpress au minimum, c’est à dire renseigner dans le fichier de configuration de wordpress l’utilisateur mariadb, son mot de passe ainsi que le nom de la base.
  • Va mettre les bons droits sur certains répertoires et fichiers.
Fichier roles/blog/tasks/main.yml
# Install RPM package(s)
- name: Ensure RPM(s) are installed and up-to-date
  yum: 
    name: "{{ packages }}"
    state: latest
  vars:
    packages: "{{ rpms }}"

- name: Ensure database exists
  mysql_db:
    name: "{{ databases.wordpress.base }}"
    state: present
    encoding: utf8
  
- name: Ensure database user exists
  mysql_user: 
    name: "{{ databases.wordpress.user }}"
    host: localhost
    password: "{{ databases.wordpress.passwd }}"
    priv: '{{ databases.wordpress.base }}.*:ALL'
    
- name: Ensure wordpress config link exists
  file:
    src: "/etc/wordpress/wp-config.php"
    dest: "/usr/share/wordpress/wp-config.php"
    state: link

# Manage wordpress
- name: Ensure WordPress is configured
  lineinfile: 
    path: /etc/wordpress/wp-config.php
    regexp: "define.*'{{ item.key }}'"
    line: "define('{{ item.key }}', '{{ item.value }}' );"
  with_dict: "{{ wordpress.settings }}"
    
- name: Ensure some WordPress directories are owned by apache
  file:
    path: "/usr/share/wordpress/wp-content/{{ item }}"
    state: directory
    owner: apache
    group: ftp
    mode: 2775
  with_items:
    - gallery
    - uploads
  
- name: Ensure wordpress config files have apache group
  file:
    dest: "{{ item }}"
    owner: root
    group: apache
    mode: 0640  
  with_items:      
  - "/etc/wordpress/wp-config.php"

- name: Ensure wordpress alias is defined for httpd
  copy:
    src: "wordpress.conf"
    dest: "/etc/httpd/conf.d/wordpress.conf"
    backup: yes
  notify:
  - restart httpd

Le nouveau fichier de configuration apache pour wordpress. En effet celui fournit par le paquet restreint l’accès au système (localhost).

Fichier roles/blog/files/wordpress.conf
Alias /wordpress /usr/share/wordpress

<Directory /usr/share/wordpress>
  AllowOverride Options
  Require all granted
</Directory>

<Directory /usr/share/wordpress/wp-content/uploads>
  # Deny access to any php file in the uploads directory
  <FilesMatch "\.(php)$">
    Require all denied
  </FilesMatch>
</Directory>

<Directory /usr/share/wordpress/wp-content/plugins/akismet>
  # Deny access to any php file in the akismet directory
  <FilesMatch "\.(php|txt)$">
    Require all denied
  </FilesMatch>
</Directory>

Exécution de nos playbooks

Le lancement est maintenant bien connu, on spécifie notre environment et notre utilisateur, et on déploie le playbook général d’installation site.yml. Si on n’a pas copié notre clé publique SSH sur le serveur, il faudra aussi rajouter l’option --ask-pass.

ansible-playbook -i staging --user root site.yml

Il est possible qu’un nouveau noyau, ou un composant essentiel ai été mise à jour avec notre playbook, on redémarrera donc notre système.

On peut vérifier qu’un redémarrage est nécessaire avec la commande needs-restarting, elle a été installée avec le rôle system (paquet yum-utils).

needs-restarting -r

Et voilà, il ne reste plus que la post-installation à faire en pointant son navigateur vers la page d’administration de wordpress. Encore une fois, notre déploiement permet de mettre facilement en place un wordpress, sans chercher à le sécuriser. Mais c’est suffisant pour faire du développement wordpress.

Conclusion

Voilà, j’espère vous avoir donner envie d’adopter ce merveilleux outil de déploiement, on peut tout faire avec. La prochaine partie sera consacré à une utilisation plus avancée de nos playbooks et la dernière partie à la partie production et donc plus axée sur la sécurité, mais on va presque tout ré-utiliser, alors gardez le répertoire racine de ce déploiement bien au chaud, il resservira.

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

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.

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 become_user)
  • 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.