Include images :D
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Fred Pauchet 2020-11-28 21:58:58 +01:00
commit d087f0726b
10 changed files with 107 additions and 93 deletions

View File

@ -2,7 +2,7 @@
Pour la méthode de travail et de développement, on va se baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*.
L'idée derrière cette méthode consiste à pousser les concepts suivants (repris grossièrement de la https://12factor.net/fr/[page d'introduction] :
L'idée derrière cette méthode, et indépendamment des langages de développement utilisés, consiste à suivre un ensemble de douze concepts , afin de:
. *Faciliter la mise en place de phases d'automatisation*; plus concrètement, de faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue]/link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu]
. *Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet*
@ -15,15 +15,29 @@ Pour reprendre de manière très brute les différentes idées derrière cette m
. *Une base de code unique, suivie par un système de contrôle de versions*. Chaque déploiement de l'application se basera sur cette source, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. On utilisera un dépôt Git - Github, Gitlab, Gitea, ... Au choix.
. *Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation*. L'idée est que chaque installation ou configuration se fasse toujours de la même manière, et puisse être répétée quel que soit l'environnement cible. Cela permet également d'éviter que l'application n'utilise une dépendance qui soit déjà installée sur un des sytèmes de développement, et qu'elle soit difficile à répercuter sur un autre environnement. Dans notre cas, cela pourra être fait au travers de https://pypi.org/project/pip/[pip - Package Installer for Python]. Dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné, et vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
. *Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation*. L'idée est que chaque installation ou configuration se fasse toujours de la même manière, et puisse être répétée quel que soit l'environnement cible. Cela permet également d'éviter que l'application n'utilise une dépendance qui soit déjà installée sur un des sytèmes de développement, et qu'elle soit difficile à répercuter sur un autre environnement.
Dans notre cas, cela pourra être fait au travers de https://pypi.org/project/pip/[pip - Package Installer for Python].
Dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné, et vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
. *Sauver la configuration directement au niveau de l'environnement*. On veut par exemple éviter d'avoir à recompiler/redéployer l'application parce que l'adresse du serveur de messagerie a été modifiée, ou parce qu'un protocole a changé en cours de route. Concrètement, toute information susceptible de modifier le comportement intrinsèque de l'application doit se trouver dans un fichier ou dans une variable d'environnement. En allant un pas plus loin, cela permettra de paramétrer facilement un container, simplement en modifiant une variable de configuration, qui spécifiera la base de données sur laquelle l'application devra se connecter. Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, on ne doit trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
. *Sauver la configuration directement au niveau de l'environnement*.
On veut par exemple éviter d'avoir à recompiler/redéployer l'application parce que l'adresse du serveur de messagerie a été modifiée, ou parce qu'un protocole a changé en cours de route.
Concrètement, toute information susceptible de modifier le comportement intrinsèque de l'application doit se trouver dans un fichier ou dans une variable d'environnement.
En allant un pas plus loin, cela permettra de paramétrer facilement un container, simplement en modifiant une variable de configuration, qui spécifiera la base de données sur laquelle l'application devra se connecter.
Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, on ne doit trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code.
. *Traiter les ressources externes comme des ressources attachées*. On parle de bases de données, de services de mise en cache, d'API externes, ... L'application doit également être capable d'effectuer des changements au niveau de ces ressources sans que le code intrinsèque ne soit modifié. On parle alors de *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le *type* n'est pas obligatoirement défini. On veut par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached". De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée. Si une base de données ne fonctionne pas correctement (problème matériel?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
. *Traiter les ressources externes comme des ressources attachées*.
On parle de bases de données, de services de mise en cache, d'API externes, ...
L'application doit également être capable d'effectuer des changements au niveau de ces ressources sans que le code intrinsèque ne soit modifié. On parle alors de *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le *type* n'est pas obligatoirement défini.
On veut par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached". De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée.
Si une base de données ne fonctionne pas correctement (problème matériel?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement.
. *Séparer proprement les phases de construction, de mise à disposition et d'exécution*. La *construction* (_build_) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources. La *mise à disposition* (_release_) associe cet ensemble à une configuration prête à être exécutée, tandis que la phase d'*exécution* (_run_) démarre les processus nécessaires au bon fonctionnement de l'application. On doit pouvoir se baser sur les _releases_ de Gitea, sur un serveur d'artefacts ou sur https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano].
. *Séparer proprement les phases de construction, de mise à disposition et d'exécution*.
La *construction* (_build_) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources.
La *mise à disposition* (_release_) associe cet ensemble à une configuration prête à être exécutée, tandis que la phase d'*exécution* (_run_) démarre les processus nécessaires au bon fonctionnement de l'application. On doit pouvoir se baser sur les _releases_ de Gitea, sur un serveur d'artefacts ou sur https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano].
. *Les processus d'exécution ne doivent rien connaître ou conserver de l'état de l'application*. On suppose donc que toute information stockée en mémoire ou sur disque n'altérera pas le comportement futur de l'application, par exemple après un redémarrage non souhaité. Si une réinitialisation devait être nécessaire, l'application ne devra pas compter sur la présence d'une information au niveau du système.
. *Les processus d'exécution ne doivent rien connaître ou conserver de l'état de l'application*.
On suppose donc que toute information stockée en mémoire ou sur disque n'altérera pas le comportement futur de l'application, par exemple après un redémarrage non souhaité.
Si une réinitialisation devait être nécessaire, l'application ne devra pas compter sur la présence d'une information au niveau du système.
. *Autoriser la liaison d'un port de l'application à un port du système hôte*. Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale. L'idée ici est qu'elles puissent être joignables grâce à un mécanisme de pont, où l'hôte effectue la redirection vers l'un des ports ouverts par l'application, typiquement, en HTTP ou via un autre protocole.

View File

@ -1,49 +1,52 @@
= Déploiement
On va déjà parler de déploiement.
Le serveur que django met à notre disposition _via_ la commande `runserver` est prévu uniquement pour le développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...).
De même, la base de donnée doit être capable de supporter plusieurs utilisateurs et connexions simultanément: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Mais sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
Il est donc plus que bénéfique de passer sur quelque chose de plus solide.
Si vous avez suivi les étapes jusqu'ici, vous devriez disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données type SQLite.
En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce dont vous aurez besoin au final.
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé sur un serveur.
Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles fonctionnent sur un environnement en particulier.
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes.
Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash.
Le serveur que django met à notre disposition _via_ la commande `runserver` est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...).
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois. En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanément... En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce dont vous aurez besoin au final.
Dans cette partie, nous aborderons les points suivants:
* La définition de l'infrastructure nécessaire à notre application,
* La configuration de l'hôte, qui hébergera l'application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt.
* Les différentes méthodes de supervision de l'application: comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Une partie sur la sécurité et la sécurisation de l'hôte.
* Définir l'infrastructure nécessaire à notre application et configurer l'hôte qui hébergera l'application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt.
* Déployer notre code source
* Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Rendre notre application accessible depuis l'extérieur.
== Infrastructure
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, nous devrions arriver à quelque chose de synthéthique:
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
== Code source
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application.
Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
== Outils de supervision et de mise à disposition
Pour une mise ne production, le standard _de facto_ est le suivant:
* Nginx comme reverse proxy
@ -53,13 +56,15 @@ Pour une mise ne production, le standard _de facto_ est le suivant:
* Celery et RabbitMQ pour l'exécution de tâches asynchrones
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
== Méthode de déploiement
Nous allons détailler ci-dessous trois méthodes de déploiement:
* Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de configurer un _load balancer_, de routeur plusieurs basées de données, mais ce sera le premier cas de figure.
* Dans des containers, avec Docker-Compose.
* Sur une *Plateforme en tant que Service* (ou plus simplement, *PaaS*), pour faire abstraction de toute la couche de configuration du serveur.
== Sur une machine hôte
=== Sur une machine hôte
La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur *root*, car la moindre faille pourrait avoir des conséquences catastrophiques.
@ -72,50 +77,42 @@ Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'éta
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (**Virtual Private Server**). A vous de choisir celui qui vous convient footnote:[Personnellement, j'ai un petit faible pour Hetzner Cloud].
include::centos+debian.adoc[]
include::debian.adoc[]
== Via Ansible & Salt
include::ansible.adoc[]
include::heroku.adoc[]
include::docker.adoc[]
== Sur Heroku
== Docker-Compose
=== Mise à jour
Script de mise à jour.
[source,bash]
----
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
gunicorn reload -HUP
----
WARNING: le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.
=== Supervision
On peut aussi passer par fabric, ansible, chef ou puppet.
== Supervision
Qu'est-ce qu'on fait des logs après ? :-)
. Sentry
. Sentry via sentry_sdk
. Nagios
. LibreNMS
. Zabbix
include::infrastructure.adoc[]
Il existe également https://munin-monitoring.org[Munin], https://www.elastic.co[Logstash, ElasticSearch et Kibana (ELK-Stack)] ou https://www.fluentd.org[Fluentd].
include::database.adoc[]
== Autres outils
Voir aussi devpi, circus, uswgi, statsd.
See https://mattsegal.dev/nginx-django-reverse-proxy-config.html
== Ressources
* https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/
* https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
* https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
* Let's Encrypt !
include::database.adoc[]

View File

@ -0,0 +1 @@
=== Ansible

View File

@ -1,8 +1,8 @@
=== Déploiement sur CentOS
=== Déploiement sur Debian
[source,bash]
----
yum update
apt update
groupadd --system webapps <1>
groupadd --system gunicorn_sockets <2>
useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift <3>
@ -13,7 +13,7 @@ chown gwift:webapps /home/gwift <5>
<2> On crée un groupe pour les communications via sockets
<3> On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire `/home/gwift`
<4> On crée le répertoire home/gwift
<5> On pourrait sans doute fusioner les étapes 3, 4 et 5.
<5> On donne les droits sur le répertoire /home/gwift
==== Installation des dépendances systèmes
@ -26,11 +26,6 @@ Pour CentOS, vous avez donc deux possibilités :
[source,bash]
----
yum install python36 -y
# CentOS 7 ne dispose que de la version 3.7 d'SQLite. On a besoin d'une version 3.8 au minimum:
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-devel-3.8.11-1.fc21.x86_64.rpm
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm
sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_64.rpm -y
----
Ou passer par une installation alternative:
@ -283,30 +278,24 @@ server {
<2> Ce répertoire sera complété par la commande `collectstatic` que l'on verra plus tard. L'objectif est que les fichiers ne demandant aucune intelligence soit directement servis par Nginx. Cela évite d'avoir un processus Python (relativement lent) qui doive être instancié pour servir un simple fichier statique.
<3> Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1
=== Mise à jour
Script de mise à jour.
=== Déploiement sur Debian
[source,bash]
----
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
gunicorn reload -HUP
----
==== Installation des dépendances systèmes
==== Préparation de l'environnement utilisateur
==== Configuration de l'application
==== Création des répertoires de logs
==== Création du répertoire pour le socket
==== Gunicorn
==== Supervision, keep-alive et autoreload
==== Ouverture des ports
==== Installation d'Nginx
== Configuration des sauvegardes
=== Configuration des sauvegardes
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
@ -326,7 +315,7 @@ Et dans le fichier crontab :
----
== Rotation des jounaux
=== Rotation des jounaux
[source,bash]
----

View File

@ -1,3 +1,5 @@
=== Docker-Compose
(c/c Ced' - 2020-01-24)
Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter pour django.

View File

@ -0,0 +1 @@
=== Heroku

View File

@ -1 +0,0 @@
See https://mattsegal.dev/nginx-django-reverse-proxy-config.html

View File

@ -0,0 +1,11 @@
= Ressources et bibliographie
* https://simpleisbetterthancomplex.com/series/beginners-guide/1.11/[Simple Is Better Than Complex]
* https://www.feldroy.com/collections/two-scoops-press/products/two-scoops-of-django-1-11[Two Scoops of Django 1.11]
* https://www.feldroy.com/products/django-crash-course[Django Crash Course]
* https://www.amazon.com/dp/B07BDGC57[Django Design Patterns and Best Practices] (Packt Publishing)
* https://books.agiliq.com/en/latest/README.html[Books by Agiliq]
include::code-snippets.adoc[]
include::legacy.adoc[]