OpenTofu — Cheat Sheet⚓︎
Référence rapide pour l'infrastructure as code avec OpenTofu (fork open-source de Terraform).
Concepts clés⚓︎
| Concept | Description |
|---|---|
| Provider | Plugin d'interaction avec une API (AWS, Proxmox, Docker…) |
| Resource | Objet d'infrastructure géré (resource "type" "nom") |
| Data source | Lecture d'une ressource existante non gérée par Tofu |
| Variable | Paramètre d'entrée du module |
| Output | Valeur exportée du module |
| Module | Ensemble de ressources réutilisables |
| State | Fichier de suivi de l'état de l'infra (terraform.tfstate) |
| Workspace | Isolation d'environnements (dev/staging/prod) |
| Backend | Stockage du state (local, S3, GitLab…) |
CLI — Commandes essentielles⚓︎
Cycle de vie de base⚓︎
tofu init # initialiser le répertoire (télécharger les providers)
tofu init -upgrade # mettre à jour les providers
tofu init -backend-config=backend.hcl # avec config backend externe
tofu validate # valider la syntaxe HCL
tofu fmt # formater les fichiers .tf
tofu fmt -recursive # récursif (sous-dossiers)
tofu fmt -check # vérifier sans modifier (CI)
tofu plan # calculer les changements (dry-run)
tofu plan -out=plan.tfplan # sauvegarder le plan
tofu plan -var="env=production" # avec variable
tofu plan -var-file=prod.tfvars # avec fichier de variables
tofu plan -target=resource.type.name # cibler une ressource spécifique
tofu plan -refresh=false # ne pas rafraîchir le state
tofu apply # appliquer les changements (confirmation demandée)
tofu apply -auto-approve # sans confirmation (CI/CD)
tofu apply plan.tfplan # appliquer un plan sauvegardé
tofu apply -replace=resource.type.name # forcer la recréation
tofu destroy # détruire toutes les ressources
tofu destroy -auto-approve # sans confirmation
tofu destroy -target=resource.type.name # détruire une ressource
Gestion du state⚓︎
tofu show # afficher le state lisiblement
tofu show plan.tfplan # afficher un plan sauvegardé
tofu state list # lister les ressources dans le state
tofu state show resource.type.nom # détails d'une ressource
tofu state mv src dst # renommer/déplacer une ressource dans le state
tofu state rm resource.type.nom # retirer du state (sans détruire)
tofu state pull # afficher le state distant brut
tofu state push state.tfstate # pousser un state local
tofu import resource.type.nom id # importer une ressource existante
tofu refresh # synchroniser le state avec la réalité
tofu output # afficher tous les outputs
tofu output nom_output # output spécifique
tofu output -json # format JSON
Workspaces⚓︎
tofu workspace list # lister les workspaces
tofu workspace new staging # créer un workspace
tofu workspace select production # changer de workspace
tofu workspace show # workspace actuel
tofu workspace delete staging # supprimer (doit être vide)
Structure de projet⚓︎
infra/
├── main.tf # ressources principales
├── variables.tf # déclarations des variables
├── outputs.tf # déclarations des outputs
├── providers.tf # configuration des providers
├── versions.tf # contraintes de version
├── terraform.tfvars # valeurs des variables (ne pas committer si secrets)
├── dev.tfvars # variables pour dev
├── prod.tfvars # variables pour prod
└── modules/
├── lxc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── docker-stack/
├── main.tf
├── variables.tf
└── outputs.tf
Syntaxe HCL⚓︎
Providers⚓︎
# versions.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.50"
}
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
backend "http" {
address = "https://gitlab.massivedynamics.be/api/v4/projects/1/terraform/state/homelab"
lock_address = "https://gitlab.massivedynamics.be/api/v4/projects/1/terraform/state/homelab/lock"
unlock_address = "https://gitlab.massivedynamics.be/api/v4/projects/1/terraform/state/homelab/lock"
lock_method = "POST"
unlock_method = "DELETE"
username = "tofu"
password = var.gitlab_token
retry_wait_min = 5
}
}
# providers.tf
provider "proxmox" {
endpoint = "https://proxmox.mkhome.internal:8006"
username = var.proxmox_user
password = var.proxmox_password
insecure = false
}
Variables⚓︎
# variables.tf
variable "environment" {
type = string
description = "Environnement de déploiement"
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "L'environnement doit être dev, staging ou prod."
}
}
variable "vm_count" {
type = number
default = 1
}
variable "tags" {
type = list(string)
default = []
}
variable "config" {
type = object({
cpu = number
memory = number
disk = string
})
default = {
cpu = 2
memory = 2048
disk = "32G"
}
}
variable "db_password" {
type = string
sensitive = true # masqué dans les logs
}
Ressources⚓︎
# Resource basique
resource "docker_container" "nginx" {
name = "nginx"
image = docker_image.nginx.image_id
ports {
internal = 80
external = 8080
}
volumes {
container_path = "/etc/nginx/conf.d"
host_path = "/opt/nginx/conf.d"
read_only = true
}
labels {
label = "traefik.enable"
value = "true"
}
restart = "unless-stopped"
}
# LXC sur Proxmox (provider bpg/proxmox)
resource "proxmox_virtual_environment_container" "app" {
node_name = "pve"
vm_id = 200
description = "App LXC - ${var.environment}"
tags = var.tags
initialization {
hostname = "lxc-app-${var.environment}"
ip_config {
ipv4 {
address = "10.0.0.20/24"
gateway = "10.0.0.1"
}
}
user_account {
keys = [file("~/.ssh/id_ed25519.pub")]
}
}
cpu {
cores = var.config.cpu
}
memory {
dedicated = var.config.memory
}
disk {
datastore_id = "local-lvm"
size = 32
}
network_interface {
name = "eth0"
bridge = "vmbr0"
}
operating_system {
template_file_id = "local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst"
type = "debian"
}
started = true
unprivileged = true
}
Data sources⚓︎
# Lire une ressource existante (non gérée par Tofu)
data "proxmox_virtual_environment_nodes" "nodes" {}
data "docker_image" "nginx" {
name = "nginx:latest"
}
# Utiliser dans une ressource
resource "docker_container" "nginx" {
image = data.docker_image.nginx.image_id
name = "nginx"
}
Outputs⚓︎
# outputs.tf
output "container_ip" {
value = proxmox_virtual_environment_container.app.initialization[0].ip_config[0].ipv4[0].address
description = "Adresse IP du conteneur LXC"
}
output "container_id" {
value = proxmox_virtual_environment_container.app.vm_id
}
output "db_connection" {
value = "postgres://user:${var.db_password}@${resource.db.host}:5432/app"
sensitive = true # masqué dans tofu output
}
Meta-arguments et fonctions⚓︎
Meta-arguments⚓︎
# count — créer N instances
resource "proxmox_virtual_environment_container" "worker" {
count = var.vm_count
vm_id = 300 + count.index
initialization {
hostname = "worker-${count.index + 1}"
}
}
# for_each — itérer sur une map/set
variable "services" {
default = {
nginx = { port = 80 }
redis = { port = 6379 }
}
}
resource "docker_container" "service" {
for_each = var.services
name = each.key
ports {
internal = each.value.port
external = each.value.port
}
}
# depends_on — dépendance explicite
resource "docker_container" "app" {
depends_on = [docker_container.db]
}
# lifecycle — contrôler la gestion du cycle de vie
resource "docker_container" "app" {
lifecycle {
prevent_destroy = true # empêcher la destruction accidentelle
ignore_changes = [image] # ignorer les changements sur image
replace_triggered_by = [var.config] # recréer si cette valeur change
}
}
Fonctions courantes⚓︎
# Chaînes
"${var.env}-server" # interpolation
format("server-%03d", count.index) # formatage
upper("hello") # "HELLO"
lower("HELLO") # "hello"
replace("hello world", " ", "-") # "hello-world"
split(",", "a,b,c") # ["a", "b", "c"]
join("-", ["a", "b", "c"]) # "a-b-c"
trimspace(" hello ") # "hello"
# Collections
length(var.tags) # longueur
contains(var.tags, "prod") # appartenance
concat(list1, list2) # fusionner des listes
flatten([[1, 2], [3]]) # [1, 2, 3]
toset(["a", "b", "a"]) # {"a", "b"}
tomap({a = 1, b = 2})
# Conditions
condition ? valeur_si_vrai : valeur_si_faux
var.env == "prod" ? "t3.large" : "t3.micro"
# Fichiers et encodage
file("~/.ssh/id_ed25519.pub") # lire un fichier
filebase64("cert.pem") # en base64
base64encode("string")
jsonencode({key = "value"}) # sérialiser en JSON
yamlencode({key = "value"}) # sérialiser en YAML
# Réseau
cidrhost("10.0.0.0/24", 10) # "10.0.0.10"
cidrsubnet("10.0.0.0/16", 8, 1) # "10.0.1.0/24"
Modules⚓︎
Créer un module⚓︎
# modules/lxc/variables.tf
variable "hostname" { type = string }
variable "ip_address" { type = string }
variable "cpu" { type = number; default = 2 }
variable "memory" { type = number; default = 2048 }
# modules/lxc/main.tf
resource "proxmox_virtual_environment_container" "this" {
# ...
}
# modules/lxc/outputs.tf
output "vm_id" {
value = proxmox_virtual_environment_container.this.vm_id
}
Appeler un module⚓︎
# main.tf
module "nginx_lxc" {
source = "./modules/lxc"
hostname = "lxc-nginx"
ip_address = "10.0.0.10/24"
cpu = 2
memory = 1024
}
module "db_lxc" {
source = "app.terraform.io/org/lxc/proxmox" # module distant (registry)
version = "~> 1.0"
hostname = "lxc-db"
ip_address = "10.0.0.11/24"
}
# Utiliser les outputs d'un module
output "nginx_id" {
value = module.nginx_lxc.vm_id
}
Backends — Stockage du state⚓︎
GitLab HTTP Backend⚓︎
terraform {
backend "http" {
address = "https://gitlab.massivedynamics.be/api/v4/projects/PROJECT_ID/terraform/state/STATE_NAME"
lock_address = "https://gitlab.massivedynamics.be/api/v4/projects/PROJECT_ID/terraform/state/STATE_NAME/lock"
unlock_address = "https://gitlab.massivedynamics.be/api/v4/projects/PROJECT_ID/terraform/state/STATE_NAME/lock"
lock_method = "POST"
unlock_method = "DELETE"
username = "tofu"
password = var.gitlab_token # ou TF_HTTP_PASSWORD en env var
retry_wait_min = 5
}
}
S3-compatible (Minio, AWS)⚓︎
terraform {
backend "s3" {
bucket = "tofu-state"
key = "homelab/terraform.tfstate"
region = "us-east-1"
endpoint = "https://minio.massivedynamics.be"
access_key = var.minio_access_key
secret_key = var.minio_secret_key
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
}
}
Bonnes pratiques⚓︎
- Ne jamais committer
terraform.tfstateouterraform.tfvarscontenant des secrets dans Git. - Utiliser un backend distant (GitLab, S3/Minio) pour partager le state entre collaborateurs.
- Toujours exécuter
tofu planet le relire avanttofu apply. - Marquer les variables sensibles avec
sensitive = true. - Utiliser
prevent_destroy = truesur les ressources critiques (bases de données, volumes). - Versionner les providers avec
~>(patch flexible) plutôt que de fixer exactement.
# .gitignore recommandé
*.tfstate
*.tfstate.backup
.terraform/
*.tfvars # si contient des secrets
*.tfplan
.terraform.lock.hcl # à committer si reproductibilité souhaitée
# Variables sensibles via variables d'environnement
# TF_VAR_db_password=secret tofu apply
# ou dans un fichier non commité :
# echo 'db_password = "secret"' > secrets.auto.tfvars