Integrate devops handbook notes

This commit is contained in:
Fred Pauchet 2021-12-31 23:20:04 +01:00
parent f26ef70f59
commit 7167745bb3
6 changed files with 293 additions and 37 deletions

View File

@ -1089,3 +1089,14 @@ ENTRYPOINT ["/entrypoint"]
----
NOTE: Voir comment nous pouvons intégrer toutes ces commandes au niveau de la CI et au niveau du déploiement (Docker-compose ?)
==== Base de données
Parfois, SQLite peut être une bonne option:
[quote]
Write througput is the area where SQLite struggles the most, but there's not a ton of compelling data online about how it fares, so I got some of my own: I spun up a Equinix m3.large.x86 instance, and ran a slightly modified1 version of the SQLite kvtest2 program on it. Writing 512 byte blobs as separate transactions, in WAL mode with synchronous=normal3, temp_store=memory, and mmap enabled, I got 13.78μs per write, or ~72,568 writes per second. Going a bit larger, at 32kb writes, I got 303.74μs per write, or ~3,292 writes per second. That's not astronomical, but it's certainly way more than most websites being used by humans need. If you had 10 million daily active users, each one could get more than 600 writes per day with that.
[quote]
Looking at read throughput, SQLite can go pretty far: with the same test above, I got a read throughput of ~496,770 reads/sec (2.013μs/read) for the 512 byte blob. Other people also report similar results — Expensify reports that you can get 4M QPS if you're willing to make some slightly more involved changes and use a beefier server. Four million QPS is enough that every internet user in the world could make ~70 queries per day, with a little headroom left over4. Most websites don't need that kind of throughput. cite:{consider_sqlite}

View File

@ -338,57 +338,239 @@ Ceci évite davoir à redéployer lensemble dune application.
> The lesson here is that depending on something that carries baggage that you dont need can cause you troubles that you didnt except.
Ce principe stipule qu'un client ne peut en aucun cas dépendre d'une méthode dont il n'a pas besoin.
Ce principe stipule qu'un client ne doit pas dépendre d'une méthode dont il n'a pas besoin.
Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces.
Ceci permet aux différents consommateurs de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin précis.
Un exemple est d'avoir une interface permettant d'accéder à des éléments.
Modifier cette interface pour permettre l'écriture impliquerait que toutes les applications ayant déjà accès à la première, obtiendraient (par défaut) un accès en écriture, ce qui n'est pas souhaité/souhaitable.
GNU/Linux Magazine cite:{gnu_linux_mag_hs_104, 37-42} propose un exemple d'interface permettant d'implémenter une imprimante:
Pour contrer ceci, on aurait alors une première interface permettant la lecture, tandis qu'une deuxième (héritant de la première) permettrait l'écriture. On aurait alors le schéma suivant :
[source,java]
----
interface IPrinter
{
public abstract void printPage();
* A : lecture
* B (héritant de A) : lecture (par A) et écriture.
public abstract void scanPage();
Mais ceci s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces,
les variables déclarées, ...
public abstract void faxPage();
}
public class Printer
{
protected string name;
public Printer(string name)
{
this.name = name;
}
}
----
L'implémentation d'une imprimante multifonction aura tout son sens:
[source,java]
----
public class AllInOnePrinter implements Printer extends IPrinter
{
public AllInOnePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Scan");
}
public void faxPage()
{
System.out.println(this.name + ": Fax");
}
}
----
Tandis que l'implémentation d'une imprimante premier-prix ne servira pas à grand chose:
[source,java]
----
public class FirstPricePrinter implements Printer extends IPrinter
{
public FirstPricePrinter(string name)
{
super(name);
}
public void printPage()
{
System.out.println(this.name + ": Impression");
}
public void scanPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
public void faxPage()
{
System.out.println(this.name + ": Fonctionnalité absente");
}
}
----
L'objectif est donc de découpler ces différentes fonctionnalités en plusieurs interfaces bien spécifiques, implémentant chacune une opération isolée:
[source,java]
----
interface IPrinterPrinter
{
public abstract void printPage();
}
interface IPrinterScanner
{
public abstract void scanPage();
}
interface IPrinterFax
{
public abstract void faxPage();
}
----
Cette réflexion s'applique finalement à n'importe quel composant: votre système d'exploitation, les librairies et dépendances tierces, les variables déclarées, ...
Quel que soit le composant que l'on utilise ou analyse, il est plus qu'intéressant de se limiter uniquement à ce dont nous avons besoin plutôt que
En Python, ce comportement est inféré lors de lexécution, et donc pas vraiment dapplication pour notre contexte d'étude: de manière plus générale, les langages dynamiques sont plus flexibles et moins couplés que les langages statiquement typés, pour lesquels l'application de ce principe-ci permettrait de mettre à jour une DLL ou un JAR sans que cela nait dimpact sur le reste de lapplication.
Il est ainsi possible de trouver quelques horreurs, dans tous les langages:
[source,javascript]
----
/*!
* is-odd <https://github.com/jonschlinkert/is-odd>
*
* Copyright (c) 2015-2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
const isNumber = require('is-number');
module.exports = function isOdd(value) {
const n = Math.abs(value);
if (!isNumber(n)) {
throw new TypeError('expected a number');
}
if (!Number.isInteger(n)) {
throw new Error('expected an integer');
}
if (!Number.isSafeInteger(n)) {
throw new Error('value exceeds maximum safe integer');
}
return (n % 2) === 1;
};
----
Voire, son opposé, qui dépend évidemment du premier:
[source,javascript]
----
/*!
* is-even <https://github.com/jonschlinkert/is-even>
*
* Copyright (c) 2015, 2017, Jon Schlinkert.
* Released under the MIT License.
*/
'use strict';
var isOdd = require('is-odd');
module.exports = function isEven(i) {
return !isOdd(i);
};
----
Il ne s'agit que d'un simple exemple, mais qui tend à une seule chose: gardez les choses simples (et, éventuellement, stupides) (((kiss))).
Dans l'exemple ci-dessus, l'utilisation du module `is-odd` requière déjà deux dépendances: `is-even` et `is-number`.
Imaginez la suite.
==== Dependency inversion Principle
Dans une architecture conventionnelle, les composants de haut-niveau dépendent directement des composants de bas-niveau.
L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente.
Lobjectif est que les interfaces soient les plus stables possibles, afin de réduire au maximum les modifications qui pourraient y être appliquées.
De cette manière, toute modification fonctionnelle pourra être directement appliquée sur le composant de bas-niveau, sans que l'interface ne soit impactée.
> The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies
refer only to abstractions, not to concretions.
Lobjectif est que les interfaces soient stables, et de réduire au maximum les modifications qui pourraient y être appliquées.
De la meme manière, il convient déviter de surcharger des fonctions ou des classes concrètes (= non abstraites).
En termes architecturaux, ce principe définira les frontières dont il a déjà été question,
en demandant à ce quune délimitation ne se base que sur une dépendance qui soit …
cela permet notamment une séparation claire au niveau des frontières, en inversant le flux de dépendance et en faisant en sorte (par exemple) que les règles métiers naient aucune connaissance des interfaces graphiques qui les exploitent. Ces interfaces pouvant être desktop, web, … cela na pas vraiment dimportance. Cela autorise une for.e dimmunité entre les composants.
Ne pas oublier quécrire du nouveau code est toujours plus facile que de modifier du code existant.
> The database is really nothing more than a big bucket of bits where we store our data on a long term basis (the database is a detail, chap 30, page 281)
Dun point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, sil sagit dun disque magnétique, de ram, … en fait, on ne devrait même pas savoir sil y a un disque du tout.
Le composant de haut-niveau peut définir qu'il s'attend à avoir un `Publisher`, afin de publier du contenu vers un emplacement particulier.
Plusieurs implémentation de cette interface peuvent alors être mise en place:
* Une publication par SSH
* Une publication par FTP
* Une publication
* ...
> The dependency inversion principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions. cite:{clean_architecture}
L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances.
Django est bourré de ce principe, que ce soit pour les _middlewares_ ou pour les connexions aux bases de données.
Lorsque nous écrivons ceci dans notre fichier de configuration,
[source,python]
----
# [snip]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# [snip]
----
Django ira simplement récupérer chacun de ces middlewares, qui répondent chacun à une https://docs.djangoproject.com/en/4.0/topics/http/middleware/#writing-your-own-middleware[interface clairement définie], dans l'ordre.
Il n'y a donc pas de magie; c'est le développeur qui va simplement brancher ou câbler des fonctionnalités au niveau du framework, en les déclarant au bon endroit.
Pour créer un nouveau _middleware_, il suffira d'implémenter le code suivant et de l'ajouter dans la configuration de l'application:
[source,python]
----
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
return middleware
----
Dans d'autres projets écrits en Python, ce type de mécanisme peut être implémenté relativement facilement en utilisant les modules https://docs.python.org/3/library/importlib.html[importlib] et la fonction `getattr`.
Un autre exemple concerne les bases de données: pour garder un maximum de flexibilité, Django ajoute une couche d'abstraction en permettant de spécifier le moteur de base de données que vous souhaiteriez utiliser, qu'il s'agisse d'SQLite, MSSQL, Oracle, PostgreSQL ou MySQL/MariaDB footnote:[http://howfuckedismydatabase.com/].
> The database is really nothing more than a big bucket of bits where we store our data on a long term basis cite:{clean_architecture, 281}.
Dun point de vue architectural, nous ne devons pas nous soucier de la manière dont les données sont stockées, sil sagit dun disque magnétique, de ram, ... en fait, on ne devrait même pas savoir sil y a un disque du tout.
Et Django le fait très bien pour nous.
En termes architecturaux, ce principe autorise une définition des frontières, et en permettant une séparation claire en inversant le flux de dépendances et en faisant en sorte que les règles métiers n'aient aucune connaissance des interfaces graphiques qui les exploitent ou des moteurs de bases de données qui les stockent.
Ceci autorise une forme d'immunité entre les composants.
==== Sources
* http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp[Understanding SOLID principles on CodeProject]
* http://en.wikipedia.org/wiki/Software_craftsmanship[Software Craftmanship]
* http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/[Dependency Injection is NOT the same as dependency inversion]
* http://en.wikipedia.org/wiki/Dependency_injection[Injection de dépendances]

View File

@ -9,16 +9,19 @@ This leads to architectures that may be make the system easy to develop, but lea
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é et accessible depuis 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 puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
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 puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque.
Lobjectif est qu'il soit rapide et fiable.
Ceci peut être atteint au travers dun partitionnement correct, incluant le fait que le composant principal sassure que
chaque sous-composant est correctement démarré intégré et supervisé.
Ceci peut être atteint au travers dun partitionnement correct, incluant le fait que le composant principal sassure que chaque sous-composant est correctement démarré intégré et supervisé.
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.
A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement cite:{devops_handbook}.
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.
[quote,DevOps Handbook, Introduction, page 8]
Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and disruptions such as service outages, service impairments, or security or compliance failures.
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és.

View File

@ -1,9 +1,20 @@
== Logging
La structure des niveaux de journaux est essentielle.
[quote,Dan North, former ToughtWorks consultant]
When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m. Low printer toner is not an ERROR.
* *DEBUG*: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
* *INFO*: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
* *WARN*: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
* *ERROR*: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, ...
* *FATAL* (ou *EXCEPTION*): ... généralement suivie d'une terminaison du programme ;-) - Bind raté d'un socket, etc.
La configuration des _loggers_ est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore.
Il est ainsi possible de définir des formattages, gestionnaires (_handlers_) et loggers distincts, en fonction de nos applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements.
Il n'est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les `StreamHandler`.
La configuration que nous allons utiliser est celle-ci:
@ -37,7 +48,7 @@ LOGGING = {
'handlers': ['console'],
'level': env("LOG_LEVEL", default="DEBUG"),
'propagate': True,
},
},
}
}
----

View File

@ -0,0 +1,43 @@
=== Arborescences
[source,python]
----
----
[source,python]
----
# <app>/management/commands/rebuild.py
"""This command manages Closure Tables implementation
It adds new levels and cleans links between entities.
This way, it's relatively easy to fetch an entire tree with just one tiny request.
"""
from django.core.management.base import BaseCommand
from rps.structure.models import Entity, EntityTreePath
class Command(BaseCommand):
def handle(self, *args, **options):
entities = Entity.objects.all()
for entity in entities:
breadcrumb = [node for node in entity.breadcrumb()]
tree = set(EntityTreePath.objects.filter(descendant=entity))
for idx, node in enumerate(breadcrumb):
tree_path, _ = EntityTreePath.objects.get_or_create(
ancestor=node, descendant=entity, weight=idx + 1
)
if tree_path in tree:
tree.remove(tree_path)
for tree_path in tree:
tree_path.delete()
----

View File

@ -74,6 +74,12 @@
isbn = {978-1-449-37332-0},
release = {Fifteenth release - 2021-03-26}
}
@misc{
consider_sqlite,
title = {Consider SQLite},
year = {2021},
url = {https://blog.wesleyac.com/posts/consider-sqlite}
}
@misc{agiliq_admin,
title = {Django Admin Cookbook, How to do things with Django admin},
year = {2018},