L’objectif de cette partie est de déployer un conteneur LXC contenant Ubuntu 24.04 LTS depuis notre machine de déploiement (qui est elle même un conteneur LXC).

Prérequis :

  • Notre machine de déploiement existe et nous y avons accès en SSH
  • L’outil terraform est accessible en ligne de commande
  • Nous avons un token d’API Proxmox VE nous permettant de créer et d’administrer des conteneurs LXC
  • Nous avons un dossier pour notre projet (~/homelab_julia_pve_tf_ansible) contenant un fichier .envrc avec notemment le token d’API Proxmox VE précédemment décrit

Génération de clés

Nous aurons besoin de nous connecter (à l’aide de SSH) depuis la machine de déploiement aux conteneurs LXC que nous allons créer. Aussi il est nécessaire de créer sur cette machine des clés.

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh-keygen                 
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/deployer/.ssh/id_ed25519): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/deployer/.ssh/id_ed25519
Your public key has been saved in /home/deployer/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:JyfdvtDMzpfSFyYoTysLqVAIUT3accIFG6IeV3u+PRA deployer@ubuntu-deploy
The key's randomart image is:
+--[ED25519 256]--+
| ..oo+o.         |
|  o o*+.         |
| + .oo=E         |
|. +...o .. .     |
| . . . oS + o    |
|    .   =* B . o |
|   .   + o= *.o..|
|    . . ...*..+ .|
|     .   .o +o . |
+----[SHA256]-----+

Créations des fichiers Terraform

Nous allons créer notre premier projet Terraform à l’aide de 3 fichiers.

  • main.tf
  • variables.tf
  • terraform.tfvars

L’organisation d’un projet Terraform en trois fichiers distincts - main.tf, variables.tf et terraform.tfvars - reflète une pratique éprouvée d’organisation et de séparation des responsabilités dans le code infrastructure. Cette structure suit le principe de séparation des préoccupations, où main.tf contient la logique principale de l’infrastructure (définition des ressources, providers et leurs configurations), variables.tf déclare et documente toutes les variables utilisables (avec leurs types, descriptions et contraintes éventuelles), et terraform.tfvars stocke les valeurs concrètes de ces variables. Cette séparation offre plusieurs avantages : elle améliore la sécurité en permettant d’isoler les informations sensibles dans terraform.tfvars (qui peut être exclu du contrôle de version), facilite la réutilisation du code en permettant différentes configurations pour différents environnements, et augmente la maintenabilité en organisant clairement le code. Cette approche modulaire permet également une meilleure collaboration au sein des équipes, car chaque fichier a un rôle bien défini et peut être maintenu indépendamment. Elle suit les meilleures pratiques de l’Infrastructure as Code en rendant le code plus lisible, plus facile à maintenir et plus sécurisé.

Commençons par créer notre fichier main.tf (je l’ai personnellement fait dans VS Code, après m’être connecté en SSH et après avoir installé l’extension Terraform pour VS Code)

main.tf

Code

Voici le contenu du fichier main.tf

# Configuration du provider Terraform
terraform {
  required_providers {
    # Utilisation du provider Proxmox de BPG
    proxmox = {
      source = "bpg/proxmox"
      version = "0.42.0"
    }
  }
}

# Configuration du fournisseur Proxmox
provider "proxmox" {
  # URL de l'API Proxmox
  endpoint = var.proxmox_endpoint
  # Token d'authentification (à définir dans les variables)
  api_token = var.api_token
  # Désactive la vérification SSL pour les environnements de test
  insecure = true
  # Configuration SSH pour l'accès root
  ssh {
    agent = true
    username = "root"
  }
}

# Définition des conteneurs LXC
resource "proxmox_virtual_environment_container" "lxc_container" {
  # Nombre de conteneurs à créer
  count = var.container_count
  # Description pour l'identification dans Proxmox
  description = "Managed by Terraform"
  # Nœud Proxmox cible
  node_name = var.target_node
  # ID de la VM (incrémenté pour chaque conteneur)
  vm_id = 241201 + count.index

  # Ajout de tags au conteneur
  # Les tags peuvent être utilisés pour identifier et organiser les conteneurs
  # Plusieurs tags peuvent être ajoutés, séparés par des points-virgules
  tags = [
    "calcul-distribue",                # Tag pour identifier le type d'usage
    "julia-worker-${count.index + 1}", # Tag unique pour chaque worker
    "terraform-managed"                # Tag indiquant la gestion par Terraform
  ]

  # Configuration initiale du conteneur
  initialization {
    # Nom d'hôte avec index
    hostname = "${var.vm_hostname}-${count.index + 1}"
    
    # Configuration IP
    ip_config {
      ipv4 {
        # Attribution d'une IP statique dans la plage 192.168.1.201+
        address = "192.168.1.${201 + count.index}/24"
        gateway = var.gateway
      }
    }
    
    # Configuration utilisateur avec clé SSH
    user_account {
      keys = var.ssh_public_keys
    }
  }

  # Configuration réseau
  network_interface {
    name = "eth0"
    bridge = "vmbr0"
  }

  # Système d'exploitation
  operating_system {
    template_file_id = var.template_file_id
    type = "ubuntu"
  }

  # Ressources CPU
  cpu {
    cores = var.cores
  }

  # Allocation mémoire
  memory {
    dedicated = var.memory
  }

  # Configuration stockage
  disk {
    datastore_id = var.disk.storage
    size         = var.disk.size
  }

  # Fonctionnalités avancées
  features {
    nesting = true
  }

  # Démarrage automatique
  start_on_boot = var.onboot
  # Mode non privilégié pour plus de sécurité
  unprivileged = true
}

Explications

Le fichier se divise en trois sections principales :

  1. Configuration du provider Terraform
  2. Configuration du fournisseur Proxmox
  3. Définition des ressources (conteneurs LXC)

Il permet de créer count (=1 ici) conteneurs LXC dont les adresses IP commencent à 192.168.1.200

variables.tf

Voici le contenu de variables.tf

# Token d'API pour l'authentification Proxmox
# À définir via une variable d'environnement ou un fichier tfvars
variable "api_token" {
  description = "Token pour la connexion à l'API Proxmox"
  type        = string
  sensitive   = true
}

# Endpoint de l'API Proxmox
variable "proxmox_endpoint" {
  description = "URL de l'API Proxmox (exemple: https://192.168.1.7:8006/)"
  type        = string

  validation {
    condition     = can(regex("^https://.*:\\d+/$", var.proxmox_endpoint))
    error_message = "L'endpoint doit être une URL HTTPS valide se terminant par un port et un slash (exemple: https://192.168.1.7:8006/)"
  }
}

# Nombre de conteneurs à créer
variable "container_count" {
  description = "Nombre de conteneurs LXC à créer"
  type        = number
  default     = 1

  validation {
    condition     = var.container_count >= 0 && var.container_count <= 10
    error_message = "Le nombre de conteneurs doit être entre 0 et 10."
  }
}

# Nœud Proxmox cible pour le déploiement
variable "target_node" {
  description = "Nom du nœud Proxmox pour le déploiement"
  type        = string
  default     = "pve"
}

# Nom de base pour les conteneurs
variable "vm_hostname" {
  description = "Préfixe de nom d'hôte pour les conteneurs"
  type        = string
  default     = "lxc-ubuntu"

  validation {
    condition     = length(var.vm_hostname) > 2 && length(var.vm_hostname) <= 63
    error_message = "Le nom d'hôte doit faire entre 3 et 63 caractères."
  }
}

# Template du conteneur à utiliser
variable "template_file_id" {
  description = "ID du template de conteneur à utiliser"
  type        = string
  default     = "local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst"
}

# Configuration du processeur
variable "cores" {
  description = "Nombre de cœurs CPU à allouer"
  type        = number
  default     = 2

  validation {
    condition     = var.cores > 0 && var.cores <= 8
    error_message = "Le nombre de cœurs doit être entre 1 et 8."
  }
}

# Configuration de la mémoire
variable "memory" {
  description = "Quantité de mémoire en MB"
  type        = number
  default     = 2048

  validation {
    condition     = var.memory >= 512 && var.memory <= 16384
    error_message = "La mémoire doit être entre 512 MB et 16 GB."
  }
}

# Configuration du disque
variable "disk" {
  description = "Configuration du stockage"
  type = object({
    storage = string
    size    = string
  })
  default = {
    storage = "local-lvm"
    size    = "20"  # Taille en GB sans le suffixe
  }

  validation {
    condition     = can(tonumber(var.disk.size))
    error_message = "La taille du disque doit être un nombre (en GB) sans suffixe."
  }
}

# Configuration du démarrage automatique
variable "onboot" {
  description = "Démarrer le conteneur au boot du système"
  type        = bool
  default     = true
}

# Configuration réseau
variable "gateway" {
  description = "Passerelle réseau par défaut"
  type        = string
  default     = "192.168.1.1"

  validation {
    condition     = can(regex("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$", var.gateway))
    error_message = "L'adresse de la passerelle doit être une IPv4 valide."
  }
}

# Serveurs DNS
variable "dns_servers" {
  description = "Liste des serveurs DNS"
  type        = list(string)
  default     = ["192.168.1.1", "1.1.1.1"]

  validation {
    condition     = length(var.dns_servers) > 0
    error_message = "Au moins un serveur DNS doit être spécifié."
  }
}

# Clé SSH publique pour l'accès aux conteneurs
variable "ssh_public_keys" {
  description = "Liste des clés SSH publiques pour l'accès aux conteneurs"
  type        = list(string)

  validation {
    condition     = length(var.ssh_public_keys) > 0
    error_message = "Au moins une clé SSH publique doit être fournie."
  }

  validation {
    condition = alltrue([
      for key in var.ssh_public_keys :
      can(regex("^(ssh-rsa|ssh-ed25519|ssh-dss)", key))
    ])
    error_message = "Toutes les clés doivent être dans un format SSH valide (ssh-rsa, ssh-ed25519, ou ssh-dss)."
  }
}

terraform.tfvars

Voici enfin le contenu de terraform.tfvars

proxmox_endpoint = "https://192.168.1.6:8006/"

# Ne jamais mettre le api_token dans ce fichier
# Utiliser plutôt une variable d'environnement :
# export TF_VAR_api_token="votre_token"
# api_token = "NE_PAS_METTRE_ICI"

# Nom du nœud Proxmox cible
target_node = "opti-7010"

# Nombre de conteneurs à créer
container_count = 1

# Configuration des conteneurs
vm_hostname = "ubuntu-lxc"

# Ressources de calcul
cores = 2
memory = 2048  # 2 GB

# Configuration du stockage
disk = {
  storage = "local-lvm"
  size    = "20"  # Taille en GB sans le suffixe "G"
}

# Configuration réseau
gateway = "192.168.1.1"
dns_servers = [
  "192.168.1.1",  # Passerelle locale comme DNS primaire
  "1.1.1.1"       # Cloudflare comme DNS secondaire
]

# La clé publique SSH doit être générée au préalable
# Exemple de commande : ssh-keygen -t ed25519 -C "terraform-proxmox"
ssh_public_keys  = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDKW96r73C2mON+Yy9Oxi8BrFiOACz11mYsZYrgrdkC2 deployer@ubuntu-deploy"
]

# Options de démarrage
onboot = true

Déploiement des conteneurs

Depuis notre machine de déploiement, nous pouvons alors lancer les commandes suivantes :

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform init
deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform plan

#  pensez à taper yes pour accepter le déploiement

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform apply

Ces 3 commandes permettent respectivement :

  • d’initialiser un répertoire de travail Terraform
  • de créer un plan d’exécution
  • d’appliquer les changements dans notre infrastructure pour atteindre l’état désiré

Nous pouvons observer sur notre hôte Proxmox VE la création du nouveau conteneur ubuntu-lxc-1

Test de connexion au conteneur nouvellement créé

Depuis notre machine de déploiement nous pouvons nous connecter au conteneur créé.

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh root@192.168.1.201
The authenticity of host '192.168.1.201 (192.168.1.201)' can't be established.
ED25519 key fingerprint is SHA256:1F9S0v6CCmzemsy4SXWmn0dvdLkD/tXbS+EX7rwGZoE.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.201' (ED25519) to the list of known hosts.
Enter passphrase for key '/home/deployer/.ssh/id_ed25519': 
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.12-2-pve x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@ubuntu-lxc-1:~#

Destruction du conteneur

Pour détruire le conteneur créé à l’aide de Terraform et Proxmox VE, il suffit de faire :

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform destroy

Création de 5 conteneurs

Notre fichier main.tf est suffisamment flexible pour permettre la création de plusieurs conteneurs. Il suffit simplement de changer dans terraform.tfvars

# Nombre de conteneurs à créer
container_count = 5

Nous pouvons aussi profiter de ce changement pour explorer les outputs (sorties) de Terraform.

Il suffit de créer un fichier outputs.tf

# Sortie simple
output "container_count" {
  description = "Nombre de conteneurs déployés"
  value       = var.container_count
}

# Sortie avec liste d'IPs
output "container_ips" {
  description = "Liste des adresses IP des conteneurs"
  value = [
    for container in proxmox_virtual_environment_container.lxc_container :
    split("/", container.initialization[0].ip_config[0].ipv4[0].address)[0]
  ]
}

# Sortie avec informations détaillées
output "container_details" {
  description = "Détails de tous les conteneurs"
  value = {
    for idx, container in proxmox_virtual_environment_container.lxc_container :
    container.initialization[0].hostname => {
      id         = container.vm_id
      ip_address = split("/", container.initialization[0].ip_config[0].ipv4[0].address)[0]
      cores      = container.cpu[0].cores
      memory     = container.memory[0].dedicated
    }
  }
}

# Sortie sensible (masquée dans les logs)
output "sensitive_info" {
  description = "Informations sensibles des conteneurs"
  value       = "information_sensible"
  sensitive   = true
}

Redéployons… tf init apply plan …

Nous pouvons voir dans PVE la création des 5 conteneurs.

Remarque : il est également possible de passer la variable container_count via la console et modifier le nombre de conteneur déployé. Par exemple :

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform apply -var 'container_count=3'

Exploitation des sorties (outputs) de Terraform

Nous pouvons également observer les sorties de Terraform via

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform output 
container_count = 5
container_details = {
  "ubuntu-lxc-1" = {
    "cores" = 2
    "id" = 241201
    "ip_address" = "192.168.1.201"
    "memory" = 2048
  }
  "ubuntu-lxc-2" = {
    "cores" = 2
    "id" = 241202
    "ip_address" = "192.168.1.202"
    "memory" = 2048
  }
  "ubuntu-lxc-3" = {
    "cores" = 2
    "id" = 241203
    "ip_address" = "192.168.1.203"
    "memory" = 2048
  }
  "ubuntu-lxc-4" = {
    "cores" = 2
    "id" = 241204
    "ip_address" = "192.168.1.204"
    "memory" = 2048
  }
  "ubuntu-lxc-5" = {
    "cores" = 2
    "id" = 241205
    "ip_address" = "192.168.1.205"
    "memory" = 2048
  }
}
container_ips = [
  "192.168.1.201",
  "192.168.1.202",
  "192.168.1.203",
  "192.168.1.204",
  "192.168.1.205",
]
sensitive_info = <sensitive>

ou dans un format facilement lisible par une machine (comme le JSON)

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform output -json
{
  "container_count": {
    "sensitive": false,
    "type": "number",
    "value": 5
  },
  "container_details": {
    "sensitive": false,
    "type": [
      "object",
      {
        "ubuntu-lxc-1": [
          "object",
          {
            "cores": "number",
            "id": "number",
            "ip_address": "string",
            "memory": "number"
          }
        ],
        "ubuntu-lxc-2": [
          "object",
          {
            "cores": "number",
            "id": "number",
            "ip_address": "string",
            "memory": "number"
          }
        ],
        "ubuntu-lxc-3": [
          "object",
          {
            "cores": "number",
            "id": "number",
            "ip_address": "string",
            "memory": "number"
          }
        ],
        "ubuntu-lxc-4": [
          "object",
          {
            "cores": "number",
            "id": "number",
            "ip_address": "string",
            "memory": "number"
          }
        ],
        "ubuntu-lxc-5": [
          "object",
          {
            "cores": "number",
            "id": "number",
            "ip_address": "string",
            "memory": "number"
          }
        ]
      }
    ],
    "value": {
      "ubuntu-lxc-1": {
        "cores": 2,
        "id": 241201,
        "ip_address": "192.168.1.201",
        "memory": 2048
      },
      "ubuntu-lxc-2": {
        "cores": 2,
        "id": 241202,
        "ip_address": "192.168.1.202",
        "memory": 2048
      },
      "ubuntu-lxc-3": {
        "cores": 2,
        "id": 241203,
        "ip_address": "192.168.1.203",
        "memory": 2048
      },
      "ubuntu-lxc-4": {
        "cores": 2,
        "id": 241204,
        "ip_address": "192.168.1.204",
        "memory": 2048
      },
      "ubuntu-lxc-5": {
        "cores": 2,
        "id": 241205,
        "ip_address": "192.168.1.205",
        "memory": 2048
      }
    }
  },
  "container_ips": {
    "sensitive": false,
    "type": [
      "tuple",
      [
        "string",
        "string",
        "string",
        "string",
        "string"
      ]
    ],
    "value": [
      "192.168.1.201",
      "192.168.1.202",
      "192.168.1.203",
      "192.168.1.204",
      "192.168.1.205"
    ]
  },
  "sensitive_info": {
    "sensitive": true,
    "type": "string",
    "value": "information_sensible"
  }
}

Sauvegardons cela dans un fichier, cela pourra nous servir.

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ terraform output -json > outputs.json

A l’aide d’un outil comme jq (JSON queries) nous pouvons récupérer uniquement les adresses IP des interfaces réseau des conteneurs nouvellement créés. Il est d’abord nécessaire d’installer cet outil.

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ sudo apt install jq

On peut ensuite utiliser jq ainsi

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ jq -r '.container_ips.value | @csv' outputs.json > servers_ip.csv

ou

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ jq -r '[.container_details.value[].ip_address] | @csv' outputs.json > servers_ip.csv

On obtient alors un fichier servers_ip.csv contenant les adresses IP des interfaces réseau de nos 5 conteneurs nouvellement créés.

"192.168.1.201","192.168.1.202","192.168.1.203","192.168.1.204","192.168.1.205"

Test de connexion SSH à nos nouveaux conteneurs

Nous pouvons nous connecter aux différents conteneurs Essayons avec le n°241205 dont l’adresse IP est 192.168.1.205

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh root@192.168.1.205
Enter passphrase for key '/home/deployer/.ssh/id_ed25519': 
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.12-2-pve x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
root@ubuntu-lxc-5:~#

Cela fonctionne sans soucis.

Par contre nous allons avoir un problème avec le 201

root@ubuntu-lxc-5:~# exit
logout
Connection to 192.168.1.205 closed.
deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh root@192.168.1.201
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:7mIyi+Jnl46k4W0us8zM4gCyKZ/zvMu39KCszNmj1ck.
Please contact your system administrator.
Add correct host key in /home/deployer/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /home/deployer/.ssh/known_hosts:3
  remove with:
  ssh-keygen -f '/home/deployer/.ssh/known_hosts' -R '192.168.1.201'
Host key for 192.168.1.201 has changed and you have requested strict checking.
Host key verification failed.

Le message est clair… l’hôte a changé ! … oui nous avons détruit le conteneur et l’avons recréé. Il n’a plus la même empreinte.

Ici nous pouvons de manière sûre faire la commande demandé… à savoir

$ ssh-keygen -f '/home/deployer/.ssh/known_hosts' -R '192.168.1.201'

qui permet de retirer l’empreinte de 192.168.1.201 des hôtes connus.

deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh-keygen -f '/home/deployer/.ssh/known_hosts' -R '192.168.1.201'
# Host 192.168.1.201 found: line 1
# Host 192.168.1.201 found: line 2
# Host 192.168.1.201 found: line 3
/home/deployer/.ssh/known_hosts updated.
Original contents retained as /home/deployer/.ssh/known_hosts.old
deployer@ubuntu-deploy:~/homelab_julia_pve_tf_ansible$ ssh root@192.168.1.201
The authenticity of host '192.168.1.201 (192.168.1.201)' can't be established.
ED25519 key fingerprint is SHA256:7mIyi+Jnl46k4W0us8zM4gCyKZ/zvMu39KCszNmj1ck.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.201' (ED25519) to the list of known hosts.
Enter passphrase for key '/home/deployer/.ssh/id_ed25519': 
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.12-2-pve x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@ubuntu-lxc-1:~# 

La connexion SSH au conteneur LXC fonctionne à nouveau.

lxcterraformproxmoxve