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.
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'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
On va créer nos deux répertoires d’environnement :
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
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/groupvars/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.
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 :
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:
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
Certes, les fichiers sont vides, mais on va maintenant les remplir.
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:
Fichier system.yml
- hosts: vps
roles:
- common
- system
Fichier lamp.yml
- hosts: vps
roles:
- common
- lamp
Fichier blog.yml
- hosts: vps
roles:
- common
- blog
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"
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/hostvars/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/groupvars/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:
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_defaultipv4.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 }}
##############################################################################
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:
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 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:
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>
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](http://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.
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.