OpenTofu : provisionner ses LXC Proxmox comme en production
Un répertoire par LXC, un provider Proxmox communautaire, et un provisioner qui enchaîne automatiquement sur Ansible, voici comment OpenTofu transforme la création d'un conteneur en opération reproductible et versionnée.
Dans l'article précédent, je décrivais l'approche IaC en deux couches. OpenTofu est la première : c'est lui qui crée les conteneurs LXC sur Proxmox avant qu'Ansible ne prenne le relais. Voici comment ça s'articule concrètement.
Pourquoi OpenTofu plutôt que Terraform
OpenTofu est un fork open source de Terraform, né après le changement de licence de HashiCorp en 2023. La syntaxe est identique, les providers sont compatibles, mais la gouvernance est communautaire et la licence reste vraiment libre. Pour un homelab dont l'un des principes est l'indépendance vis-à-vis des éditeurs, le choix s'imposait.
Une structure simple : un répertoire par LXC
Chaque conteneur LXC a son propre répertoire OpenTofu :
opentofu/
├── infra-core/
│ ├── provider.tf
│ ├── main.tf
│ ├── variables.tf
│ └── secret.tfvars ← gitignored
├── home-automation/
├── media-server/
└── web-server/
Pas de mutualisation forcée, pas de module complexe. Chaque répertoire est autonome et peut être appliqué indépendamment. C'est volontairement simple.
Le provider Proxmox
OpenTofu dialogue avec l'API Proxmox via le provider communautaire bpg/proxmox. C'est aujourd'hui le provider le plus complet et le plus maintenu pour Proxmox, il supporte aussi bien les VM que les conteneurs LXC, la gestion du stockage, des réseaux, et des permissions.
La connexion au cluster se configure une fois dans provider.tf : l'URL de l'API, l'utilisateur, et le mot de passe (ce dernier injecté via variable pour ne jamais apparaître en clair dans le code).
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "0.90.0"
}
}
}
provider "proxmox" {
endpoint = "https://192.168.1.110:8006/"
api_token = var.proxmox_t4_deploy_token
insecure = true
}
Le mot de passe est injecté via variable, jamais en clair dans le code.
Ce que décrit main.tf
Le fichier main.tf décrit entièrement le conteneur LXC :
resource "proxmox_virtual_environment_container" "infra_core" {
node_name = "srv-pve-1"
vm_id = 100
unprivileged = true
operating_system {
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
type = "debian"
}
initialization {
hostname = "srv-infra-core"
dns {
servers = ["1.168.1.120", "1.1.1.1"]
}
ip_config {
ipv4 {
address = "192.168.1.120/24"
gateway = "192.168.1.1"
}
}
user_account {
keys = [trimspace(file("~/.ssh/id_rsa.pub"))]
}
}
network_interface {
name = "eth0"
bridge = "vmbr0"
}
cpu {
cores = 2
}
memory {
dedicated = 2048
}
disk {
datastore_id = "local-zfs"
size = 10
}
features {
nesting = true
keyctl = true
}
mount_point {
volume = "/mnt/public"
path = "/mnt/public"
}
mount_point {
volume = "/mnt/backup"
path = "/mnt/backup"
}
provisioner "local-exec" {
// ...
}La feature nesting est indispensable : elle autorise Docker à tourner à l'intérieur du conteneur LXC. Les points de montage NAS sont déclarés directement ici, pas besoin de configuration supplémentaire côté Ansible.
Le vrai intérêt : le provisioner local-exec
C'est là que la chaîne IaC devient intéressante. À la fin du main.tf, un provisioner local-exec s'exécute automatiquement après la création du LXC. Il fait trois choses dans l'ordre :
- Attend que le SSH soit disponible sur le nouveau conteneur
- Lance le playbook
bootstrap.ymlpour créer l'utilisateur Ansible et déposer la clé SSH - Lance le playbook complet du service pour configurer tout ce qui doit tourner dessus
Un seul tofu apply crée le LXC et le configure entièrement. Le LXC est prêt à l'emploi à la fin de la commande.
provisioner "local-exec" {
command = <<EOT
# On nettoie l'hôte connu
ssh-keygen -f '/home/ubuntu/.ssh/known_hosts' -R '192.168.1.120' || true
# On attend que le SSH réponde vraiment
echo "Waiting for SSH on 192.168.1.120..."
until nc -z -v -w5 192.168.1.120 22; do
echo "SSH not available, waiting 2 seconds..."
sleep 2
done
export ANSIBLE_HOST_KEY_CHECKING=False
export ANSIBLE_SSH_ARGS="-o ControlMaster=no -o ControlPersist=no -o PreferredAuthentications=publickey"
ansible-playbook -i ${path.module}/../../ansible/hosts ${path.module}/../../ansible/bootstrap.yml --limit "infra-core" \
--vault-password-file ~/.vault -u root -e 'ansible_user=root' || exit 1
ansible-playbook -i ${path.module}/../../ansible/hosts ${path.module}/../../ansible/infra-core.yml \
--vault-password-file ~/.vault
EOT
}
Les secrets : secret.tfvars
Les credentials Proxmox (mot de passe root, tokens API) ne vivent pas dans le code. Ils sont isolés dans un fichier secret.tfvars exclu du dépôt Git. Le Makefile s'assure qu'ils sont toujours passés à la commande via -var-file="secret.tfvars", sans avoir à s'en souvenir.
# variables.tf
variable "proxmox_root_password" {
description = "Mot de passe de l'utilisateur root@pam"
type = string
sensitive = true
}
Le Makefile, que nous avons évoqué dans un article précédent, s'assure qu'il est toujours passé via -var-file="secret.tfvars".