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:
[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.
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.
- import_playbook: system.yml - import_playbook: lamp.yml - import_playbook: blog.yml
- 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.
- hosts: vps roles: - common - system
- hosts: vps roles: - common - lamp
- 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.
- 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.
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.
- 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.
# 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
# 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é.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
# 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.
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
- 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.
############################################################################## 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.
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.
- 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.
# 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.
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.
# 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).
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.
Leave a Reply