Hashtagueulehttps://hashtagueule.fr Blog feed Thu, 28 May 2020 06:34:59 +0200Thu, 28 May 2020 06:34:59 +0200Wireguard, le VPN sauce KISShttps://hashtagueule.fr/posts/wireguard-le-vpn-sauce-kissFri, 15 May 2020 00:00:00 +0200raspbeguytutorielvpnkisssécuritéréseauhttps://hashtagueule.fr/posts/wireguard-le-vpn-sauce-kissVous vous souvenez la dernière fois que je vous ai parlé de Wireguard ? Moi non plus, je n'en ai jamais parlé, et c'est un scandale. Ça fait pourtant presque un an que je l'utilise à titre personnel et c'est clairement un de mes outils système préféré toutes catégories confondues.

Il y a eu beaucoup d'articles sur Wireguard, beaucoup de tests, beaucoup de comparatifs. Il faut dire que ce n'est pas passé inaperçu, à tel point que même le grand Linus en a dit du bien.

Mais vous savez bien que je ne suis pas du genre à vanter les prouesses d'un outil pour la seule raison qu'il est à la mode. Sinon je ferais du Docker, de nombreux frameworks Javascript, du Big Data et d'autres digitaleries marketeuses.

Sans plus tarder, je vais donc vous faire l'affront de vous présenter un outil qui est déjà bien couvert.

Je suis tombé sur Wireguard il y a quelques années en me baladant sur le site de Jason Donenfeld, l'auteur de pass, un gestionnaire de mot de passe que j'utilisais déjà. De quoi, je vous en ai jamais parlé non plus ? Franchement c'est impardonnable.

Présentation

Wireguard est un VPN. Un VPN, pour rappel, c'est le fait de mettre plusieurs machines plus ou moins éloignées géographiquement sur un même réseau abstrait, et bien entendu de manière sécurisée. Les VPN sont très utilisés de nos jour, surtout depuis le début du confinement et l'explosion du télétravail. Je vous ai déjà parlé d'OpenVPN par exemple ici et ici. Pour moi, avant Wireguard, OpenVPN était le VPN le plus pratique à déployer. Cependant il a plusieurs défauts :

  • La sécurité des transferts est faible voire trouée, notament à cause de l'usage de la bibliothèque OpenSSL.
  • Les performances sont loin d'être optimales.
  • Plusieurs implémentations différentes mênent à des comportement différents selon les plateformes.
  • Le volume de code est extravagant.

D'autres VPN existent et sont un peu plus performants et plus sûrs, comme IPsec. Mais, concernant IPsec, son utilisation est un peu mystique si j'ose dire, et niveau volume de code, c'est encore pire. Mais alors, d'où vient cette malédiction des VPN ? Pas vraiment d'explication si ce n'est que le code est assez vieux, ce qui d'habitude mène à un outil solide et à l'épreuve des bugs, alors qu'ici cela a apporté des couches de codes cancéreuses et la nécrose qui l'accompagne.

Wireguard est donc un nouveau VPN qui est, malgré son aproche moderne et son chiffrement dernier cri, le premier à miser sur la maintenabilité et le minimalisme de son code. Cette stratégie vise à faciliter les audits de sécurité (afin que tout le monde sache que c'est un protocole sûr) et la réduction de la surface d'attaque de personnes malveillantes (moins de code = moins de failles).

Autre point très important, et c'est pour moi le point clef, c'est la simplicité d'utilisation pour les administrateurs systèmes. En gros, une connexion Wireguard pourra être manipulées avec les outil réseau standards disponibles sur UNIX. La simplicité se retrouve aussi dans la configuration. L'établissement d'une liaison Wireguard est pensée pour être aussi simple qu'une connexion SSH. Il s'agit en effet d'un simple échange de clef publiques. Donenfeld dit s'inspirer de la simplicité des outils OpenBSD.

Le protocole étant été prévu comme intégré directement au noyau, il s'est présenté sous la forme d'un module kernel jusqu'à sa fusion directe dans le noyau Linux le 30 mars dernier. Un patch pour le noyau OpenBSD est en cours de peaufinage et fera d'OpenBSD le deuxième système d'exploitation intégrant Wireguard nativement.

Topologie

Contrairement à OpenVPN qui par défaut a une vision client/serveur, c'est à dire que tous les membres du réseau privé gravitent autour d'un unique point d'accès, Wireguard laisse une grande liberté de manœuvre et considère tous les membres du VPN comme des pairs indépendants : chaque nœud possède une liste de pair à qui il va parler directement. Par example, imaginons un réseau privé de trois machines Alice, Bob et Carol. Le plus efficace serait que chacune des trois machines connaisse toutes les autres, c'est à dire qu'elle ait deux pairs correspondant aux deux autres machines :

  • Pairs d'Alice :
    • Bob
    • Carol
  • Pairs de Bob :
    • Alice
    • Carol
  • Pairs de Carol :
    • Alice
    • Carol

L'ennui, c'est que si on veut ajouter une nouvelle machine Dave en respectant cette topologie, il faut récupérer les infos de toutes les autres machines (Alice, Bob et Carol) pour dresser la liste de pairs de Dave, et ensuite on doit ajouter Dave à la liste de pairs d'Alice, Bob et Carol.

De plus, pour établir une liaison entre deux pairs, il faut un point d'accès connu pour au moins un des pairs (une IP et un port UDP). Si on ne connait pas les points d'accès d'Alice et Bob, ou alors si Alice et Bob n'ont pas besoin de se parler entre eux et s'intéressent uniquement à Carol, alors Alice et Bob n'ont besoin que de Carol dans leurs liste de pairs. Carol doit avoir Alice et Bob dans ses pairs. C'est le scénario connu sous le nom de roadwarrior : Alice et Bob sont des pairs mobiles et Carol est une passerelle vers un autre réseau dont les pairs mobiles ont besoin. Dans le cas d'usage du télétravail, Alice et Bob sont des salariés chez eux et Carol est la passerelle du parc informatique de l'entreprise.

  • Pairs d'Alice :
    • Carol
  • Pairs de Bob :
    • Carol
  • Pairs de Carol :
    • Alice
    • Bob

Ainsi, quand on voudra ajouter Dave dans la topologie, il suffira que Dave ait connaissance de Carol dans sa liste de pairs, et il faudra dire à Carol d'ajouter Dave à ses pairs.

Ne vous méprenez pas, dans la topologie roadwarrior, le fait que les pairs mobiles ne soient pas les uns dans les listes de pairs des autres ne signifie pas obligatoirement qu'il ne pourront pas communiquer entre eux. Simplement, ils devront communiquer en passant par la passerelle (qui devra être préparée à cet effet) au lieu de communiquer directement comme dans le cas où tout le monde connait tout le monde.

Paire de clefs

Je vous ai parlé d'échanges de clefs sauce SSH. Pour établir une laison entre deux pairs, il faut que chacun des pairs génère une paire de clef.

wg genkey | tee private.key | wg pubkey > public.key

wg genkey va générer une clef privée et wg pubkey va créer la clef publique correspondante.

Ensuite, les deux pairs doivent s'échanger leur clefs publiques par les moyens qu'ils estiment les plus adéquats (vérification en personne, mail, SMS, télégramme, fax...).

Disons qu'Alice et Bob souhaitent devenir pairs l'un de l'autre. Chacun se crée une paire de clef.

  • Pour Alice :
    • Clef privée : 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
    • Clef publique : gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
  • Pour Bob :
    • Clef privée : yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
    • Clef publique : htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=

Alice et Bob vont ensuite s'échanger leurs clefs publiques.

Notez comme la tête d'une clef privée ressemble à celle d'une clef privée. C'est tentant de confondre les deux. Mais ne le faîtes pas, ce serait mauvais pour votre karma.

Ensuite Alice et Bob vont constituer leurs fichiers de configuration, à placer dans /etc/wireguard/wg0.conf. Le fichier n'est pas obligé de s'appeler wg0.conf, il doit juste se terminer par .conf.

Pour Alice :

[Interface]
PrivateKey = 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
Address = 10.0.0.1/16

[Peer]
PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
AllowedIPs = 10.0.0.2/32

Pour Bob :

[Interface]
PrivateKey = yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
Address = 10.0.0.2/16

[Peer]
PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
AllowedIPs = 10.0.0.1/32

Trois remarques :

  • Seule la clef publique permet de différencier les pairs. Il n'y a pas de champs pour un nom ou un éventuel commentaire.
  • L'IP ou la plage IP définie dans AllowedIPs correspond à toutes les adresses IP cibles des paquets qui seront envoyées à ce pair, et à toutes les adresses IP sources des paquets susceptibles d'être reçus par ce pair. On en reparle plus tard.
  • En l'état, le VPN ne pourra pas marcher : ni Alice ni Bob ne sais où trouver l'autre pair. Il faut qu'au moins un des deux pairs ait un point d'accès, comme nous l'avons expliqué plus haut. S'il est décidé qu'Alice communique son point d'accès, Alice devra ajouter un champ ListenPort à ta rubrique Interface, et Bob ajoutera un champ Endpoint à la déclaration du pair correspondant à Alice.

Pour Alice, sa configuration devient :

[Interface]
PrivateKey = 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
Address = 10.0.0.1/16
ListenPort = 51820

[Peer]
PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
AllowedIPs = 10.0.0.2/16

Pour Bob :

[Interface]
PrivateKey = yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
Address = 10.0.0.2/16

[Peer]
PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
AllowedIPs = 10.0.0.1/32
Endpoint = alice.example.com:51820

Routage des pairs

La signification du champ AllowedIPs est un peu subtile, car elle concerne les deux sens de circulation des paquets. C'est à la fois utilisé pour filtrer les paquets arrivant pour vérifier qu'ils utilisent une IP attendue et pour router les paquets sortants vers ce pair.

On est pas obligé de ne mettre que l'adresse VPN du pair. D'ailleurs, notament dans le scénario roadwarrior, il faut que les machines mobiles configurent le pair correspondant à la passerelle d'accès avec un champ AllowedIPs correspondant au réseau VPN entier, par exemple 10.0.0.0/16.

Reprenons notre scénario roadwarrior avec Alice et Bob en pair mobile et Carol en passerelle d'accès. On définit le réseau VPN 10.0.0.0/16. D'autre part, disons que le réseau interne auquel Carol doit servir de passerelle est en 192.168.0.0/16 et contient une machine Dave.

Carol a donc une paire de clef :

  • Clef privée : 8NnK2WzbsDNVXNK+KOxffeQyxecxUALv3vqnMFASDX0=
  • Clef publique : u8MYP4ObUBmaro5mSFojD6FJFC3ndaJFBgfx3XnvDCM=

La configuration de Carol ressemble alors à ceci :

[Interface]
PrivateKey = 8NnK2WzbsDNVXNK+KOxffeQyxecxUALv3vqnMFASDX0=
Address = 10.0.0.1/16
ListenPort = 51820

[Peer] # Alice
PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
AllowedIPs = 10.0.0.1/32

[Peer] # Bob
PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
AllowedIPs = 10.0.0.2/16

Celle d'Alice et Bob ne contiennent d'un seul pair correspondant à Carol et ressemblant à ceci :

[Peer]
PublicKey = u8MYP4ObUBmaro5mSFojD6FJFC3ndaJFBgfx3XnvDCM=
AllowedIPs = 10.0.0.0/16, 192.168.0.0/16
Endpoint = vpn.example.com:51820

La valeur d'AllowedIPs signifie que des paquets en 10.0.0.0/16 et 192.168.0.0/16 vont arriver en provenance de Carol, et que les paquets vers ces même plages IP seront acheminés vers ce pair. On retrouve alors bien le fait que si Alice désire parler à Bob, elle ne le pourra le faire qu'en passant par Carol. Mais ce genre de besoin est rare en roadwarrior.

Dans un futur article j'aborderai la configuration d'une telle passerelle et les subtilités de routages VPN.

]]>
Repérage dans les dataclasses Pythonhttps://hashtagueule.fr/posts/reperage-dans-les-dataclasses-pythonTue, 12 May 2020 00:00:00 +0200motiuspythondataclassesdéveloppementhttps://hashtagueule.fr/posts/reperage-dans-les-dataclasses-pythonBonjour !

Aujourd'hui un tout petit tutoriel pour parler esthétique et sémantique dans le développement Python.

Comme vous le savez sûrement, Python a introduit dans sa version 3.7 le nouveau module dataclasses qui permet de réduire la verbosité dans la création de classes. Pour rappel, cela permet de transformer quelque chose comme ça :

class Chat:
    def __init__(self,
                 taille: float = None,
                 âge: int = None, # oui oui, c'est légal, déclarez π = math.pi aussi
                 couleur: str = None,
                 vivant: bool = None, # Schrödinger
                ):
        self.taille = taille
        self.âge = âge
        self.couleur = couleur
        self.vivant = vivant

en ça :

@dataclasses.dataclass
class Chat:
    taille: float
    âge: int
    couleur: str
    vivant: bool

avec éventuellement plein de paramètres dans le décorateur dataclass, que je vous encourage à aller lire dans la doc Python. On devine assez facilement ce que font les options :

- init=True
- repr=True
- eq=True
- order=False
- unsafe\_hash=False
- frozen=False

et les conséquences de leurs valeurs par défaut, mais il y a quelques subtilités qui mérite un peu de lecture.

Ce sur quoi je souhaitais mettre l'accent dans cet article, c'est l'existence de la méthode __post_init__ dans le module. En effet, cette méthode est très pratique, et je trouve qu'elle permet facilement de distinguer deux types d'usages de dataclass.

En général, les classes qui n'utilisent pas cet méthode sont plus souvent des vraies dataclass, au sens classes qui contiennent de la données, comparable à une struct en C ou au Records java à venir.

A contrario, celles qui utilisent cette méthode peuvent parfois être des classes qui retiennent un état, par exemple pour de la configuration, mais celles-ci peuvent faire des choses bien plus avancées dans cette méthode qui est appelée automatiquement après la méthode init qui, je le rappelle, est gérée par le module dataclasses.

Par conséquent, si vous rencontrez une dataclass, cela peut être pour deux raisons :

  • utiliser une nouvelle fonctionalité Python déclarative et plus élégante ;
  • créer un objet contenant principalement de la donnée.

Connaître cette distinction sémantique et l'avoir en tête permet une analyse statique du code à la lecture plus rapide, je suis tombé sur une occurrence de ce phénomène et la partage donc.

Joyeux code !

Motius

]]>
Édito #11 : Refonte en profondeurhttps://hashtagueule.fr/posts/edito-11-refonte-en-profondeurMon, 11 May 2020 00:00:00 +0200raspbeguyéditohttps://hashtagueule.fr/posts/edito-11-refonte-en-profondeurAujourd'hui, je mets en production le nouveau site, plus simple, plus beau, plus léger. Ça faisait longtemps que j'avais envie de refaire le site.

Lorsque je l'ai créé au cours de l'été 2015, je ne connaissait pas grand chose en administration système ni en bonnes pratiques pour entretenir un site web. Je suis donc plongé dans le pière de Wordpress. Cette solution est très bien pour avoir un résultat rapide sans toucher au code une seule fois. C'est une solution que je continuerai de recommander à des gens qui n'y connaissent rien, à la condition qu'ils soient accompagnés par des experts Wordpress. Et je ne le suis pas.

J'avais perdu tout contrôle sur le site, qui étais devenu une sorte de tumeur. J'ai décider d'amputer avant la phase terminale.

Vous le savez, au cours de mes tribulations, j'ai affirmé mon goût pour les solutions KISS. Je me suis donc mis à la recherche d'un CMS simple à entretenir, dont je comprends le fonctionnement (et un peu le code), mais aussi dont je puisse me passer avec un minimum d'effort si je décide de trouver encore mieux. J'ai donc opté pour PicoCMS.

PicoCMS, comme son nom l'indique, est un CMS minimaliste. Il a le bon goût de travailler sans base de données : les articles sont stockés sous la forme de simples fichiers Markdown. Il est donc très facile de versionner le contenu du site avec git. D'autre part le langage Markdown est plus ou moins standard et fonctionne avec des tonnes d'autres CMS et générateurs de sites statiques. S'il me prends l'envie de changer du jour au lendemain pour passer à Pelican ou Hugo par example, ce sera très facile.

Pour effectuer la migration de Wordpress à PicoCMS, cela n'a pas été très aisé. La conversion du format base de donnée au Markdown a été assez pénible. Il existe un script assez dégoutant qui fait la plupart du travail. Mais hélas il n'est pas exhaustif et j'ai du repasser sur beaucoup de points manuellement, à coup de find, sed, grep, notamment pour reprendre la liste des tags et les dates. L'intégration des images était aussi à revoir complètement, et les extraits de code, à cause d'une extention Wordpress bizarre (qui me semblait cool à l'époque) a été bâclée.

Une fois les article à peu près propres, je suis tombé sur un thème très intéressant mettant en avant les tags. Et je suis assez satisfait du résultat.

En attendant le prochain changement majeur, je vais danser sur la tombe de Wordpress.

]]>
La virtu pour les nuls : le stockagehttps://hashtagueule.fr/posts/la-virtu-pour-les-nuls-le-stockageMon, 27 Jan 2020 00:00:00 +0100raspbeguyauto-hébergementclusterdrbdkisslibvirtlvmsanstockagetutorielsvirtualisationhttps://hashtagueule.fr/posts/la-virtu-pour-les-nuls-le-stockageMise en garde : cet article va être très long, car j'ai pensé qu'il serait intéressant d'exposer ma démarche complète pour la conception de mon infrastructure, et de présenter aussi non seulement ce que j'ai mis en place, mais également ce que je n'ai pas retenu. En plus on va parler de plein de technos différentes donc vous n'êtes pas obligé de tout lire si vous voulez. D'un autre côté vous êtes toujours libre de faire ce que vous voulez donc je sais pas de quoi je me mêle.

L'introduction reste générale, objective, et pose les contraintes, la première partie expose la solution choisie, et la deuxième partie aborde la mise en œuvre.

Comme tout ingénieur système sain d'esprit, j'aime bien les infrastructures qui ont de la gueule. Par là, j'entends une infra qui soit à l'épreuve de trois fléaux :

  • Le temps : Les pannes étant choses fréquentes dans le métier, les différents éléments de l'infrastructure, tant matériels que stratégiques, doivent pouvoir être facilement et indépendamment ajoutés, remplacés ou améliorés.
  • L'incompétence : Les opérateurs étant des êtres humains plus ou moins aguerris, le fonctionnement de l'infrastructure doit être le plus simple possible et sa maintenance doit réduire au maximum les risques d'erreurs de couche 8.
  • L'utilisation : Personne ne doit être en mesure d'utiliser l'infrastructure d'une façon ou dans un objectif non désiré par ses concepteurs. C'est valable pour certains utilisateurs finaux qui tenteraient d'exploiter votre œuvre dans un but malveillant, aussi bien que d'éventuels changements d'équipe qui ne comprend rien à rien (cf. les dangers de l'incompétence).

Risques de Corona

J'aurais pu aussi préciser que votre infrastructure doit également être à l'épreuve du coronavirus, mais pas grand risque de ce côté là, quoiqu'il ne faille pas confondre avec la bière Corona, qui, si renversée négligemment sur un serveur, peux infliger des dégâts regrettables.

Concrètement, dans le cadre d'un cluster de virtualisation, ça se traduit par quelques maximes de bon sens :

  • On fait en sorte qu'une VM puisse être exécutée sur n'importe quel hyperviseur.
  • N'importe quel opérateur doit être en mesure d'effectuer des opérations de base (et de comprendre ce qu'il fait) sans corrompre le schmilblick, à toute heure du jour ou de la nuit, à n'importe quel niveau de sobriété, et s'il est dans l'incapacité physique, il doit pouvoir les expliquer à quelqu'un qui a un clavier et qui tient debout.
  • La symphonie technologique (terme pompeux de mon cru pour désigner les différentes technos qui collaborent, s'imbriquent et virevoltent avec grâce au grand bal des services) doit s'articuler autour de technos et protocoles indépendants, remplaçables, robustes et s'appuyant les uns sur les autres.

Que de belles résolutions en ce début d'année, c'est touchant. Alors pour retomber sur terre, on va parler du problème épineux de l'unicité des instances de VM.

Et bien oui, le problème se pose, car dans un cluster, on partage un certain nombre de ressources, ça tombe sous le sens. Dans notre cas, il s'agit du système de stockage. Comme on l'a déjà évoqué, chaque nœud du cluster doit pouvoir exécuter n'importe quelle VM. Mais le danger est grand, car si on ne met pas en place un certain nombre de mécanismes de régulation, on risque d'exécuter une VM à plusieurs endroit à la fois, et c'est extrêmement fâcheux. On risque de corrompre d'une part les systèmes de fichiers de la VM concernée, ce qui est déjà pénible, mais en plus, si on effectue des modifications de la structure de stockage partagé à tort et à travers, on risque d'anéantir l’ensemble du stockage du cluster, ce qui impactera l'ensemble du service. C'est bien plus que pénible.

La solution de stockage

La virtualisation

L'outil de gestion de machines invitées que je préfère est libvirt, développé par les excellents ingénieurs de Redhat. À mon sens c'est le seul gestionnaire de virtualisation qui a su proposer des fonctionnalités très pratiques sans se mettre en travers de la volonté de l'utilisateur. Les commandes sont toujours implicites, on sait toujours ce qui est fait, la documentation est bien remplie, les développeurs sont ouverts, et surtout, libvirt est agnostique sur la gestion des ressources, c'est à dire qu'il manipule des concepts simples qui permettent à l'opérateur de le coupler à ses propres solutions avec une grande liberté.

Voici une liste d'autres solutions, qui ne me correspondent pas :

  • Proxmox : Il s'agit d'une de ces solutions qui misent sur le tout-en-un, et donc contraire à l'approche KISS. Cela dit, son approche multi-utilisateurs est très intuitive surtout quand on a pas de compétences poussées en virtualisation, du fait de la présence d'une interface web intuitive. Il s'agit d'une solution satisfaisante lorsqu'on souhaite utiliser un unique nœud mais dès lors qu'on construit un cluster, Proxmox fait intervenir des élément très compliqués qui peuvent vite se retourner contre vous lors de fausses manipulations qui peuvent vous sembler bénignes. De ce fait, sa difficulté d'utilisation et de maintenance augmente de façon spectaculaire, et c'est ce contraste que je lui reproche.
  • oVirt : Il s'agit d'une solution également développée par Redhat, avec interface web conne proxmox et beaucoup (peut-être trop) de fonctionnalités, et qui utilise libvirt pour faire ses opérations. Cependant, comme j'ai du mal à voir ce qui est fait sous le capot, j'ai décidé de ne pas l'utiliser.
  • Virtualbox : Du fait de l'omniprésence et de la disponibilité des tutoriels sur Virtualbox, cela aurait semblé être une bonne solution. Mais il y a plusieurs problèmes : en premier lieu cette solution a été conçue principalement autour de son interface graphique, et son interface en ligne de commande est assez pauvre. Ensuite, par Virtualbox on parle d'une part de l'outil de gestion de VM, mais également du moteur d'exécution lui même. Ce moteur nécessite l'installation d'un module tiers dans le noyau du système, et je préfère éviter cette pratique.
  • VMware : Non là je trolle. #Sapucépalibre

Sous linux, la solution la plus logique pour faire de la virtualisation est QEMU + KVM. Comme je l'ai évoqué, il existe aussi le moteur Virtualbox que j'ai exclu pour les raisons que vous connaissez. On aurait pu aussi s'éloigner de la virtualisation au sens propre et se tourner vers des solutions plus légères, par exemple la mise en conteneurs de type LXC ou Docker.

Les conteneurs sont de petits animaux très gentils avec un petit chapeau sur la tête pour pouvoir se dire bonjour. Et ils sont très, très intelligents.

Les conteneurs sont très utiles lorsqu'il s'agit de lancer des programmes rapidement et d'avoir des ressources élastiques en fonction de l'utilisation. L'avantage des conteneurs est qu'ils sont très réactifs en début et en fin de vie, pour la simple raison qu'ils ne possèdent pas de noyau à lancer et donc ses processus sont directement lancés par l'hôte. En revanche, je compte lancer des machines virtuelles complètes, dotées de fonctions propres, parfaitement isolées de l'hôte, et qui vont durer pendant de longues périodes. Pour des services qui doivent durer, les conteneurs n'apportent rien d'autre que des failles de sécurité. Mais bon, c'est à la mode, vous comprenez...

Bon c'est entendu, partons sur libvirt ne demande pas grand chose pour lier une machine virtuelle à son stockage, ce qui fait qu'il est compatible avec beaucoup de façons de faire. En particulier, si votre stockage se présente sous la forme d'un périphérique en mode block dans votre /dev, libvirt saura en faire bon usage.

Le partitionnement

Au plus haut niveau pour le stockage des VM, je me suis tourné naturellement vers LVM. C'est la solution par excellence quand on cherche à gérer beaucoup de volumes modulables et élastiques, avec de très bonnes performances. Besoin d'un nouveau volume ? Hop, c'est fait en une commande. Le volume est trop petit ? Pas de problème, LVM peut modifier la taille de ses partitions sans avoir à s'inquiéter de la présence d'autres partitions avant ou après, LVM se débrouille comme un chef.

LVM s'articule autour de trois notions :

  • Les LV (logical volumes) : Les partitions haut niveau qui seront utilisées par les VM.
  • Les VG (volumes groups) : un VG est une capacité de stockage sur laquelle on va créer des LV.
  • Les PV (physical volumes) : Les volumes qui sont utilisés pour créer des VG. Un VG peut utiliser plusieurs PV, en addition ou en réplication, de façon similaire au RAID.

Par défaut, un VG est local, c'est à dire conçu pour n'être utilisé que sur une seule machine. Ce n'est pas ce qu'on souhaite. On souhaite un VG commun à tous les nœuds, sur lequel se trouvent des LV qui, on le rappelle, ne sont utilisés que par un seul nœud à la fois. LVM nous permet de créer un tel VG partagé, en ajoutant un système de gestion de verrous.

Là encore, il y a beaucoup de façons de faire. Moi, je cherchais un moyen qui ne m'impose pas de mettre en place des technos qui sont très mises en valeur dans la littérature mais sur lesquelles je me suis cassé les dents, à l'installation comme au dépannage. Typiquement, je refusais catégoriquement de donner dans du Corosync/Pacemaker. J'ai déjà joué, j'ai perdu, merci, au revoir. Un jour peut-être vous vanterai-je dans un article les bienfaits de Corosync lorsque j'aurai compris le sens de la vie et obtenu mon prix Nobel de la paix. Ou bien depuis l'au-delà lors du jugement dernier lorsque les bons admins monterons au ciel et les mauvais descendront dans des caves obscures pour faire du Powershell ou se faire dévorer par les singes mutants qui alimentent en entropie les algorithmes de Google. En attendant, j'y touche pas même avec un bâton.

Une technique qui m'a attiré par sa simplicité et son bon sens fait intervenir la notion de sanlock : plutôt que de mettre en place une énième liaison réseau entre les différents nœuds et de paniquer au moindre cahot, on part du principe que de toute façon, quelle que soit la techno de stockage partagé utilisé, les nœuds ont tous en commun de pouvoir lire et écrire au mème endroit. Donc on va demander à LVM de réserver une infime portion du média pour y écrire quelles ressources sont utilisées, et avec quelles restrictions. Ainsi, les nœuds ne se marchent pas sur les câbles. Moi ça m'allait bien.

Une photo de groupe de l'équipe admin de mon ancien travail. On venait de changer de locaux.

D'ailleurs, à ce niveau de l'histoire, je dois dire que je ne suis pas peu fier. En effet, pour mettre au point cette infrastructure, j'ai été inspiré en grande partie par l'infrastructure utilisée dans une de mes anciennes entreprises, dans laquelle j'avais la fonction de singe savant, et je ne comprenais pas (et ne cherchais pas à comprendre) le fonctionnement de mon outil de travail. Après mon départ, quand j'ai cherché à comprendre, j'ai entretenu une correspondance courriel avec un de mes anciens référents techniques, qui a conçu cette infra et pour qui j'ai toujours eu beaucoup de respect.

Je lui avais demandé des infos sur le sanlocking, ce à quoi il m'a répondu :

Quand j'ai testé le sanlocking ça avait fonctionné, mais je suis pas allé plus loin. Mais ici on gère vraiment le locking à la main.

En effet, les VG de stockages étaient de simples VG locaux. Les métadonnées étaient individuellement verrouillées localement sur chaque nœud, et dès lors qu'on voulait provisionner une nouvelle VM, on devait lancer un script qui déverouillait les métadonnées sur le nœud de travail. Du coup, rien n'empêchait un autre nœud de faire la même chose, au risque d'avoir des modifications concurrentes et la corruption de l'univers. Donc, on était obligé de hurler à la cantonade dans l'open space dès qu'on touchait à quoi que ce soit. Si on avait de l'esprit, on aurait pu parler de screamlock à défaut de sanlock. De la même façon, on risquait à tout moment de lancer des VM à deux endroits à la fois !

En fait on passe petit à petit sur oVirt pour gérer ce cas là. Aujourd'hui sur notre cluster KVM "maison", rien ne nous empêche de démarrer x fois la même machine sur plusieurs hôtes et de corrompre son système de fichiers 😕️ C'est une des raisons de la bascule progressive vers oVirt.

Donc je suis fier de ne pas avoir eu à passer à oVirt pour faire ça. Libvirt est suffisamment simple et ouvert pour nous laisser faire ce qu'on veut et c'est ce que j'aime.

Le partage

Quel PV doit-on choisir pour notre cluster ? Il nous faut un machin partagé, d'une manière ou d'une autre. J'ai envisagé au moins trois solutions :

  • Un LUN iSCSI depuis un SAN (aka. la solution optimale) : Un SAN est un serveur de stockage mettant à disposition des volumes (appelés LUN). Le stockage est donc géré par des appareils dédiés, qui se fichent complètement de la façon dont on utilise ses volumes ni sur combien de nœuds. Le SAN ne sait pas si ses volumes contiennent des systèmes de fichiers, et il ne bronchera pas du tout si vous corrompez leur contenu parce que vous êtes pas fichu de vous organiser. Le soucis avec les SAN, c'est leur coût qui fait souvent dans les cinq chiffres...
  • Une réplication DRBD multimaster (aka. la solution du pauvre) : Si vous êtes comme moi et que les brousoufs ne surpopulent pas votre compte en banque, vous pouvez vous limiter à utiliser le stockage local de vos nœuds, que vous mettez en réplication multimater DRBD. De cette façon, chaque modification du volume sera instantanément répercuté sur l'autre nœud. L'avantage de cette solution est que le stockage est local, donc pour les accès en lecture on ne génère pas de trafic réseau. D'autre part, on a de facto une réplication de l'ensemble des données en cas de panne d'un nœud. Le soucis avec DRBD, c'est qu'il ne supporte, pour l'instant, que deux nœuds. D'un autre côté, comme on est pauvre, on a difficilement plus de deux nœuds de toute façon, mais cela empêche une mise à l'échelle horizontale dans l'immédiat. On peut se consoler en planifiant qu'on pourra passer sans trop d'efforts à la solution SAN quand les finances seront propices.
  • D'autres protocoles plus lourds comme Ceph qui vous promettent la lune et des super fonctions haut niveau (aka. la mauvaise solution) : Attention danger, on rentre dans l'effroyable empire des machines à gaz : on sait pas trop ce que ça fait, ça bouffe des ressources sans qu'on sache pourquoi, et quand ça foire, si on a pas passé sa vie à en bouffer, on peut rien faire. Déjà, Ceph est une solution mise en avant par Proxmox, donc il y a déjà matière à se méfier. En plus, les fameuses fonctions haut niveau, comme le partitionnement, le modèle qu'on a choisit nous pousse à le gérer avec autre chose, ici LVM. Donc merci, mais non merci.

Notez que peu importe la solution que vous choisirez, comme il s'agit de stockage en réseau, il est fortement recommandé d'établir un réseau dédié au stockage, de préférence en 10 Gbps. Et malheureusement, les cartes réseau et les switchs 10 Gbps, c'est pas encore donné.

Schéma récapitulatif

La mise en œuvre

Lors de la phase de conception, on est parti du besoin haut niveau pour descendre au contraintes matérielles. Pour la mise en œuvre, on va cheminer dans l'autre sens.

DRBD

Comme je vous l'ai avoué, d'un point de vue budget infrastructure, j'appartiens à la catégorie des pauvres. J'ai donc du me rabattre sur la solution DRBD.

Sur chacun de mes deux nœuds, je dispose de volumes de capacité identique, répondant au nom de /dev/sdb.

Après avoir installé DRBD, il faut créer sur chacun des nœuds la ressource associés à la réplication, qu'on va nommer r0.

# /etc/drbd.d/r0.res

resource r0 {
    meta-disk internal;
    device /dev/drbd0;
    disk /dev/sdb;

    syncer { rate 1000M; }
        net {
                allow-two-primaries;
                after-sb-0pri discard-zero-changes;
                after-sb-1pri discard-secondary;
                after-sb-2pri disconnect;
        }
    startup { become-primary-on both; }

    on node1.mondomaine.net { address 10.0.128.1:7789; }
    on node2.mondomaine.net { address 10.0.128.2:7789; }
}

Quelques explications :

  • /dev/sdb est le périphérique de stockage qui va supporter DRBD.
  • /dev/drbd0 sera le volume construit par DRBD
  • Les noms des nœuds doivent correspondre au nom complet de l'hôte (défini dans /etc/hostname) et ne pointe pas obligatoirement vers l'adresse IP précisée, qui est souvent dans un réseau privé dédié au stockage.

Une fois que c'est fait, on peut passer à la création de la ressource sur les deux machines :

drbdadm create-md r0
systemctl enable --now drbd

DRBD va ensuite initier une synchronisation initiale qui va prendre pas mal de temps. L'avancement est consultable en affichant le fichier /proc/drbd. À l'issue de cette synchronisation, vous constaterez que l'un des nœuds est secondaires. On arrange ça par la commande drbdadm primary r0 sur les deux hôtes (normalement uniquement sur l'hôte secondaire mais ça mange pas de pain de le faire sur les deux).

LVM

On a besoin d'installer le paquet de base de LVM ainsi que les outils pour gérer les verrous.

apt install lvm2 lvm2-lockd sanlock

La page du manuel de lvmlockd est extrêmement bien ficelée mais je sens que je vais quand même devoir détailler sinon ce serait céder à la paresse. On va donc reprendre les étapes de la même façon que la documentation.

On prends conscience que c'est bien le type sanlock qu'on veut utiliser. L'autre (dlm) est presque aussi nocive que Corosync.

Dans le fichier /etc/lvm.conf on modifie :

use_lvmlockd = 1
locking_type = 1

Sur la version de LVM de la Debian actuelle (Buster), le paramètre locking_type est obsolète et n'a pas besoin d'être précisé.

Ne pas oublier de donner un numéro différent à chaque hôte dans le fichier /etc/lvm/lvmlocal.conf dans la rubrique local dans le paramètre host_id.

On active sur chaque hôte les services qui vont bien :

systemctl enable --now wdmd sanlock lvmlockd

On crée le VG partagé virtVG (sur un seul nœud) :

vgcreate --shared virtVG /dev/drbd0

Puis enfin on active le VG sur tous les hôtes :

vgchange --lock-start

Vous pouvez dès à présent créer des LV. Pour vérifier que votre VG est bien de type partagé, vous pouvez taper la commande vgs -o+locktype,lockargs. Normalement vous aurez un résultat de ce genre :

  VG     #PV #LV #SN Attr   VSize   VFree   LockType VLockArgs    
  virtVG   1   2   0 wz--ns 521,91g 505,66g sanlock  1.0.0:lvmlock

Dans la colonne Attr, le s final signifie shared, et vous pouvez voir dans la colonne LockType qu'il s'agit bien d'un sanlock.

Pour créer un LV de 8 Go qui s'appelle tintin :

node1:~# lvcreate -L 8g virtVG -n tintin
  Logical volume "tintin" created.
node1:~# lvs
  LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  prout  virtVG -wi------- 8,00g                                                    
  test   virtVG -wi------- 8,00g                                                    
  tintin virtVG -wi-a----- 8,00g

Le a de la commande Attr indique que le volume est activé. C'est sur cette notion d'activation qu'on va pouvoir exploiter le verrou. Pour en faire la démonstration, rendez-vous sur un autre hôte (on n'a qu'un seul autre hôte comme on est pauvre).

node2:~# lvs
  LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  prout  virtVG -wi------- 8,00g                                                    
  test   virtVG -wi------- 8,00g                                                    
  tintin virtVG -wi------- 8,00g

Notez que tintin n'est pas activé ici, et on a d'ailleurs tout fait pour que l'activation n'ait lieu que sur un seul nœud à la fois. Voyons ce qui se passe si on essaye de l'activer :

node2:~# lvchange -ay virtVG/tintin
  LV locked by other host: virtVG/tintin
  Failed to lock logical volume virtVG/tintin.

Cessez les applaudissements, ça m'embarrasse. Donc, pour l'utiliser sur le deuxième nœud il faut et il suffit de le désactiver sur le premier.

node1:~# lvchange -an virtVG/tintin
node1:~# lvs
  LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  prout  virtVG -wi------- 8,00g                                                    
  test   virtVG -wi------- 8,00g                                                    
  tintin virtVG -wi------- 8,00g

Et sur le deuxième on retente :

node2:~# lvchange -ay virtVG/tintin
node2:~# lvs
  LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  prout  virtVG -wi------- 8,00g                                                    
  test   virtVG -wi------- 8,00g                                                    
  tintin virtVG -wi-a----- 8,00g

Magie. Quand un volume est activé, libre à vous d'en faire un système de fichier, de le monter, de faire un dd dessus... Tous ce dont vous aurez besoin c'est du chemin du périphérique /dev/virtVG/tintin. Le plus probable dans le cadre de la virtualisation est qu'il va y être déployé une table de partition MBR ou GPT, auquel cas il faudra, si un jour vous souhaitez explorer ce LV autrement que depuis la VM, lire sa table de partition avec kpartx.

Virtualisation

On arrive presque à l'issue de la chaîne des technos. En fait, dès maintenant, libvirt est capable de créer des VM. Mais par contre, en l'état, vous devrez vous charger manuellement de l'activation des LV. C'est un peu fastidieux, surtout dans un milieu industriel.

D'un point de vue conceptuel, deux possibilités s'offrent à nous :

  • On définit toutes les VM sur tous les hôtes (et bien sûr on ne démarre des VM que sur un seul hôte à la fois).
  • On définit sur un hôte uniquement les VM que cet hôte a le droit d'exécuter pour l'instant

J'ai choisi la deuxième option pour des raisons de clarté, mais je n'exclue pas de passer à la première option dans le futur pour une raison ou une autre.

Il faut ensuite adopter une stratégie d'activation des LV. On peut en choisir entre ces deux là :

  • Un hôte active les LV de toutes les VM qui sont définies sur lui.
  • Un hôte active uniquement les LV des VM en cours d'exécution sur lui.

J'ai choisi la deuxième stratégie, car la première est incompatible avec l'option de définir toutes les VM sur tous les hôtes.

Selon cette deuxième stratégie, il y a trois opérations de base sur les VM qui nécessitent d'intervenir sur l'activation des LV :

  • La mise en service
  • L'arrêt
  • La migration (le transfert d'une VM d'un hôte à un autre)

Pour la mise en service, il faut activer le volume (et donc poser un verrou) avant que la machine ait commencé à s'en servir. Pour l'arrêt, il faut désactiver le volume (et donc libérer le verrou) après que la machine ait fini de s'en servir. Pour la migration... c'est plus compliqué.

Pour la migration à froid, c'est à dire pendant que la machine est à l'arrêt, c'est facile : il faut activer le volume sur la machine cible avant la migration et le désactiver après. Pour la migration à chaud, c'est à dire pendant que la VM est en marche, je vous propose d'étudier la suite des états de la VM et du LV, avec en gras les événements et en italique les périodes :

  1. Avant l'instruction de migration : la VM est en marche sur l'hôte source, donc le LV associé doit être activé sur l'hôte source.
  2. Instruction de migration
  3. Entre l'instruction de migration et la migration effective : la VM est en marche sur l'hôte source, donc le LV associé doit être activé sur l'hôte source ; l'hôte cible vérifie qu'il peut accéder au LV, donc le LV doit être activé sur l'hôte cible.
  4. Migration effective
  5. Après la migration effective : la VM est en marche sur l'hôte cible, donc le LV associé doit être activé sur l'hôte cible.

Vous le voyez bien : à l'étape 3, il est nécessaire que le LV de la VM soit activé à la fois sur la source et sur la cible. Et là, ce pour quoi on s'est battu depuis le début de cet article vole en éclat, et notre beau château de carte est désintégré par une bourrasque de vent.

Mais non, il reste de l'espoir, car je ne vous ai pas encore tout dit.

En fait, par défaut, les verrous des LV sont exclusifs, mais il est aussi possible de poser un verrou partagé :

lvchange -asy virtVG/tintin

Lorsqu'un LV est activé avec un verrou partagé, n'importe quel autre hôte a la possibilité d'activer le même LV avec un verrou partagé. Il faudra attendre que tous les hôtes aient désactivé le LV (et donc libéré leurs verrous partagés) pour que le LV puisse à nouveau être activé avec un verrou exclusif.

Bien entendu, si on laisse un verrou partagé sur plusieurs hôtes pendant une longue période, il est probable qu'un opérateur négligeant finisse par commettre l'irréparable en lançant la VM en question alors qu'elle est déjà en marche ailleurs...

C'est pourquoi il faut absolument que les périodes d'usage des verrous partagés soient réduites au nécessaire, c'est à dire pendant la migration.

Heureusement, libvirt vous donne la possibilité de mettre en place des crochets, c'est à dire des scripts qui vont être automatiquement appelés par libvirt au moment où il lance ces opérations. L'emplacement du crochet dont on a besoin est /etc/libvirt/hooks/qemu.

Nouveau problème : en l'état actuel, en ce qui concerne la migration, libvirt invoque le crochet uniquement sur l'hôte cible, et uniquement avant la migration effective. Nous avons besoin qu'il invoque le crochet à trois autres reprises :

  • Avant la migration effective, sur l'hôte source, pour permettre de muter le verrou exclusif en verrou partagé
  • Après la migration effective, sur l'hôte source et l'hôte, pour permettre de libérer les deux verroux partagés, dans le cas d'une migration à froid

Dans le cas d'une migration à chaud, libvirt notifie de toute façon, à la source, l'arrêt de la VM et donc la libération du verrou quel qu'il soit, et sur la cible, le démarrage de la VM et donc la pose d'un verrou exclusif qui écraserait un éventuel verrou partagé.

J'ai remonté le problème aux ingénieurs de Redhat en charge de libvirt, ils sont en train de développer la solution (suivez le fil sur la mailing list). Je publierai mon script dès que ce problème sera réglé.

À propos de la configuration de libvirt, j'ai un dernier conseil qui me vient à l'esprit. Il ne sert pas à grand chose de configurer le VG en temps que pool de volume, au sens de libvirt. En vérité, on peut très bien démarrer une VM même si son volume est en dehors de tout pool défini dans libvirt. En plus, si le pool est en démarrage automatique, libvirt va activer tous les LV quand il va démarrer, ce qui va provoquer une erreur dans le cas d'un redémarrage d'un hôte dans un cluster en service.

Le seul avantage de définir un pool auquel je peux penser est que libvirt peut créer des LV sans commande LVM auxiliaire au moment de la création d'une VM. La belle affaire. Personnellement je provisionne déjà mes VM avec un outil auxiliaire qui s'appelle virt-builder, faisant partie de la très utile suite utilitaire libguestfs. Je vous la recommande, elle fera peut être l'objet d'un futur article.

J'ai déjà beaucoup trop parlé et ça m'a saoulé, alors maintenant je vais la fermer et dormir un peu.

]]>
La virtu pour les nuls : faire un bridgehttps://hashtagueule.fr/posts/la-virtu-pour-les-nuls-faire-un-bridgeSun, 11 Aug 2019 00:00:00 +0200raspbeguynon classéhttps://hashtagueule.fr/posts/la-virtu-pour-les-nuls-faire-un-bridgeJe vous avais déjà fait part de ma fascination pour la virtualisation. Et c'est vrai. J'aime la virtualisation. Bon Dieu ce que je l'aime. Aaaaaah. Diantre j'adore ça. Ça me donne envie de sortir dans la rue et de faire des trucs stupides tellement je kiffe ça.

Bref.

Plus rationnellement et plus décemment, la virtualisation (ou l'isolation de contexte, alias les conteneurs, mais j'ai plus de mal avec ça) est le moyen le plus efficace pour avoir des services propres, maintenables et sûrs. C'est une méthode de travail excellente et très intéressante.

La virtualisation nécessite que la machine hôte mette à disposition des machines invitées un certain nombre de ressources : processeurs, mémoire, interfaces... Notamment, nos machines virtuelles ont la plupart du temps besoin de réseau.

Il existe au moins deux façons de faire du réseau avec des machines virtuelles, une bonne et une mauvaise une facile et une pratique :

  • La méthode facile, c'est de mettre toutes les VMs d'un hyperviseur dans un réseau NATé, dont l'hyperviseur est la passerelle.
  • La méthode pratique, c'est de passer un pont (bridge) liant l'interface physique de l'hôte avec les interface virtuelles des invités.

Maintenant un brin d'explication.

  • La méthode NAT est facile car elle est déployée par défaut par la plupart des hyperviseurs. Elle ne demande aucune modification du réseau de l'hyperviseur. Toutes les requêtes des invités semblent donc émaner de l'hôte, de la même manière que dans votre réseau local dans votre maison, toutes vos machines passent leurs requêtes à votre box-routeur. Cela pose un problème de taille lorsque les invités ont besoin d'être accessible de l'extérieur, ce qui nécessite de gérer des règles d'ouvertures de ports (et donc deux machines ne peuvent pas écouter sur le même port, vu de l'extérieur). D'autre part, il arrive souvent que l'hôte soit lui même derrière un NAT (par exemple si vous ne disposez que d'une seule IP publique pour votre foyer comme c'est le cas dans 99% des connexions internet des particuliers). Placer donc un deuxième NAT géré par votre hôte apporte donc un niveau de complexité à votre infrastructure réseau.
  • La méthode Bridge est pratique car, d'un point de vue réseau, vos invitées sont sur le même niveau que votre hôte. Outre le fait que vous avez autre chose à faire de votre vie que de gérer un double NAT et les redirections qui s'ensuivent, ça rend les invités en quelque sortes indépendants de leurs hôtes. Par exemple, imaginons que vous avez deux hôtes H1 et H2 qui font tourner un invité chacun, G1 sur H1 et G2 sur H2. Un client tiers n'a pas besoin de savoir sur quel hôte se situe G1 et G2 pour les contacter car ils ont tous les deux des IPs indépendantes. D'autre part, si vous devez passer H1 en maintenance, il vous suffit de migrer G1 sur H2 sans autre forme de procès, ce sera complètement transparent pour votre infrastructure (pour peu que votre routage de flux soit flexible).eth0

Disons donc que pour faire tourner une distribution desktop ponctuellement pour tester un programme, vous n'avez probablement pas besoin de cet article et vous pouvez très bien vivre avec la solution NAT. Mais pour un parc de machine en production, il est presque toujours indispensable de passer par la solution Bridge.

Mais au fait dis moi Jamy, c'est quoi un bridge ? Et bien Fred, un bridge ça veut dire pont en anglais, et comme son nom l'indique, ça va permettre de lier des interfaces réseau. Ici, le but est de lier les interfaces virtuelles des invités à l'interface physique de la machine. Cela implique que l'interface physique sera partagée par plusieurs machines.

Mettons nous dans la tête d'un hôte dont on a pas encore configuré le bridge. Disons que son interface réseau physique s'appelle eth0. Donc, par défaut et de manière naturelle, cette interface va porter son IP et il ne va pas se poser plus de question que ça. Nous, on veut changer ça. On va créer un bridge, appelons le br0, qui va être le maître de eth0, que sera donc son esclave (ne me regardez pas comme ça, ce sont là les termes officiels, je vous assure). On configurera notre plateforme de virtualisation préférée (libvirt en ce qui me concerne, proxmox pour d'autres, vmware pour les pêcheurs) pour lui dire d'utiliser br0 comme périphérique réseau. Alors, lorsqu'on lancera un invité, son interface virtuelle sera esclave de br0. Quant à l'hôte, l'interface qui utilisera à présent pour se connecter au réseau sera br0.

Si votre hôte est sous CentOS, on peut configurer un nouveau bridge, ici en adressage statique sur mon réseau privé derrière mon routeur, en créant le fichier /etc/sysconfig/network-scripts/ifcfg-br0 contenant ceci :

DEVICE=br0
TYPE=Bridge
BOOTPROTO=static
DNS1=8.8.8.8
DNS2=8.8.4.4
GATEWAY=192.168.1.1
IPADDR=192.168.1.10
NETMASK=255.255.255.0
ONBOOT=yes

Pour un hôte sous Debian, on aura ce bloc dans /etc/network/interfaces :

iface br0 inet static
        bridge_ports eth0
        address 192.168.1.10
        broadcast 192.168.1.255
        netmask 255.255.255.0
        gateway 192.168.1.1

Il ne faut alors pas oublier de changer la configuration de eth0 pour qu'aucune IP ne lui soit associé. Cette interface n'aura d'autre rôle que de transmettre et recevoir les paquets bruts à notre bridge, qui se chargera de dispatcher les paquets à ses esclaves où à l'hôte.

C'est tout pour aujourd'hui. Allez chauffe Marcel, envoie le générique de fin.

]]>
Le package Jitsi-Meet : la fête du sliphttps://hashtagueule.fr/posts/le-package-jitsi-meet-la-fete-du-slipThu, 23 May 2019 00:00:00 +0200raspbeguyauto-hébergementjitsytutorielsvideoxmpphttps://hashtagueule.fr/posts/le-package-jitsi-meet-la-fete-du-slipAujourd'hui j'ai du installer un serveur Jitsi Meet pour un client. Je ne l'aurais pas prévu mais ça a été la grosse galère pour des raisons tout à fait singulières.

Jistti Meet, pour ceux qui ne le connaissent pas, c'est une solution libre pour faire des discussion vidéo qui s'appuie entre autres sur les technologies XMPP et WebRTC. En gros ça a l'avantage de s'appuyer sur une techno de communication existante et ça ne demande pas d'installation de programmes autres qu'un navigateur un minimum récent. C'est compatible Firefox, Chrome, Edge, pour ne citer qu'eux.

Pour faciliter l'installation, Jitsi met à disposition des dépôts APT permettant d'installer sans galérer cet outil clef en main. En théorie seulement car en pratique il faut quelques étapes préliminaires afin que tous les rouages s'emboîtent correctement.

J'ai donc déployé une nouvelle VM sous Debian Stretch toute nue et j'ai installé les packets.

echo 'deb https://download.jitsi.org stable/' >> /etc/apt/sources.list.d/jitsi-stable.list
wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
apt update
apt install jitsi-meet

L'installeur pose ensuite quelques questions :

  • La première porte sur le nom du service (comprendre ici le FQDN).
  • La seconde vous demande si vous souhaitez utiliser votre propre certificat ou bien générer un certificat auto-signé. Moi je suis parti sur mon propre certificat, ce qui active les deux questions suivantes.
  • Le chemin du fichier de clef.
  • Le chemin du fichier de certificat.

Je me dis : cool, cela a l'air de s'être bien passé. Mais je me trompais.

J'ai refais l'installation plusieurs fois en variant plusieurs paramètres : certificat, chemin des fichiers, installation du reverse proxy avant... Dans certains cas j'avais droit à un handshake TLS foireux, dans d'autres cas des comportements bizarre comme l'impossibilité de rejoindre une room, et dans le reste des cas des erreur APT/DPKG.

Il se trouve que l'unique suite d'opération qui ait fonctionné pour moi est la suivante :

  • Installer nginx (ou apache si vous aimez utiliser un tank pour casser un œuf). De cette façon, le paquet Jitsi va détecter la présence d'un proxy et va tout seul ajouter son vhost.
  • Obtenir vos certificats (letsencrypt si vous le souhaitez) et faire en sorte que nginx écoute sur les ports 80 et 443) avant l'installation de Jistsi. Si ce n'est pas le cas, le processus java de Jitsi va s'approprier les ports 80 et 443 ce qui va créer une erreur au moment de recharger nginx avec la nouvelle configuration. C'est délicat comme position...
  • Finalement, installer Jitsy.

J'ai écrit cet article pour économiser un peu de temps au prochains admin qui devront installer ça. Par ailleurs, il est possible que lorsque vous lancerez votre première room, un bug vous frappe: à chaque fois que quelqu'un d'autre rentre dans la room, l'occupant actuel est éjecté... Selon la page du bug sur Github, un reboot de la machine suffit et le bug ne se reproduit apparemment plus. Pour moi ce n'est pas satisfaisant d'appliquer des méthodes à la Windows, mais j'avoue que je n'ai pas eu le courage de chercher plus longtemps.

]]>
Édito #10 : J'ai vu de la lumièrehttps://hashtagueule.fr/posts/edito-10-j-ai-vu-de-la-lumiereFri, 03 May 2019 00:00:00 +0200raspbeguyéditohttps://hashtagueule.fr/posts/edito-10-j-ai-vu-de-la-lumiereVous avez déjà été peut-être dans cette situation, celle où vous avez quelque chose à faire depuis si longtemps, mais à chaque fois que vous y pensez, vous n'avez aucune idée de par où commencer et au bout d'un moment, vous commencez à vous dire que vous pouvez vivre sans, et que remuer le truc ne ferait que vous apporter des ennuis et vous espérez secrètement que tout le monde a oublié le fameux truc que vous avez à faire, même si vous vous dites aussi que c'est un truc qu'il faut faire et que ça vous rendrait bien service quand même.

Je suis revenu sur ce vieux WordPress tout pété que j'ai laissé pourrir pendant presque 2 ans, et je me suis dit que j'en avais assez de ce marécage nauséabond dans lequel je m'étais embourbé. J'ai donc sorti les doigts de mon anus et j'ai remis ça sur pieds.

Je plaide coupable, mais qui irait me jeter la pierre ? Parfois on a besoin de faire d'autres trucs ou de mettre en pause certains d'entre eux, surtout quand le succès n'est pas au rendez-vous. Je ne suis d'ailleurs pas seul, mon compagnon de plume Motius n'ayant plus non plus autant de temps à consacrer à ses tribunes que naguère.

D'ailleurs, que suis-je devenu entre temps ? Il m'est arrivé beaucoup de choses, sans mentir. Rassurez-vous, principalement de bonnes choses, de merveilleuses choses même. Rendez-vous compte, j'ai changé de travail, j'ai déménagé et je me suis marié. Oui, marié. C'est plutôt positif ça comme bilan, non ? Vous comprendrez donc que j'ai pas mal d'autres choses à faire en plus que de m'occuper d'administrer des services web ou d'autres machins. Déjà que le simple fait de faire plus de 8 heures de travail sur systèmes informatiques correspond à une dose de technique presque trop importante pour moi pour ressentir le besoin d'allumer un PC à la maison, mais maintenant que je mène une vie heureuse et épanouie, ça coupe un peu l'envie de bouffer du code et de la ligne de shell.

Cependant ne craignez pas, j'ai décidé de ne pas arrêter. Je vais me refaire une station de travail à la maison pour m'occuper du blog, de mon infra, et du reste. Pourquoi ? Et bien, c'est important de finir ce qu'on commence, en tout cas de le finir autrement que laisser un cadavre de site qui pue. Et d'autre part, et surtout, je ne dois pas oublier ce qui m'a poussé à créer ce blog, à savoir que ça me plaît d'écrire. Je n'écris plus du tout assez ces derniers temps.

J'ai envie de continuer à expliquer des choses au fur et à mesure que je les apprends. J'ai envie de piquer des crises contres telle ou telle techno, tel ou tel événement. J'ai envie de vous expliquer ma vision de l'informatique, du libre et du futur. J'ai envie de collaborer avec Motius, mais également avec toute âme qui serait prête à accorder un peu de son temps pour ce site. J'ai envie de discuter avec vous sur IRC, comme au bon vieux temps, de m'énerver contre Motius parce qu'il parle trop, mais qui a tant de bonnes idées et d’initiatives, ce qui en fait un collaborateur de qualité.

Tout cela m'a manqué. Vous m'avez manqué. J'espère que ça pourra reprendre. Je vais donc lutter férocement contre mon envie de tout lâcher et je vais dédier un peu de mon temps à ce blog.

D'ailleurs, l'IRC est dans les choux en ce moment. Les amis qui hébergeaient le serveur ont disparu du jour au lendemain sans donner de nouvelle. Il faut croire qu'il ont soudain eu de nouveaux chats à fouetter eux aussi. Je vais donc prochainement créer un nouveau canal sur un réseau stable, je vous tiendrai au courant.

Merci de continuer à me lire, portez vous bien, à très bientôt. Merci d'être restés gentils.

]]>
Let's Encrypt sur HAProxy (Partie 2)https://hashtagueule.fr/posts/let-s-encrypt-sur-haproxy-partie-2Sat, 11 Nov 2017 00:00:00 +0100raspbeguyarchitectureauto-hébergementplanet libresécuritésslsysadmintutorielswebhttps://hashtagueule.fr/posts/let-s-encrypt-sur-haproxy-partie-2Le fonctionnement de la Terre, avant qu'un certain Galilée y mette son grain de sel, a toujours déchaîné les passions des Hommes et a consommé beaucoup d'encre. Parmi les théories les plus folles, Terry Pratchett nous enseigne dans sa série de livres du Disque Monde que le monde repose sur le dos de quatre gigantesques éléphants, eux-même reposant sur la carapace d'une tortue encore plus gigantesque appelée A'Tuin. Les quatre éléphants (Bérilia, Tubul, Ti-Phon l'Immense et Jérakine) se répartissent la charge que représente le poids du disque terrestre grâce à la rotation quotidienne de ce dernier. Si maintenant vous ne voyez pas le rapport entre une cosmologie impliquant des animaux au moins aussi grands que des continents capable de tenir en apnée pendant des milliards d'années et le sujet d'aujourd'hui, alors je ne sais plus quoi faire.

Nous avons vu il y a quelques jours les principes de base d'un répartiteur de charge et exposé brièvement les concepts fondamentaux de HAProxy. Nous avons également mené une réflexion sur la manière de gérer un certificat sur une architecture avec répartiteur de charge et la voix du bon sens nous a susurré que le certificat devait être porté par HAProxy. Bon, mettons ça en pratique.

Rangez vos affaires, sortez une feuille, interro surprise. Rappelez-moi ce qu'est le principe de base du protocole ACME. Et comme vous suivez bien et que vous buvez les paroles de l'instituteur comme un breton boit du cidre, vous saurez donc me répondre que l'agent de certification crée un challenge ACME en générant des fichiers de test auxquels le serveur ACME devra pouvoir accéder via HTTP, ces fichiers étant soit servis par l'agent via un serveur web intégré, soit mis à disposition du serveur de notre choix qui servira ces fichiers en statique. C'est en pratique presque toujours la deuxième solution qui est utilisée, sachant que justement, un serveur web tourne déjà sur les ports web standard (80 et 443). On ne veut pas d'interruption de service, non mais.

Serveur statique

Nous avons un petit problème ici. Les ports HTTP sont utilisés par HAProxy, qui n'est pas un serveur web, mais un répartiteur de charge. Souvenez-vous, il ne peut pas générer de lui même un flux web, pas même servir des fichiers statiques, il se contente de relayer des flux web créés par d'autres serveurs. Conclusion : il nous faut un serveur web tournant sur la machine hébergeant HAProxy capable de servir des fichiers statiques. L'idée, c'est qu'on va faire écouter ce serveur uniquement en local, et HAProxy se chargera de le propulser sur le réseau publique.

On n'a que l'embarras du choix pour trouver un serveur web sachant servir du bon vieux statique. On pourrait choisir Nginx ou Lighttpd par exemple, mais comme nous sommes sur OpenBSD, le choix se portera sur le serveur web déjà préinstallé, communément appelé openhttpd.

Première étape, il faut dire à OpenBSD d'activer le service.

echo 'httpd_flags=""' >> /etc/rc.conf.local

Ainsi on pourra démarrer le service, et il sera démarré à chaque démarrage de l'OS.

Ensuite, il va nous falloir créer un dossier qui va accueillir les fichiers du challenge ACME. Comme le processus httpd est chrooté dans /var/www (sur OpenBSD, on appelle ça un jail), ce dossier sera donc /var/www/htdocs/webroot.

La configuration de notre httpd sera on ne peut plus simple :

server "default" { 
    listen on 127.0.0.1 port 1375 #ça fait "let's" en l33t
    root "/htdocs/webroot"
}

Démarrons le service :

/etc/rc.d/httpd start

Notre serveur statique est prêt.

Configuration HAProxy

Il faut dire à HAProxy de rediriger les requêtes ACME vers notre serveur statique fraîchement configuré.

Pour ça, on va définir un backend dédié à Let's Encrypt, vers lequel seront redirigés tous les flux ACME reçus sur les frontends qui nous intéressent.

backend letsencrypt-backend
        server letsencrypt 127.0.0.1:1375

Propre. Syntaxe explicite, pas besoin d'expliquer je pense. Je fais juste remarquer qu'en plus de nommer le backend globalement, je donne un nom au serveur du backend même si ici il n'a aucune forme d'importance, il ne sera jamais appelé ailleurs dans le fichier, c'est néanmoins obligatoire sur HAProxy. Enfin, un nom clair est toujours profitable, car il est éventuellement utilisé dans ses logs, ses statistiques et il rend la lecture du fichier de conf plus facile.

Le backend est prêt, c'est excellent, mais encore faut-il l'utiliser. Pour être exact, on ne va l'utiliser que si l'URL demandée correspond au chemin classique demandé par le validateur ACME, à savoir /.well-known/acme-challenge/. Reprenons l'exemple de la dernière fois, modifions-le de la sorte :

frontend mon_super_site
    bind *:80
    mode http

    acl url-back-office path_dir /admin
    acl letsencrypt-acl path_beg /.well-known/acme-challenge/

    use_backend back-office if url-back-office
    use_backend letsencrypt-backend if letsencrypt-acl
    default_backend mes-frontaux

backend back-office
    server backoffice 192.168.0.14:80

backend mes-frontaux
    balance roundrobin
    server backoffice1 192.168.0.11:80
    server backoffice2 192.168.0.12:80
    server backoffice3 192.168.0.13:80

backend letsencrypt-backend
    server letsencrypt 127.0.0.1:1375

On a ajouté une ACL et une directive use_backend. Rappelez-vous, une ACL est tout simplement une condition, ici elle porte sur le chemin de l'URL. Rien de nouveau, on a déjà expliqué le concept.

On recharge le schmilblick :

/etc/rc.d/haproxy reload

On est prêt à générer notre premier certificat. Mais on ne va pas le faire tout de suite. Comme vous vous en souvenez, HAProxy veut ses certificats sous une forme un peu spécifique, il nous faut donc traiter le certificat dès qu'il a été généré, et pour ça on va faire un peu de scripting. On pourrait déjà générer le certificat sans le script de post-traitement, mais si on faisait ça, on devrait ensuite éditer la configuration d'acme.sh et c'est un peu barbant, sachant que la commande qui génère le premier certificat génère également par magie la configuration associée pour les renouvellements. Donc ce serait stupide.

Script de post traitement

Rien de bien compliqué, je vous rassure. Le principe est simple : HAProxy n’accepte des certificats que sous la forme d'un fichier contenant la chaîne complète de certification et la clef privée, dans cet ordre.

Le script est très simple. Vous pouvez bien entendu le mettre où bon bous semble, pour ma part j'ai décidé de le mettre dans /etc/haproxy/generate-ssl.sh.

#!/bin/sh

SITE=$1
LE_CERT_LOCATION=/root/.acme.sh/$SITE
HA_CERT_LOCATION=/etc/haproxy/ssl

cat $LE_CERT_LOCATION/fullchain.cer $LE_CERT_LOCATION/$SITE.key > $HA_CERT_LOCATION/$SITE.haproxy.pem

/etc/rc.d/haproxy reload

LE_CERT_LOCATION représente l'endroit où seront générés les certificats par acme.sh (il s'agit ici de l'emplacement par défaut, mais vous pouvez changer ce chemin à condition d'utiliser l'option adéquate lors de la génération du premier certificat).

HA_CERT_LOCATION est l'emplacement où seront créés les certificats au format HAProxy. D'ailleurs, n'oubliez pas de créer le dossier en question.

Le script devra être appelé avec en paramètre le domaine complet principal du certificat. N'oubliez pas de le rendre exécutable.

Génération du premier certificat

On y est, maintenant on va enfin pouvoir générer ce satané certificat. Partons du principe que vous avez correctement installé acme.sh, le README du projet est très simple à suivre.

acme.sh --issue -d mon-super-site.com -w /var/www/htdocs/webroot/ --renew-hook "/etc/haproxy/generate-ssl.sh mon-super-site.com" --debug

Remarquez que vous pouvez générer un certificat couvrant plusieurs domaines à la fois, il suffit d'ajouter -d mon-domaine-secondaire.com entre le domaine principal et l'option -w. Remarquez l'option --renew-hook, elle permet d'appeler le script qu'on vient de définir à chaque fois qu'un renouvellement est effectué.

Une fois la commande effectuée avec succès (ça peut prendre une minute ou deux), votre certificat est généré, il vous faut encore ajouter le certificat dans la configuration du frontend comme il suit :

bind *:443 ssl cert /etc/haproxy/ssl/mon-super-site.com.haproxy.pem

Comme il s'agit de la première génération et non d'un renouvellement, il est nécessaire d'appliquer le script à la main.

/etc/haproxy/generate-ssl.sh mon-super-site.com

Vérifiez, et pleurez de joie. Votre certificat est déployé et fonctionnel. C'est magnifique.

Automatisation du renouvellement

Ultime étape, celle qui fait prendre tout son sens à la révolution Let's Encrypt, celle qui fait qu'on peut oublier sans scrupule que tel ou tel site a un certificat proche de l'expiration, il s'agit du renouvellement automatique. Pas de subtilités, une simple ligne dans la crontab fait l'affaire. Et en plus, si je ne dis pas de bêtises, acme.sh s'occupe tout seul d'ajouter cette ligne.

26 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

La commande sera lancée tous les jours à minuit vingt-six, libre à vous de changer cet horaire. acme.sh sera assez intelligent pour savoir si votre certificat a besoin d'un coup de jeune ou non, donc rassurez vous, il n'y aura pas un renouvellement par jour.

Conclusion

Le protocole ACME peut s'appliquer à toutes les architectures web pour peu qu'on puisse traiter les requêtes du challenge correctement. À moins de disposer d'un stockage partagé (NSF, iSCSI, Samba...), il est indispensable que le challenge ACME soit créé par la machine capable de le traiter.

Illustration : The Great A'Tuin par Paul Kidby

]]>
Let's Encrypt sur HAProxy (Partie 1)https://hashtagueule.fr/posts/let-s-encrypt-sur-haproxy-partie-1Mon, 06 Nov 2017 00:00:00 +0100raspbeguyarchitectureauto-hébergementplanet libreserveursysadminthéorietutorielswebhttps://hashtagueule.fr/posts/let-s-encrypt-sur-haproxy-partie-1Vous vous en souvenez peut-être, nous avions effectué un tutoriel Let's Encrypt quelques jours après sa mise en bêta publique. D'ailleurs, il mériterait un petit relooking, vu que quelques détails ont un peu changé, et que d'autres clients ACME ont vu le jour. Par exemple on a honteusement passé sous silence l'excellent acme.sh qui a le mérite de ne pas demander une ribambelle de dépendance en temps que simple script Bash. De plus, le client principal (dont le nom est désormais Certbot) a désormais implémenté des extensions Apache et Nginx qui fonctionnent (presque) comme on le souhaite, à savoir modifier tout seul les configurations des sites pour fonctionner avec Let's Encrypt. Cependant je reste plus enclin à utiliser la méthode manuelle.

À l'époque, je vous parlais d'une infrastructure simple, à savoir un unique serveur frontal : c'est lui qui interceptait les requêtes et qui les traitait, point barre. L'infra d'un site avec une audience restreinte, car tout doit être traité par une unique machine. Bonjour l'indispo surprise.

Parlons à présent d'une architecture un peu plus ambitieuse, le genre de topologie qu'on peut trouver en production pour des sociétés importantes (et les technophiles enthousiastes). Cette première partie sera dédiée à l'explication de cette mystérieuse architecture et à la présentation d'une manière de la mettre en œuvre. Dans une seconde partie, on apprendra à y déployer ses certificats Let's Encrypt.

Souvent donc, on ne se contente pas d'un unique frontal, mais de plusieurs frontaux, sous la coupe d'un répartiteur de charge, (load-balancer en anglais) qui se chargera de réceptionner les requêtes des utilisateurs et de les distribuer aux frontaux, selon une stratégie définie. Cette architecture a quatre avantages :

  • On gagne en charge maximale supportée, donc en nombre de visiteurs.
  • Multiplier les frontaux amoindrit fortement les risques de crash du site, car en cas de panne d'un frontal, les autres frontaux se partagent le trafic.
  • La mise en production d'une nouvelle version d'un site web peut se faire sans aucune interruption de service : il suffit de déployer sur un frontal à la fois.
  • Les frontaux sont sur un réseau privés et non directement accessible sur le réseau global, donc la sécurité du site est renforcée.

load-balancer

On constate que le répartiteur de charge est un potentiel goulot d'étranglement et il nous faut donc un programme optimisé pour la haute disponibilité afin que le flux des requêtes ne soit pas le facteur limitant.

Disons maintenant que l'on souhaite apposer un certificat sur cette architecture. Question à dix sesterces : qui parmi ces serveurs doit porter ce certificat ?

Si on choisit de faire porter le certificat par les frontaux, cela signifie qu'il faut que tous les frontaux doivent le porter, et qu'il faudra déployer le certificat sur toutes ces machine à chaque renouvellement. Mais surtout, cela signifie que, le trafic étant chiffré jusqu'aux frontaux, le répartiteur de charge ne pourra pas prendre en compte le contenu de la requête dans sa stratégie de répartition, ce qui est fort pénalisant. En effet, il est coutume de configurer le répartiteur de charge pour distribuer certaines URL sur un pool de frontaux différent du pool normal, par exemple le back-office (la page d'administration et de saisie de contenu du site). Enfin dernier point, si vous vous souvenez du fonctionnement du protocole ACME comme décrit dans notre tutoriel, vous saurez que le certificat doit être généré sur la machine qui en fait la demande et qui répondra au challenge ACME. Ceci implique qu'un des frontaux doit être désigné pour la génération des certificat, et donc que le répartiteur de charge doit rediriger toutes les requêtes de type ACME vers ce frontal, ce qui est à la fois moche et justement impossible car notre répartiteur de charge ne peut pas comprendre ce qu'il redirige à cause du chiffrement.

La solution la plus sensée, qui maintient également le plus l'isométrie des frontaux, est de faire porter le certificat par le répartiteur de charge.

Zoomons alors sur cette fameuse machine, le répartiteur de charge. On va partir d'un socle OpenBSD. Pourquoi pas un Linux ? Parce qu'on utilise souvent la même machine pour la répartition de charge et pour le pare-feu, et qu'OpenBSD est le système de prédilection pour faire un routeur libre. Et en plus j'aime bien cet OS, ça change un peu les idées.

Sur ce socle, on va utiliser le répartiteur de charge HAProxy. Les seules tâches de HAProxy sont de porter le certificat du ou des sites (on parle de SSL-offloading) et d'aiguiller le trafic web sur les frontaux. Il ne peut pas générer de flux web par lui-même, il peut seulement relayer une requête à un autre service. Et d'ailleurs il est très bon pour ça.

HAProxy est organisé en frontends et en backends : un frontend est la façade présentée au monde extérieur qui va recevoir les requêtes des internautes. Un backend est un pool de serveurs frontaux qui va traiter la requête. HAProxy doit donc choisir sur quel backend envoyer une requête, mais il doit également gérer le pool de frontaux que représente ce backend.

Prenons un exemple de configuration simple. On passera les directives globales HAProxy qui ne sont pas intéressantes et présentent peu de valeur ajoutée. Et bien entendu, on part du principe que vos frontaux sont configurés, écoutent sur des adresses locales et tout le toutim.

frontend mon_super_site
    bind *:80
    mode http

    acl url-back-office path_dir /admin

    use_backend back-office if url-back-office
    default_backend mes-frontaux

backend back-office
    server backoffice 192.168.0.14:80

backend mes-frontaux
    balance roundrobin
    server backoffice 192.168.0.11:80
    server backoffice 192.168.0.12:80
    server backoffice 192.168.0.13:80

Dans cette configuration, le frontend écoute sur toutes les adresses IP attribuées au serveur, sur le port 80 (donc pour le moment, uniquement du trafic en clair). Le mode HTTP signifie qu'on utilise ce frontend pour répartir du flux web, et non pas comme le mode TCP ou l'on redirige un flux TCP intouché.

On a écrit une ACL, autrement dit une condition. Retenez cette notion car les ACL sont au cœur du principe de HAProxy. Ici notre ACL est vraie si le chemin de l'URL commence par /admin et fausse sinon. On utilisera donc le backendback-office si l'ACL est vraie et sinon, on utilisera le backend par défaut mes-frontaux.

Le backendback-office est très simple car il ne comporte qu'un serveur. Le backendmes-frontaux comporte trois serveurs répartis en round-robin.

Et voilà, vous avez un répartiteur de charge. Si vous avez un certificat SSL, vous pouvez ajouter une ligne au frontend :

bind *:443 ssl cert /chemin/vers/mon/certificat.pem

Sauf que ça ne marchera pas. En tout cas pas tant que vous aurez mis votre certificat sous la forme attendue par HAProxy, à savoir la clef privée et le certificat dans le même fichier. Nous verrons cela dans la seconde partie, qui vous expliquera également (et finalement) comment diable utiliser Let's Encrypt avec ce bidule. En tout cas, si vous êtes perspicace, vous vous doutez que ça tournera en partie autour des ACL, car il y a bien une raison pour laquelle c'est le seul élément en gras de l'article.

(Source de l'image d'en-tête : Great A'tuin par Schiraki)

]]>
Une Flask de Djangohttps://hashtagueule.fr/posts/une-flask-de-djangoMon, 25 Sep 2017 00:00:00 +0200motiusdéveloppementdéveloppementdjangoframeworkmakefileplanet librepythonpython3théorietutorieltutorielswebhttps://hashtagueule.fr/posts/une-flask-de-djangoBonjour à tous !

Pour ceux qui ont déjà fait du développement web avec Django le titre est évident, pour les autres, bienvenue sur mon introduction à Django !

Django est un excellent framework Python qui permet de développer rapidement des applications web, et qui est très complet. Il est plus long à prendre en main que Flask, un autre microframework Python que je vais utiliser comme exemple afin d'introduire quelques concepts de Django, et de montrer que ces idées bizarres ne sortent pas de nulle part.

Parés ? C'est parti !

Prérequis

Ce tutoriel va référencer des tags du dépôt git à cette adresse. Je vous conseille aussi cet excellent tutoriel qui pourra compléter ce que j'écris ci-dessous. Enfin, la documentation de Django est complète et facile à utiliser.

Je vous conseille donc de cloner le dépôt ci-dessus :

git clone https://git.hashtagueule.fr/motius/django-tutorial.git

et de suivre les tags que j'ai indiqués au fur et à mesure de l'avancement du tutoriel grâce à la commande :

git checkout <tagname>

Je préfixe les noms de tous les tags git de ce tutoriel par un "_", afin que ceux-ci ne polluent pas le projet si vous en réutilisez le dépôt git par la suite (git tag admet l'option -d pour ceux qui veulent vraiment se débarasser de mes tags).

La quasi-totalité de ce tutoriel n'impose pas d'accès root sur le système de développement, je suppose tout de même que vous avez python3 et pip3 d'installés, un accès au réseau pour les packages Python supplémentaires, ainsi que le gestionnaire de versions de code git.

J'utilise une debian GNU/Linux 9.1 Stretch pour le développement, mais ce tutoriel doit facilement être adaptable à d'autres distributions.

Commençons en douceur avec Flask.

Flask, en deux mots

Je vais me faire battre, je vais parler de Flask alors que je l'ai utilisé en tout et pour tout 7 minutes 24 secondes dans ma courte vie.

Installation

Prenons l'exemple le plus basique avec Flask. La documentation du projet se trouve ici.

Elle indique qu'il suffit de taper la commande suivante pour installer Flask :

pip3 install --user Flask

Le projet fil d'ariane

Si vous avez suivi l'étape précédente, vous savez qu'il faut revenir à la version du tag "_v0_flask" comme ceci :

git checkout _v0_flask

pour cette partie du tutoriel. Vous pouvez alors faire tourner le serveur de test à l'aide de la commande :

make

Je vais faire une utilisation intensive des Makefiles, que j'affectionne, mais je vous encourage à les parcourir pour voir ce qu'ils exécutent.

Ici, la commande est simplement :

python3 ma_flasque.py

Vous pouvez alors vous rendre à l'url suivante : http://localhost:5000 dans mon cas.

Le pourquoi du comment

La partie intéressante que je voulais mettre en avant avec cet exemple concerne la structure du code Flask : j'ai divisé le source en trois parties, les imports, les routes Flask, et le code pour faire tourner l'application. La deuxième partie concernant les routes et le code métier est celle qui nous intéresse.

Les routes sont indiquées par un :

@app.route('/', methods=["GET"])

tandis que le code à exécuter dans le cas où l'utilisateur demande ('GET') la page par défaut de l'application ('/') se situe dans le code de la fonction Python index, qui dans notre cas retourne la chaîne de caractères "Hello World!" (tradition oblige) :

def index():
    return "Hello World!"

Ainsi, Flask pose les bases de la programmation avec le motif MVC, que Django utilise : on décrit de manière séparée le code de routage des URL et le code permettant d'obtenir le résultat demandé.

Django utilisera des fichiers séparés pour le routage (Vues) et pour les fonctions Python du Contrôleur, mais le principe est le même.

Il va falloir un peu de travail pour arriver au même résultat avec Django, mais n'aillez pas peur, on y arrivera. L'essentiel est de garder les yeux sur l'objectif : avoir une séparation entre le code de routage et le code métier.

Django va un peu plus loin car il permet de s'interfacer très facilement avec une base de données et en propose une abstraction très propre, il possède un moteur de template pour prémâcher du HTML, permet d'installer des modules, mais tout ça viendra plus tard, je vous mets l'eau à la bouche...

gâteau au chocolat

Un gâteau au chocolat pour vous mettre l'eau à la bouche si ma description des fonctionnalités de Django n'a pas suffi.

Même résultat avec Django

Installation

La page de téléchargement de Django indique, à l'heure actuelle, qu'il suffit de taper la commande suivante pour installer la dernière version de Django :

pip3 install Django==1.11.5

Si vous utilisez Django de manière professionnelle ou amateure, il faut consulter la page de téléchargement pour mettre à jour la version de Django utilisée.

Vous pouvez aussi installer la version de votre distribution, par exemple à l'aide de

su -c 'apt-get install python3-django'

si vous utilisez une debian ou dérivée (Ubuntu, Mint...)

On utilisera Django dans un virtualenv Python pour ce projet.

Virtualenv

Python est un langage de programmation modulaire dont le cœur des fonctionnalités est assez réduit, mais qui reste très puissant grâce au grand écosystème disponible de bibliothèques logicielles, officielles ou tierces.

Celles-ci peuvent être importées dans un script par une primitive import du type :

import os, sys
import re
import numpy as np
from django.http import HttpResponse

etc.

Les virtualenv Python servent à gérer pour chaque projet ses dépendances exactes, à l'inclusion des numéros de version des bibliothèques utilisées, et ce sans interférer avec d'autres projets.

On utilisera donc les virtualenv pour des questions de facilité, et parce qu'ils permettent de s'assurer de la pérennité d'un développement.

Vous pouvez installer virtualenv à l'aide de la commande :

sudo apt-get install python3-virtualenv

ou en tapant :

pip3 install --user virtualenv

Premier projet Django dans un Virtualenv

La version "_v1_django" permet d'installer Django dans un virtualenv Python. Si vous avez installé Django et Virtualenv comme indiqué précédemment, l'installation devrait se faire sans retélécharger des bibliothèques Python depuis internet.

Elle suppose que vous installez toutes les bibliothèques avec pip, si ce n'était pas le cas, il faut éventuellement changer la variable VENV du Makefile, ou installer une version avec pip3.

Pour installer le virtualenv, Django, créer un projet et une application, tapez simplement la commande :

make

Pour l'arrêter, tapez la combinaison de touches CTRL-C.

Détaillons les étapes du Makefile :

  • création du répertoire d'installation de l'environnement de développement, représenté par la variable INSTALL_DIR dans le Makefile ;
  • création du virtualenv Python dans l'environnement de développement ;
  • mise à jour de pip3 dans l'environnement de développement ;
  • installation de Django ;
  • création du projet, dont le nom est représenté par la variable PROJECT_NAME ;
  • création de l'application APP_NAME.

Vous remarquerez que chacune des étapes ne modifie que le répertoire d'installation situé à l'intérieur du répertoire git, jamais des bibliothèques système.

Utilisation de Django

Jusqu'ici, on n'a toujours pas écrit de code correspondant à notre application, seulement décrit une manière générale d'obtenir un projet Django d'équerre pour commencer. Il reste quelques étapes à connaître pour avancer dans le développement d'applications web avec Django sans que celui-ci se mette en travers de notre chemin.

Migrations

Lors de la création d'un projet Django, certaines applications sont installées pour nous, par exemple l'interface d'administration. Celle-ci n'est pas migrée, opération qu'il faut réaliser pour pouvoir l'utiliser.

On a par ailleurs créé notre propre application, qui n'est pas non plus migrée.

Il est nécessaire d'effectuer les migrations des applications, car celles-ci peuvent entraîner une modification du modèle en base de données.

Obtenez le code de la version _v2_migration du projet git pour pouvoir migrer les applications du projet, et lancer le serveur Django de développement.

La commande :

make

permet d'effectuer toutes les étapes décrites jusqu'ici :

  • installation de virtualenv, mise à jour de pip3, installation de django, etc. ;
  • création d'un projet django ;
  • création d'une application django ;
  • migrations du projet et de l'application ;
  • lancement du serveur de développement.

Rendez-vous  à l'URL suivante pour la page par défaut de Django : http://localhost:8000

Organisation du dépôt

Le dépôt est organisé de la manière suivante :

.
+-- doc
│   \-- README.md
+-- exec\_django.sh
+-- LICENSE
+-- Makefile
+-- mon\_django
│   +-- Makefile
│   +-- mon\_app
│   \-- mon\_django
+-- README.md
\-- sandbox
    +-- bin
    +-- db.sqlite3
    +-- include
    +-- lib
    +-- Makefile -> ../mon\_django/Makefile
    +-- manage.py
    +-- mon\_app -> ../mon\_django/mon\_app
    +-- mon\_django -> ../mon\_django/mon\_django
    +-- pip-selfcheck.json
    +-- static
    \-- templates -> ../mon\_django/templates
  • les sources sont dans le répertoire mon_django (nom du projet) ;
  • la production est dans le répertoire sandbox ;
  • les sources ont des liens sympboliques vers la production ;
  • à la racine du projet se retrouvent éventuellement :
    • la documentation ;
    • la licence ;
    • les tests (unitaires, intégration).

Cette organisation a pour but de simplifier le développement :

  • le Makefile (et les scripts qu'il appelle) permet de construire et reconstruire le projet à l'aide d'une seule commande ;
  • à chaque enregistrement d'une modification d'un fichier source sur disque, le serveur de développement Django redémarre avec la mise à jour, grâce aux liens symboliques vers les sources ;
  • la production est séparée des sources ;
  • le dépôt git peut être organisé en suivant les bonnes pratiques de développement Python :
    • disposition de la documentation ;
    • disposition des tests unitaires et intégration...

Comparaison Django et Flask

Ça y est, on a enfin tout un environnement prêt pour le développement. Vous allez me dire "était-ce bien la peine de faire tout ça pour en arriver au même point ?" Je crois que oui.

Avantages de Django

Pour plusieurs raisons :

  • le code de Django est plus organisé que celui de Flask, ce qui procure un avantage sur le long terme :
    • les vues et le contrôleur sont séparés ;
    • Django incite à faire du développement de petites applications réutilisables au sein de plusieurs projets
  • Django va permettre de s'interfacer facilement avec une base de données, ce qui fait qu'on a en fait de l'avance par rapport au cas où l'on serait restés avec Flask
  • j'ai mis en place un système de construction avec les Makefiles qui permet de tester son application à l'aide d'une seule commande, comme pour Flask.

Je ne détaille pas les inconvénients de Django, il y en a un principal selon moi, la relative difficulté de le mettre en place, ce qui est fait par le système de construction logicielle avec Makefile.

Comprendre le code de Django en comparaison avec celui de Flask

Django fonctionne fondamentalement de la même manière que Flask pour le routage, mais il sépare les vues et le contrôleur :

  • on utilise généralement le fichier urls.py pour décrire la liste des urls proposées par une applications :
    • ces URL ne doivent pas se chevaucher, autrement une seule des URL serait prise en compte (la première rencontrée par Django) ;
    • la liste des URL de tous les fichiers urls.py de toutes les applications constitue l'API backend offerte par votre projet Django ;
  • on utilise le fichier views.py pour le code métier qui va recevoir les requêtes GET/POST HTTP ;
  • on peut éventuellement créer d'autres modules Python pour du code spécifique ;
  • on utilise le fichier models.py pour décrire les modèles des données de l'application, tels que ces données seront sauvegardées en base de données.

Le développement avec Django

Maintenant que vous avez vu l'installation de Django en virtualenv Python, ainsi que la théorie sur la manière dont il faut organiser le code Python, allons voir quelques exemples simples.

Hello World!

Obtenez la version _v3_application du dépôt. Elle contient un simple "Hello World!" si le client demande la page http://localhost:8000/, et une simple indication "Not found" pour toutes les autres pages. Comme pour chanque étape de ce tutoriel, la commande :

make

permet de faire le café (enfin presque). Par rapport à l'étape précédente, on dispose désormais d'une application Django dont le code Python est importé.

café au lait

Le café au lait que mon ordinateur ne sait toujours pas faire... Ça viendra.

En lisant le code,on s'aperçoit que son organisation est similaire à celle de Flask : d'un côté on route les urls, on utilise un ou plusieurs fichiers urls.py pour ce faire, et dans les vues du fichier views.py de chaque application du projet, on peut écrire le code à exécuter pour chaque requête.

urlpatterns = [
    url('^$', views.index),
    url(r'^admin/', admin.site.urls),
    url('^.*', appviews.p404),
]

Le routage des URL du projet (et de chacune des applications) est défini à l'aide de regexPython. Je fait correspondre les URL suivantes :

  • http://localhost:8000/
  • http://localhost:8000

au code ci-dessous :

def index(request):
    return HttpResponse("Hello World!")

à l'aide de la première règle de routage.

La page suivante de la documentation permet une utilisation avancée des URL de routage de l'application. On peut par exemple utiliser un champ de l'URL comme paramètre.

Je ne vous ai pas promis la lune mais presque, allons plus loin dans les fonctionnalités qui sont un des grands avantages de Django.

Mon premier gabarit

La version _v4_template permet d'utiliser les templates de Django. Je n'en fait qu'un usage très limité, je me limite à indiquer à Django où il doit aller chercher les fichiers de template correspondant au code HTML/CSS/JavaScript, servis par le serveur de développement, mais ce moteur de gabarits possède de nombreuses fonctionnalités, comme les variables, les tags, des boucles, des conditions...

La page accessible à l'URL par défaut http://localhost:8000/ affiche le code du template index.html :

def index(request):
    return render(request, "index.html", {})

J'ai rajouté de manière explicite un paramètre optionnel : le dictionnaire vide {}. Il sert à préciser des variables Python à transmettre au moteur de gabarits.

Toutes les autres requêtes redirigent vers la page 404.

Un accès à la base de données

Une grande quantité d'applications web nécessite une base de données. Django propose un moyen simple pour s'interfacer avec une base de données sans nécessairement en connaître les subtilités, fournit des garanties qui permettent éventuellement de changer de moteur de base de données, et permet au développeur de manipuler les objets en base facilement.

La version _v5_bdd présente un exemple simple d'utilisation d'une base de données. Les URL suivantes sont valides :

  • http://localhost:8000/create/ : crée un objet en base de données ;
  • http://localhost:8000/delete/ : supprime tous les objets ;
  • http://localhost:8000/get_nb_objects/ : affiche le nombre d'objets en base de données.

Attention, celles-ci ne le sont pas, il manque un / à la fin :

  • http://localhost:8000/create ;
  • http://localhost:8000/delete ;
  • http://localhost:8000/get_nb_objects.

Si vous vous demandez pourquoi l'exemple marche bien que tous les objets aient le même nom et la même valeur de booléen, c'est parce qu'ils se voient attribuer un identifiant entier automatique croissant si on ne définit pas d'attribut ayant une clef primaire, cf. la documentation.

Découpage en modules

Vous remarquerez l'utilisation de la primitive Django include dans le code, par rapport à la version _v4_template, qui permet de découper le routage vers les URL d'une application.

Utilisation d'un constructeur personnalisé

Django utilise la fonction __init__ qui sert à construire des objets Python. La documentation explique comment attacher un constructeur aux objets sérialisés en base de données, plutôt que de recourir à une méthode build comme je m'y suis pris dans l'exemple _v5_bdd. L'exemple se trouve dans le code de la version  _v6_constructeur.

Pour aller plus loin

Je ferai peut-être une suite à ce tutoriel Django. Dans tous les cas, sachez que le point auquel nous sommes arrivés n'est pas suffisant ! En effet, on utilise toujours le serveur de développement de Django, qui n'est pas fait pour être utilisé en production, entre autres :

"(notre métier est le développement d’environnements Web, pas de serveurs Web)."

Mise en production

D'autres éléments sont à considérer durant et après la phase de développement :

  • la mise en place d'un mécanisme de journalisation de l'application ;
  • la mise en place d'un chien de garde permettant de s'assurer que le serveur (nginx, apache, ou autre) sert toujours l'application que vous avez écrite ;
  • la mise en place d'une sauvegarde des données en base...

Il s'agit d'un travail d'administration système indispensable. À ce travail s'ajoute éventuellement celui de développer une interface web ou autres, si besoin est.

Modules

Je n'ai pas parlé des modules de Django. Dans la version _v4_bdd, on utilise la fonction Django render, qui permet de retourner une chaîne de caractères en temps que réponse HTTP. Cela diffère de Flask, qui permet de simplement retourner une chaîne de caractères. L'explication tient au fait que Django utilise l'objet request pour le passer aux modules Django, dans la fonction render.

Si vous êtes intéressés par le sujet, je vous conseille cette page, qui décrit les interactions possibles sur une requête, ainsi que la base de leur fonctionnement.

Réutilisation

Voilà un bon début de projet Django. Pour le réutiliser, n'oubliez pas :

  • de changer la clef API située dans le fichier mon_django/settings.py ;
  • de respecter la licence GPLv3.

Vous pouvez également créer un répertoire git de zéro à partir d'un des tags du dépôt exemple. Pour cela, obtenez les sources du dépôt au tag que vous souhaitez :

git checkout <tagname>

Puis supprimez le dépôt git sans toucher au fichiers existant en enlevant l'arborescence sous le répertoire .git :

rm -rf ./.git

Enfin, créez un nouveau dépôt :

git init

Vous pouvez aussi garder le dépôt git en l'état, le script del_tags.sh permet de supprimer les tags commençant par "_".

Conclusion

Mon but ici n'est pas de prétendre que Django est meilleur que Flask, comme je l'ai indiqué au début de ce tutoriel, j'ai très peu touché à Flask. Flask est un prétexte pour moi pour montrer un des différents aspects de Django (le routage des requêtes). Par ailleurs, en lisant un peu sur le sujet, on s'aperçoit que beaucoup de modules peuvent être utilisés pour donner à Flask des capacités similaires à celles qui viennent avec Django, par exemple :

  • une interface d'administration ;
  • un module ORM pour s'interfacer avec une base de données...

Il y a donc des arguments en faveur de Flask par rapport à Django, par exemple :

  • le code de Flask est composé de moins de lignes de code  ;
  • on peut choisir ce dont on a réellement besoin parmi des fonctionnalités offertes par la bibliothèque qu'on utilise...

Django, a de son côté :

  • l'organisation générale d'un projet web :
    • Django encourage à structurer son code ;
    • Django suggère la division du code en applications ;
    • il permet aux développeurs de travailler sur des applications différentes, ce qui facilite la collaboration ;
  • il vient avec un moteur de templates ;
  • il vient avec un ORM...

Cette comparaison n'est évidemment pas exhaustive, mais permet de se rendre compte des convergences et divergences de philosophie de ses deux projets.

Vous voilà parés pour démarrer ! J'espère que ce tutoriel vous a été utile. Pour ma part, je trouve que Django est très sympathique à utiliser, sa documentation est facile à prendre en main pour un usage débutant, même si elle est touffue. Enfin sachez que raspbeguy et moi-même avons quelques projets web (parmi lesquels clickstart, et Hashtagueule Express) qui utiliseront peut-être cette technologie.

Bonne journée et bon développement Python/Django !

Motius

PS : après avoir commencé cet article, je suis tombé sur celui-ci, en anglais. Je tiens à le mentionner car la majorité des articles Flask vs Django sont théoriques (souvent des descriptions fonctionnelles, assez intéressantes pour se familiariser rapidement avec les deux projets), mais je n'avais pas vu (ni vraiment cherché) d'article les comparant en mettant la main dans le code.

]]>