Des environnements de développement locaux avec Terraform + LXD
1 juin 2023
Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.
En tant qu’architecte de solutions Big Data et InfraOps, j’ai besoin d’environnements de développement pour installer et tester des logiciels. Ils doivent être configurables, flexibles et performants. En travaillant avec des systèmes distribués, les setups les plus adaptées à ce cas d’utilisation sont des clusters virtualisés locaux de plusieurs instances Linux.
Depuis quelques années, j’utilise Vagrant de HashiCorp pour gérer des instances libvirt/KVM. Cela fonctionne assez bien, mais j’ai récemment expérimenté une autre option qui fonctionne mieux pour moi : LXD pour gérer les instances et Terraform (un autre outil HashiCorp) pour piloter LXD. Dans cet article, j’explique quels en sont les avantages et comment mettre en place un tel environnement.
Glossaire
Vagrant et Terraform
Vagrant permet aux utilisateurs de créer et de configurer des environnements de développement légers, reproductibles et portables. Il est principalement utilisé pour provisionner des machines virtuelles localement.
Terraform est un outil d’Infrastructure as Code largement utilisé qui permet de provisionner des ressources sur pratiquement n’importe quel cloud. Il prend en charge de nombreux fournisseurs, du cloud public (AWS, Azure, GCP) à l’infrastructure privée auto-hébergée (OpenStack, Kubernetes et LXD bien sûr). Avec Terraform, les équipes InfraOps appliquent les meilleures pratiques GitOps pour gérer leur infrastructure.
Virtualisation/conteneurisation Linux
Voici une revue rapide des différents outils (et acronymes) utilisés dans cet article qui composent le vaste écosystème de virtualisation/conteneurisation Linux :
-
Outils de machines virtuelles :
- QEMU (Quick EMUlator) : un outil d’émulation et de virtualisation qui fait office d’hyperviseur de VM, à la place de Virtualbox par exemple.
- KVM (Kernel Virtual Machine) : un module du noyau Linux qui exploite la virtualisation matérielle, notamment les fonctionnalités spéciales du processeur spécialement conçues pour la virtualisation (par exemple, Intel VT).
- libvirt : une API de virtualisation qui supporte plusieurs hyperviseurs de VM et simplifie la gestion des VM depuis n’importe quel langage de programmation.
-
Outils de conteneurs Linux (voir LXD : la pièce manquante pour en savoir plus sur les conteneurs Linux) :
-
LXC (LinuX Containers) : une interface pour créer et gérer des conteneurs système ou applicatif
-
LXD (LinuX container Daemon) : gestionnaire de VM et de conteneurs système qui offre une expérience utilisateur unifiée autour de systèmes Linux complets. Il fonctionne au-dessus de LXC (pour les conteneurs) et de QEMU (pour les machines virtuelles) pour gérer les machines hébergées par un hôte individuel ou par un cluster d’hôtes fédérés.
-
LXC (the other one) : la CLI pour LXD
Utiliser Vagrant pour gérer des VMs KVM est réalisé via le provider vagrant-libvirt. Voir Machines KVM pour Vagrant sur Archlinux pour savoir comment configurer libvirt/KVM avec Vagrant.
Pourquoi Terraform ?
LXD se pilote en CLI avec la command lxc
pour gérer ses ressources (conteneurs et VM, réseaux, pools de stockage, profils d’instance). Étant un outil en ligne de commande, il n’est par nature pas compatible avec Git.
Heureusement, il existe un fournisseur Terraform pour gérer LXD : terraform-provider-lxd. Cela permet de versionner la configuration de l’infrastructure LXD parallèlement au code d’une application.
Remarque : Un autre outil pour piloter LXD pourrait être Juju de Canonical, mais il semble un peu plus complexe à apprendre.
Pourquoi Terraform + LXD ? Avantage par rapport à Vagrant + libvirt/KVM
Redimensionnement en live des instances
Les conteneurs Linux sont plus flexibles que des VMs, ce qui permet de redimensionner les instances sans redémarrage. C’est une fonctionnalité très pratique.
Outillage unifié du développement à la production
LXD peut être installé sur plusieurs hôtes pour créer un cluster qui peut être utilisé comme couche de base d’un cloud auto-hébergé. Le couple Terraform + LXD permet ainsi de gérer des environnements locaux, d’intégration et de production. Cela facilite considérablement le test et le déploiement des configurations d’infrastructure.
Prise en charge de LXD dans Ansible
Pour installer et configurer des logiciels sur des instances locales, j’utilise souvent Ansible. Il existe plusieurs connection plugins disponibles dans Ansible pour se connecter aux hôtes cibles, le principal étant ssh
.
Lors du provisionnement des instances LXC, nous pouvons utiliser le plugin ssh
standard mais aussi un plugin LXC natif : lxc
(qui utilise la bibliothèque Python LXC) ou lxd
(qui utilise la CLI LXC). Ceci est bénéfique pour deux raisons :
- Pour la sécurité car nous n’avons pas besoin de démarrer un serveur OpenSSH et d’ouvrir le port SSH sur nos instances
- Pour plus de simplicité car nous n’avons pas à gérer les clés SSH pour Ansible
Aperçu des changements de configuration
L’une des principales fonctionnalités de Terraform est la possibilité de prévisualiser les modifications qu’une commande appliquerait. Cela évite les déploiements de configuration indésirables et les erreurs de commande.
Exemple avec le redimensionnement d’un profil d’instance LXD :
$ terraform plan
...
Terraform will perform the following actions:
# lxd_profile.tdp_profiles["tdp_edge"] will be updated in-place
~ resource "lxd_profile" "tdp_profiles" {
~ config = {
~ "limits.cpu" = "1" -> "2"
~ "limits.memory" = "1GiB" -> "2GiB"
}
id = "tdp_edge"
name = "tdp_edge"
}
Plan: 0 to add, 1 to change, 0 to destroy.
Lisibilité et modularité de la configuration
Le langage Terraform est déclaratif. Il décrit un objectif visé plutôt que les étapes pour atteindre cet objectif. En tant que tel, il est plus lisible que le langage Ruby utilisé dans les fichiers Vagrant. De plus, comme Terraform analyse tous les fichiers du répertoire actuel et permet de définir des modules avec des inputs et des outputs, nous pouvons très facilement diviser la configuration pour augmenter la maintenabilité.
# Using multiple Terraform config files:
$ ls -1 | grep -P '.tf(vars)?$'
local.auto.tfvars
main.tf
outputs.tf
provider.tf
terraform.tfvars
variables.tf
Gain de performance
L’utilisation de Terraform + LXD accélère les opérations quotidiennes dans les environnements de développement locaux, ce qui est toujours agréable.
Voici un benchmark des performances lors de l’exploitation d’un cluster de développement local avec les spécifications suivantes :
- OS hôte : Ubuntu 20.04
- Nombre d’instances guest : 7
- Ressources allouées : 24GiB de RAM et 24 vCPUs
Métrique | Vagrant + libvirt/KVM | Terraform + LXD | Gain de performance |
---|---|---|---|
Création du cluster (sec) | 56.5 | 51 | 1.1x faster |
Démarrage du cluster (sec) | 36.5 | 6 | 6x faster |
Arrêt du cluster (sec) | 46 | 13.5 | 3.4x faster |
Destruction du cluster (sec) | 9 | 17 | 2x slower |
Mise en place d’un environnement Terraform + LXD minimal
Essayons maintenant de configurer un environnement Terraform + LXD minimal.
Prérequis
Votre ordinateur à besoin de :
- LXD (voir Installation)
- Terraform >= 0.13 (voir Install Terraform)
- Linux cgroup v2 (pour faire tourner des conteneurs Linux récents comme Rocky 8)
- 5 GB de RAM disponible
Créez également un répertoire à partir duquel travailler :
mkdir terraform-lxd-xs
cd terraform-lxd-xs
Linux cgroup v2
Pour vérifier si votre système utilise cgroup v2, exécutez :
stat -fc %T /sys/fs/cgroup
# cgroup2fs => cgroup v2
# tmpfs => cgroup v1
Les distributions récentes utilisent cgroup v2 par défaut (consultez la liste ici) mais la fonctionnalité est disponible sur tous les hôtes qui exécutent un noyau Linux >= 5.2 (par exemple Ubuntu 20.04). Pour l’activer, consultez Enabling cgroup v2.
Le provider Terraform
Nous utiliserons le provider Terraform terraform-lxd/lxd
pour gérer nos ressources LXD.
Créez provider.tf
:
terraform {
required_providers {
lxd = {
source = "terraform-lxd/lxd"
version = "1.7.1"
}
}
}
provider "lxd" {
generate_client_certificates = true
accept_remote_certificate = true
}
Définition des variables
Il est recommandé de permettre aux utilisateurs de configurer l’environnement Terraform via des input variables. Nous forçons l’exactitude des variables en déclarant leurs types attendus.
Créez variables.tf
:
variable "xs_storage_pool" {
type = object({
name = string
source = string
})
}
variable "xs_network" {
type = object({
ipv4 = object({
address = string
})
})
}
variable "xs_profiles" {
type = list(object({
name = string
limits = object({
cpu = number
memory = string
})
}))
}
variable "xs_image" {
type = string
default = "images:rocky/8"
}
variable "xs_containers" {
type = list(object({
name = string
profile = string
ip = string
}))
}
Les variables suivantes sont définies :
xs_storage_pool
: la storage pool LXD qui stocke les disques des conteneursxs_network
: le réseau IPv4 LXD utilisé par les conteneurs pour communique au sein d’un réseau partagéxs_profiles
: les profiles LXD créés pour nos conteneurs. Les profiles permettent la définition d’un ensemble de propriétés qui peuvent être appliqué à n’importe quel conteneur.xs_image
: l’image LXD. Cela définie principallement quel OS les conteneurs utilisent.xs_containers
: Les instances LXD à créer.
Main
Le fichier Terraform main
définit toutes les ressources configurées par les variables. Ce fichier n’est pas modifié très souvent par les développeurs après sa première implémentation pour le projet.
Créez main.tf
:
# Storage pools
resource "lxd_storage_pool" "xs_storage_pool" {
name = var.xs_storage_pool.name
driver = "dir"
config = {
source = "${path.cwd}/${path.module}/${var.xs_storage_pool.source}"
}
}
# Networks
resource "lxd_network" "xs_network" {
name = "xsbr0"
config = {
"ipv4.address" = var.xs_network.ipv4.address
"ipv4.nat" = "true"
"ipv6.address" = "none"
}
}
# Profiles
resource "lxd_profile" "xs_profiles" {
depends_on = [
lxd_storage_pool.xs_storage_pool
]
for_each = {
for index, profile in var.xs_profiles :
profile.name => profile.limits
}
name = each.key
config = {
"boot.autostart" = false
"limits.cpu" = each.value.cpu
"limits.memory" = each.value.memory
}
device {
type = "disk"
name = "root"
properties = {
pool = var.xs_storage_pool.name
path = "/"
}
}
}
# Containers
resource "lxd_container" "xs_containers" {
depends_on = [
lxd_network.xs_network,
lxd_profile.xs_profiles
]
for_each = {
for index, container in var.xs_containers :
container.name => container
}
name = each.key
image = var.xs_image
profiles = [
each.value.profile
]
device {
name = "eth0"
type = "nic"
properties = {
network = lxd_network.xs_network.name
"ipv4.address" = "${each.value.ip}"
}
}
}
Les ressources suivantes sont créées par Terraform :
lxd_network.xs_network
: le réseau pour toutes nos instanceslxd_profile.xs_profiles
: plusieurs profils pouvant être définis par l’utilisateurlxd_container.xs_containers
: les définitions des instances (y compris l’application du profil et l’attachement du périphérique réseau)
Fichier de variables
Enfin, nous fournissons à Terraform les variables propres à notre environnement. Nous utilisons l’extension auto.tfvars
pour charger automatiquement les variables lors de l’exécution de terraform
.
Créez local.auto.tfvars
:
xs_storage_pool = {
name = "xs_storage_pool"
source = "lxd-xs-pool"
}
xs_network = {
ipv4 = { address = "192.168.42.1/24" }
}
xs_profiles = [
{
name = "xs_master"
limits = {
cpu = 1
memory = "1GiB"
}
},
{
name = "xs_worker"
limits = {
cpu = 2
memory = "2GiB"
}
}
]
xs_image = "images:rockylinux/8"
xs_containers = [
{
name = "xs-master-01"
profile = "xs_master"
ip = "192.168.42.11"
},
{
name = "xs-master-02"
profile = "xs_master"
ip = "192.168.42.12"
},
{
name = "xs-worker-01"
profile = "xs_worker"
ip = "192.168.42.21"
},
{
name = "xs-worker-02"
profile = "xs_worker"
ip = "192.168.42.22"
},
{
name = "xs-worker-03"
profile = "xs_worker"
ip = "192.168.42.23"
}
]
Provisionnement de l’environment
Nous avons maintenant tous les fichiers nécessaires pour provisionner notre environnement :
# Install the provider
terraform init
# Create the directory for the storage pool
mkdir lxd-xs-pool
# Preview and apply the resource changes
terraform apply
Une fois les ressources créées, nous pouvons vérifier que tout fonctionne correctement :
# Check resources existance in LXD
lxc network list
lxc profile list
lxc list
# Connect to an instance
lxc shell xs-master-01
Et voilà !
Note, pour détruire l’environment : terraform destroy
Exemple plus avancé
Vous pouvez jeter un œil à tdp-lxd pour une configuration plus avancée avec :
- Plus de profils
- Du templating de fichier (pour un inventaire Ansible)
- La définition des outputs
Conclusion
La combinaison de Terraform et LXD apporte une nouvelle façon de gérer les environnements de développement locaux qui présente plusieurs avantages par rapport aux concurrents (à savoir Vagrant). Si vous utilisez souvent ce type d’environnement, je vous suggère de l’essayer !