TP - Créer un playbook de déploiement d'application flask
Création du projet
- Créez un nouveau dossier
tp2_flask_deployment. - Créez le fichier
ansible.cfgcomme précédemment.
[defaults]
inventory = ./inventory.cfg
roles_path = ./roles
host_key_checking = false
- Créez deux machines ubuntu
ubu1etubu2.
incus launch ubuntu_ansible ubu1
incus launch ubuntu_ansible ubu2
- Créez l'inventaire statique
inventory.cfg.
$ incus list # pour récupérer l'adresse ip puis
[all:vars]
ansible_user=stagiaire
[appservers]
ubu1 ansible_host=10.x.y.z
ubu2 ansible_host=10.x.y.z
- Ajoutez à l'intérieur les deux machines dans un groupe
appservers. - Pinguez les machines.
ansible all -m ping
Facultatif :
git init # à executer à la racine du projet
- Ajoutez un fichier
.gitignoreavec à l'intérieur:
*.retry # Fichiers retry produits lors des execution en echec de ansible-playbook
- Committez vos modifications avec git.
git add -A
git commit -m "démarrage tp2"
Créer le playbook : installer les dépendances
Le but de ce projet est de déployer une application flask, c'est a dire une application web python. Le code (très minimal) de cette application se trouve sur github à l'adresse: https://github.com/e-lie/flask_hello_ansible.git.
N'hésitez pas consulter extensivement la documentation des modules avec leur exemple ou d'utiliser la commande de doc
ansible-doc <module>Créons un playbook : ajoutez un fichier
flask_deploy.yml. avec la base suivante:
- hosts: <hotes_cible_cible_a_completer>
gather_facts: true
become: true
tasks:
Commençons par installer les dépendances de cette application. Tous nos serveurs d'application sont sur ubuntu. Nous pouvons donc utiliser le module apt pour installer les dépendances. Il fournit plus d'options que le module package.
- Avec le module
aptinstallez les applications:python3-dev,python3-pip,python3-virtualenv,virtualenv,nginx,git. Donnez à cette tache le nom:ensure basic dependencies are present. ajoutez pour cela la directivebecome: yesau début du playbook.
En utilisant une loop (et en accédant aux différentes valeurs qu'elle prend avec {{ item }}), on va pouvoir exécuter plusieurs fois cette tâche :
- name: Ensure basic dependencies are present
apt:
name: "{{ item }}"
state: present
loop:
- python3-dev
- python3-pip
- python3-virtualenv
- virtualenv
- nginx
- git
Lancez ce playbook avec la commande
ansible-playbook <nom_playbook>.Ajoutez une tâche
systemdpour s'assurer que le servicenginxest démarré.
- name: Ensure nginx service started
systemd:
name: nginx
state: started
Relancez bien votre playbook à chaque tâche : comme Ansible est idempotent il n'est pas grave en situation de développement d'interrompre l'exécution du playbook et de reprendre l'exécution après un échec.
Ajoutez une tâche pour créer un utilisateur
flasket l'ajouter au groupewww-data. Utilisez bien le paramètreappend: yespour éviter de supprimer des groupes à l'utilisateur.
- name: Add the user running webapp
user:
name: "flask"
state: present
append: yes # important pour ne pas supprimer les groupes d'un utilisateur existant
groups:
- "www-data"
N'hésitez pas à tester l'option --diff -v avec vos commandes pour voir l'avant-après.
Récupérer le code de l'application
Pour déployer le code de l'application deux options sont possibles.
- Télécharger le code dans notre projet et le copier sur chaque serveur avec le module
syncronizequi fait une copie rsync. - Utiliser le module
git.
- Télécharger le code dans notre projet et le copier sur chaque serveur avec le module
Nous allons utiliser la deuxième option (
git) qui est plus cohérente pour le déploiement et la gestion des versions logicielles. Allez voir la documentation pour voir comment utiliser ce module.Utilisez-le pour télécharger le code source de l'application (branche
master) dans le dossier/home/flask/hellomais en désactivant la mise à jour (au cas où le code change).
- name: Git clone/update python hello webapp in user home
git:
repo: "https://github.com/e-lie/flask_hello_ansible.git"
dest: /home/flask/hello
clone: yes
update: no
- Lancez votre playbook et allez vérifier sur une machine en ssh que le code est bien téléchargé.
Installez les dépendances python de l'application
Le langage python a son propre gestionnaire de dépendances pip qui permet d'installer facilement les librairies d'un projet. Il propose également un méchanisme d'isolation des paquets installés appelé virtualenv. Normalement installer les dépendances python nécessite 4 ou 5 commandes shell.
nos dépendances sont indiquées dans le fichier
requirements.txtà la racine du dossier d'application.pipa une option spéciale pour gérer ces fichiers.Nous voulons installer ces dépendances dans un dossier
venvégalement à la racine de l'application.Nous voulons installer ces dépendances en version python3 avec l'argument
virtualenv_python: python3.même si nous pourrions demander à Ansible de lire ce fichier, créer une variable qui liste ces dépendances et les installer une par une, nous n'allons pas utiliser
loop. Le but est de toujours trouver le meilleur module pour une tâche.
Avec ces informations et la documentation du module pip installez les dépendances de l'application.
Cliquez pour voir la solution :
- name: Install python dependencies for the webapp in a virtualenv
pip:
requirements: /home/flask/hello/requirements.txt
virtualenv: /home/flask/hello/venv
virtualenv_python: python3
state: present
Changer les permissions sur le dossier application
Notre application sera exécutée en tant qu'utilisateur flask pour des raisons de sécurité. Pour cela le dossier doit appartenir à cet utilisateur or il a été créé en tant que root (à cause du become: yes de notre playbook).
- Créez une tache
filequi change le propriétaire du dossier de façon récursive. N'hésitez pas à tester l'option--diff -vavec vos commandes pour voir l'avant-après.
- name: Change permissions of app directory
file:
path: /home/flask
state: directory
owner: "flask"
group: www-data
recurse: true
Module Template : configurer le service qui fera tourner l'application
Notre application doit tourner comme c'est souvent le cas en tant que service (systemd). Pour cela nous devons créer un fichier service adapté hello.service et le copier dans le dossier /etc/systemd/system/.
Ce fichier est un fichier de configuration qui doit contenir le texte suivant:
[Unit]
Description=Gunicorn instance to serve hello
After=network.target
[Service]
User=flask
Group=www-data
WorkingDirectory=/home/flask/hello
Environment="PATH=/home/flask/hello/venv/bin"
ExecStart=/home/flask/hello/venv/bin/gunicorn --workers 3 --bind unix:hello.sock -m 007 app:app
[Install]
WantedBy=multi-user.target
Pour gérer les fichier de configuration on utilise généralement le module template qui permet à partir d'un fichier modèle situé dans le projet ansible de créer dynamiquement un fichier de configuration adapté sur la machine distante.
Créez un dossier
templates, avec à l'intérieur le fichierapp.service.j2contenant le texte précédent.Utilisez le module
templatepour le copier au bon endroit avec le nomhello.service.Utilisez ensuite
systemdpour démarrer ce service (avecstate: restarteddans le cas où le fichier a changé).
Configurer nginx
- Comme précédemment créez un fichier de configuration
hello.test.confdans le dossier/etc/nginx/sites-availableà partir du fichier modèle:
nginx.conf.j2
# {{ ansible_managed }}
# La variable du dessus indique qu'il ne faut pas modifier ce fichier directement, on peut l'écraser dans notre config Ansible pour écrire un message plus explicite à ses collègues
server {
listen 80;
server_name hello.test;
location / {
include proxy_params;
proxy_pass http://unix:/home/flask/hello/hello.sock;
}
}
Utilisez
filepour créer un lien symbolique de ce fichier dans/etc/nginx/sites-enabled(avec l'optionforce: yespour écraser le cas échéant). C'est une bonne pratique Nginx que nous allons respecter dans notre playbook Ansible.Ajoutez une tache pour supprimer le site
/etc/nginx/sites-enabled/default.Ajouter une tâche de redémarrage de nginx.
Ajoutez l'IP de la VM puis
hello.testséparé par un espace dans votre fichier/etc/hosts, pour que le domainehello.testsoit résolu par l'IP d'un des serveurs d'application.Visitez l'application dans un navigateur et debugger le cas échéant.
Solution intermédiaire
flask_deploy.yml
Code de solution :
- hosts: appservers
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
- name: Ensure basic dependencies are present
apt:
name:
- python3-dev
- python3-pip
- python3-virtualenv
- virtualenv
- nginx
- git
state: present
- name: Ensure nginx service started
systemd:
name: nginx
state: started
- name: Add the user running webapp
user:
name: "flask"
state: present
append: yes # important pour ne pas supprimer les groupes d'un utilisateur existant
groups:
- "www-data"
- name: Git clone/update python hello webapp in user home
git:
repo: "https://github.com/e-lie/flask_hello_ansible.git"
dest: /home/flask/hello
clone: yes
update: no
- name: Install python dependencies for the webapp in a virtualenv
pip:
requirements: /home/flask/hello/requirements.txt
virtualenv: /home/flask/hello/venv
virtualenv_python: python3
- name: Change permissions of app directory recursively if needed
file:
path: /home/flask
state: directory
owner: "flask"
group: www-data
recurse: true
- name: Template systemd service config
template:
src: templates/app.service.j2
dest: /etc/systemd/system/hello.service
- name: Start systemd app service
systemd:
name: "hello.service"
state: restarted
enabled: yes
- name: Template nginx site config
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/hello.test.conf
- name: Remove default nginx site config
file:
path: /etc/nginx/sites-enabled/default
state: absent
- name: Enable nginx site for hello webapp
file:
src: /etc/nginx/sites-available/hello.test.conf
dest: /etc/nginx/sites-enabled/hello.test.conf
state: link
force: yes
- name: Restart nginx service
systemd:
name: "nginx"
state: restarted
enabled: yes
- Renommez votre fichier
flask_deploy.ymlenflask_deploy_precorrection.yml. - Copiez la solution dans un nouveau fichier
flask_deploy.yml. - Lancez le playbook de solution
ansible-playbook flask_deploy.yml. - Ajoutez
hello.testdans votre fichier/etc/hosts - enfin, testez votre application en visitant la page
hello.test.
Facultatif :
- Validez/Commitez votre version corrigée:
git add -A
git commit -m "tp2 solution intermediaire"
- Installez l'extension
git graphdans vscode. - Cliquez sur le bouton
Git Graphen bas à gauche de la fenêtre puis cliquez sur le dernier point (commit) avec la légende tp2 solution intermédiaire. Vous pouvez voir les fichiers et modifications ajoutées depuis le dernier commit.
!!! Nous constatons que git a mémorisé les versions successives du code et permet de revenir à une version antérieure de votre déploiement.
Ajouter un handler pour nginx et le service
Pour le moment dans notre playbook, les deux tâches de redémarrage de service sont en mode restarted c'est à dire qu'elles redémarrent le service à chaque exécution (résultat: changed) et ne sont donc pas idempotentes. En imaginant qu'on lance ce playbook toutes les 15 minutes dans un cron pour stabiliser la configuration, on aurait un redémarrage de nginx 4 fois par heure sans raison.
On désire plutôt ne relancer/recharger le service que lorsque la configuration conrespondante a été modifiée. c'est l'objet des tâches spéciales nommées handlers.
Ajoutez une section handlers: à la suite
Déplacez la tâche de redémarrage/reload de
nginxdans cette section et mettez comme nomreload nginx.Ajoutez aux deux tâches de modification de la configuration la directive
notify: <nom_du_handler>.Testez votre playbook. il devrait être idempotent sauf le restart de
hello.service.Testez le handler en ajoutant un commentaire dans le fichier de configuration
nginx.conf.j2.
- name: template nginx site config
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/{{ app.domain }}.conf
notify: reload nginx
...
handlers:
- name: reload nginx
systemd:
name: "nginx"
state: reloaded
# => penser aussi à supprimer la tâche maintenant inutile de restart de nginx précédente
Solution
- Pour la solution complète, clonons le dépôt via cette commande :
cd # Pour revenir dans notre dossier home
git clone https://github.com/Uptime-Formation/ansible-tp-solutions -b tp2_correction
Vous pouvez également consulter la solution directement sur le site de Github : https://github.com/Uptime-Formation/ansible-tp-solutions/tree/tp2_correction
Réorganisation : rendre le playbook dynamique avec des variables, puis une boucle, pour se préparer aux rôles
Améliorer notre playbook avec des variables
Ajoutons des variables pour gérer dynamiquement les paramètres de notre déploiement:
Ajoutez une section
vars:avant la sectiontasks:du playbook.Mettez dans cette section la variable suivante (dictionnaire):
app:
name: hello
user: flask
domain: hello.test
(il faudra modifier votre fichier /etc/hosts pour faire pointer le domaine hello.test vers l'IP de votre conteneur)
- ajoutons une petite task dans la section
pre_tasks:pour afficher cette variable au début du playbook, c'est le moduledebug:
pre_tasks:
- debug:
msg: "{{ app }}"
Remplacez dans le playbook précédent et les deux fichiers de template:
- toutes les occurences de la chaine
hellopar{{ app.name }} - toutes les occurences de la chaine
flaskpar{{ app.user }} - toutes les occurences de la chaine
hello.testpar{{ app.domain }}
- toutes les occurences de la chaine
Relancez le playbook : toutes les tâches devraient renvoyer
okà part les "restart" car les valeurs sont identiques.
Facultatif :
- Remplacez les valeurs correspondante dans le playbook par ces nouvelles variables.
app:
name: hello
user: flask
domain: hello.test
repository: https://github.com/e-lie/flask_hello_ansible.git
version: master
- Pour la solution intermédiaire, clonons le dépôt via cette commande :
cd # Pour revenir dans notre dossier home
git clone https://github.com/Uptime-Formation/ansible-tp-solutions -b tp2_before_handlers_correction tp2_before_handlers
Vous pouvez également consulter la solution directement sur le site de Github : https://github.com/Uptime-Formation/ansible-tp-solutions/tree/tp2_before_handlers_correction
Rendre le playbook dynamique avec une boucle
Nous allons nous préparer à transformer ce playbook en rôle, plus général.
Plutôt qu'une variable app unique on voudrait fournir au playbook une liste d'application à installer (liste potentiellement définie durant l'exécution).
- Identifiez dans le playbook précédent les tâches qui sont exactement communes à l'installation des deux apps.
Réponse
Il s'agit des tâches d'installation des dépendances
aptet de vérification de l'état de nginx (démarré)
- Créez un nouveau fichier
deploy_app_tasks.ymlet copier à l'intérieur la liste de toutes les autres tâches mais sans les handlers que vous laisserez à la fin du playbook.
Réponse
Il reste donc dans le playbook seulement les deux premières tâches et les handlers, les autres tâches (toutes celles qui contiennent des parties variables) sont dans
deploy_app_tasks.yml.
Ce nouveau fichier n'est pas à proprement parler un playbook mais une liste de tâches.
Utilisez
include_tasks:(cela se configure comme une task un peu spéciale) pour importer cette liste de tâches à l'endroit où vous les avez supprimées.Vérifiez que le playbook fonctionne et est toujours idempotent. Note: si vous avez récupéré une solution, il va falloir récupérer le fichier d'inventaire d'un autre projet et adapter la section
hosts:du playbook.Ajoutez une tâche
debug: msg={{ app }}(c'est une syntaxe abrégée appelée free-form ) au début du playbook pour visualiser le contenu de la variable. Note : La version non-free-form (version longue) de cette tâche est :
debug:
msg: {{ app }}
- Ensuite remplacez la variable
apppar une listeflask_appsde deux dictionnaires (avecname,domain,userdifférents les deux dictionnaires etrepositoryetversionidentiques).
flask_apps:
- name: hello
domain: "hello.test"
user: "flask"
version: master
repository: https://github.com/e-lie/flask_hello_ansible.git
- name: hello2
domain: "hello2.test"
user: "flask2"
version: version2
repository: https://github.com/e-lie/flask_hello_ansible.git
Il faudra modifier la tâche de debug par debug: msg={{ flask_apps }}. Observons le contenu de cette variable.
- A la task
debug:, ajoutez la directiveloop: "{{ flask_apps }}(elle se situe à la hauteur du nom de la task et du module) et remplacez lemsg={{ flask_apps }}parmsg={{ item }}. Que se passe-t-il ? note: il est normal que le playbook échoue désormais à l'étapeinclude_tasks
La directive loop_var permet de renommer la variable sur laquelle on boucle par un nom de variable de notre choix. A quoi sert-elle ? Rappelons-nous : sans elle, on accéderait à chaque item de notre liste flask_apps avec la variable item. Cela nous permet donc de ne pas modifier toutes nos tasks utilisant la variable app et de ne pas avoir à utiliser item à la place.
- Utilisez la directive
loopetloop_control+loop_varsur la tâcheinclude_taskspour inclure les tâches pour chacune des deux applications, en complétant comme suit :
- include_tasks: deploy_app_tasks.yml
loop: "{{ A_COMPLETER }}"
loop_control:
loop_var: A_COMPLETER
Créez le dossier
group_varset déplacez le dictionnaireflask_appsdans un fichiergroup_vars/appservers.yml. Comme son nom l'indique ce dossier permet de définir les variables pour un groupe de serveurs dans un fichier externe.Testez en relançant le playbook que le déplacement des variables est pris en compte correctement.
Pour la solution : activez la branche
tp2_correctionavecgit checkout tp2_correction.