De nos jours, l'automatisation des tâches et la création d'infrastructures immuables sont des aspects incontournables. C'est dans cette optique que l'utilisation d'outils comme Terraform ou son alternative libre OpenTofu deviennent particulièrement intéressants pour la création automatique et en masse de conteneurs LXC.
Depuis 2025, j'utilise OpenTofu qui est compatible avec les commandes Terraform. Changez les commandes "terraform" par "tofu". L'article a été mis à jour le 24/01/2025 suite au live sur Twitch de TheRealSeboss666 (blog : https://blog.seboss666.info).
L'idée sous-jacente à cette approche est de déployer des conteneurs pour isoler et héberger différents services, tout en évitant l'usage habituel de Docker ou de Podman. Outre l'aspect sécuritaire qu'offre cette méthode, elle vise également à explorer et à exploiter pleinement les outils disponibles.
Ce document part du principe que vous maîtrisez déjà Terraform, que vous disposez des droits d'administration sur Proxmox et que vous êtes capable d'effectuer des manipulations sans contraintes majeures.
Plutôt que de réinventer la roue, je vous suggère l'article de Quentin, "au final, qu'est-ce qu'un conteneur ?", pour découvrir ce qu'est un conteneur Linux.
L'environnement de travail pour cet article est le suivant :
- un serveur Proxmox 8.3.x ;
- OpenTofu 1.9.x ;
- un conteneur LXC, avec Debian.
Préparation du serveur Proxmox
Pour simplifier les opérations dans vos environnements de test, vous pouvez utiliser le compte root@pam
créé lors de l'installation de Proxmox. Ce compte possède tous les privilèges nécessaires pour effectuer des actions sur la plateforme.
Par la suite, je vais créer un utilisateur nommé "terrabot" dans le realm "pve". Ce compte aura un rôle spécifique, doté des droits nécessaires pour la création et la gestion des conteneurs, des machines virtuelles et des disques virtuels.
Création d'un rôle
La première étape est la création d'un rôle. Ce rôle disposera des droits nécessaires pour gérer les machines et leur stockage. Voici donc tous les rôles nécessaires :
"Datastore.Allocate",
"Datastore.AllocateSpace",
"Datastore.AllocateTemplate",
"Datastore.Audit",
"Pool.Allocate",
"Pool.Audit",
"SDN.Allocate",
"SDN.Audit",
"SDN.Use",
"Sys.Audit",
"Sys.Console",
"VM.Allocate",
"VM.Audit",
"VM.Backup",
"VM.Clone",
"VM.Config.CDROM",
"VM.Config.CPU",
"VM.Config.Cloudinit",
"VM.Config.Disk",
"VM.Config.HWType",
"VM.Config.Memory",
"VM.Config.Network",
"VM.Config.Options",
"VM.Console",
"VM.Migrate",
"VM.Monitor",
"VM.PowerMgmt",
"VM.Snapshot",
"VM.Snapshot.Rollback",
"Mapping.Use",
"Mappping.Audit",
"Mapping.Modify",
"Realm.AllocateUser",
"Permissions.Modify"

Je n'ai pas utilisé Terraform pour créer ce rôle, puisque Terraform a besoin de ce rôle avant de créer des ressources... Terraform-ception. Une autre solution plus rapide est d'utiliser le rôle "PVEAdmin" et de le propager à la racine.
Création d'un utilisateur
Maintenant, créons l'utilisateur "terrabot" ; cet utilisateur sera utilisé exclusivement pour Terraform. Dirigez-vous dans l'onglet "Permissions", puis sur "Users", et cliquez sur le bouton "Create". Créez un nouvel utilisateur dans le realm "pve", ajoutez des informations selon votre besoin et validez les changements.
Par la suite, ajoutez un mot de passe complexe à cet utilisateur (vous ne l'utiliserez probablement pas, mais ajoutez quand même un mot de passe) et validez ces changements.
Création d'un token pour l'utilisateur
Avant-dernière étape, créons un token pour cet utilisateur fraichement créé. Toujours dans l'onglet "Permissions", cliquez sur le lien "API Token" et cliquez sur le bouton "Add". Saisissez l'utilisateur et le nom du token. Ce nom est important, vous en aurez besoin plus loin dans le code Terraform. Ici, j'ai appelé ce token "tf". J'ai décoché la case "Privileges separations" et n'ai pas mis de date d'expiration pour le token. Dans des environnements de production, il est fortement conseillé de renouveler les tokens régulièrement.
Attribution du rôle
Nous sommes au virage final. Attribuons désormais le rôle à notre utilisateur et plus particulièrement au token. Retournez une dernière fois dans l'onglet "Permissions" et sélectionnez "API User token". Choisissez l'utilisateur créé (ici Terrabot) et sélectionnez le rôle "Terraform". Validez les changements et répétez l'opération avec l'utilisateur précédemment créé (dans notre exemple, "terrabot"). Vous allez ainsi avoir dans la fenêtre "Permissions" deux lignes - IMAGE À METTRE.
Le code Terraform complet
Enfin, voici le code complet que j'utilise pour créer des conteneurs sur Proxmox avec Terraform. Le code est assez complet, j'aime bien avoir le maximum d'informations et d'attributs pour préciser au maximum mes ressources.
Fichier "variables.tf" :
### variables.tf
variable "ct_bridge" {
type = string
}
variable "ct_datastore_storage_location" {
type = string
}
variable "ct_datastore_template_location" {
type = string
}
variable "ct_description" {
type = string
}
variable "ct_disk_size" {
type = string
default = "20"
}
variable "ct_id" {
type = number
}
variable "ct_ip" {
type = string
}
variable "ct_name" {
type = string
}
variable "ct_nic_rate_limit" {
type = number
}
variable "ct_memory" {
type = number
}
variable "ct_source_file_path" {
type = string
}
variable "ct_swap" {
type = number
}
variable "ct_tags" {
type = list(string)
}
variable "dns_domain" {
type = string
}
variable "dns_servers" {
type = list(string)
}
variable "gateway" {
type = string
}
variable "node" {
type = string
}
variable "os_type" {
type = string
}
variable "pve_api_token" {
type = string
}
variable "pve_api_user" {
type = string
}
variable "pve_host_address" {
type = string
}
variable "ssh_pubkey" {
type = list(string)
}
variable "tmp_dir" {
type = string
}
Le fichier "variables.auto.tfvars", est à modifier pour que vous puissiez avoir un conteneur correspondant à votre besoin :
### variables.auto.tfvars
ct_bridge = "vmbr0"
ct_datastore_storage_location = "local"
ct_datastore_template_location = "local"
ct_description = "Managed by OpenTofu. Change me :)"
ct_disk_size = "20"
ct_id = 9999123
ct_ip = "192.168.1.3/24"
ct_name = "ct-test"
ct_nic_rate_limit = 100
ct_memory = 512
ct_source_file_path = "http://download.proxmox.com/images/system/debian-12-standard_12.7-1_amd64.tar.zst"
ct_swap = 0
ct_tags = ["linux", "infra"]
dns_domain = "domain.net"
dns_servers = ["192.168.1.1"]
gateway = "192.168.1.1"
node = "pve01"
os_type = "debian"
pve_api_token = "terrabot@pve!tf=change-my-token"
pve_api_user = "terrabot"
pve_host_address = "https://192.168.1.2:8006"
ssh_pubkey = ["changeme"]
tmp_dir = "/var/tmp"
Fichier "container.tf" :
### container.tf
# see https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password.html. It will download "hashicorp/random" provider
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.70"
}
random = {
source = "hashicorp/random"
version = "3.6.3"
}
}
}
provider "proxmox" {
endpoint = var.pve_host_address
api_token = var.pve_api_token
insecure = true
ssh {
agent = true
username = var.pve_api_user
}
tmp_dir = var.tmp_dir
}
resource "random_password" "container_root_password" {
length = 24
override_special = "_%@"
special = true
}
output "container_root_password" {
value = random_password.container_root_password.result
sensitive = true
}
# location of containers templates
resource "proxmox_virtual_environment_file" "debian_container_template" {
content_type = "vztmpl"
datastore_id = var.ct_datastore_template_location
node_name = var.node
source_file {
path = var.ct_source_file_path
}
}
resource "proxmox_virtual_environment_container" "debian_container" {
description = var.ct_description
node_name = var.node
start_on_boot = true
tags = var.ct_tags
unprivileged = true
vm_id = var.ct_id
cpu {
architecture = "amd64"
cores = 1
}
disk {
datastore_id = var.ct_datastore_storage_location
size = var.ct_disk_size
}
memory {
dedicated = var.ct_memory
swap = var.ct_swap
}
operating_system {
template_file_id = proxmox_virtual_environment_file.debian_container_template.id
type = var.os_type
}
initialization {
hostname = var.ct_name
dns {
domain = var.dns_domain
server = var.dns_server
}
ip_config {
ipv4 {
address = var.ct_ip
gateway = var.gateway
}
}
user_account {
keys = var.ssh_pubkey
password = random_password.container_root_password.result
}
}
network_interface {
name = var.ct_bridge
rate_limit = var.ct_nic_rate_limit
}
features {
nesting = true
fuse = false
}
}
Veillez à bien changer les valeurs dans le fichier "variables.auto.tfvars" pour correspondre à votre environnement. L'axe d'amélioration serait d'utiliser cette base de fichiers et en créer un module, permettant d'unifier le code pour plusieurs ressources et itérer en fonction du nombre de conteneurs à créer.