Aller au contenu
Infrastructure as Code medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

Backends Terraform : stocker le state à distance

24 min de lecture

logo terraform

Par défaut, Terraform stocke le state dans un fichier local (terraform.tfstate). C’est suffisant pour apprendre, mais en équipe ou en production, un backend distant permet de partager le state, de le sauvegarder automatiquement et d’éviter les modifications concurrentes. Ce guide vous montre comment passer d’un backend local à un backend S3 compatible (MinIO), en créant votre propre serveur de stockage dans une VM KVM.

Prérequis : avoir compris le rôle du state Terraform et savoir créer une VM avec cloud-init (Première infrastructure).

  • Ce qu’est un backend et pourquoi le backend local ne suffit pas en équipe
  • Les principaux types de backends (local, S3, consul, pg…)
  • Déployer MinIO (stockage S3 compatible) dans une VM KVM avec cloud-init
  • Configurer le backend S3 dans Terraform pour pointer vers MinIO
  • Migrer un state existant du backend local vers S3
  • Vérifier que le state est bien stocké à distance

Le backend est le composant de Terraform qui décide et comment le state est stocké. Il définit deux responsabilités :

  1. Le stockage : où le fichier terraform.tfstate est sauvegardé (disque local, serveur distant, bucket S3…)
  2. Le verrouillage : empêcher deux terraform apply simultanés d’écrire dans le même state

Par défaut, Terraform utilise le backend local : le state est un fichier dans votre dossier de projet. Aucune configuration n’est nécessaire.

Le backend local fonctionne bien pour un développeur seul sur sa machine. En équipe ou en production, il pose trois problèmes :

ProblèmeConséquence
Pas de partageChaque développeur a son propre state — les modifications de l’un sont invisibles pour l’autre
Pas de sauvegardeSi le disque tombe en panne, le state est perdu
Risque de conflitDeux apply simultanés corrompent le state

Terraform supporte plusieurs backends. Voici les plus courants :

BackendStockageVerrouillageCas d’usage
localFichier sur disqueOui (verrou fichier)Développement solo, apprentissage
s3Bucket S3 (AWS ou compatible)Oui (natif ou DynamoDB)Équipes, production, MinIO on-premise
consulConsul KV storeOui (natif)Environnements HashiCorp
pgBase PostgreSQLOui (natif)Infra existante avec PostgreSQL
gcsGoogle Cloud StorageOui (natif)Projets GCP
azurermAzure Blob StorageOui (natif)Projets Azure

Pour tester un backend S3 sans compte cloud, vous allez créer une VM qui héberge MinIO. Cloud-init installe et configure MinIO automatiquement au premier démarrage.

Le fichier cloud-init installe MinIO, crée un service systemd et prépare un bucket pour le state :

#cloud-config
users:
- name: terraform
sudo: ALL=(ALL) NOPASSWD:ALL
groups: sudo
shell: /bin/bash
ssh_authorized_keys:
- ${ssh_public_key}
package_update: true
packages:
- qemu-guest-agent
- wget
runcmd:
- systemctl enable --now qemu-guest-agent
# Télécharger et installer MinIO
- wget -q https://dl.min.io/server/minio/release/linux-amd64/minio -O /usr/local/bin/minio
- chmod +x /usr/local/bin/minio
# Créer l'utilisateur et le répertoire de stockage
- useradd -r -s /sbin/nologin minio-user
- mkdir -p /data/minio
- chown minio-user:minio-user /data/minio
# Configurer MinIO via un fichier d'environnement
- |
cat > /etc/default/minio << 'EOF'
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
MINIO_VOLUMES="/data/minio"
MINIO_OPTS="--console-address :9001"
EOF
# Créer le service systemd
- |
cat > /etc/systemd/system/minio.service << 'SVCEOF'
[Unit]
Description=MinIO Object Storage
After=network-online.target
Wants=network-online.target
[Service]
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_VOLUMES $MINIO_OPTS
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SVCEOF
- systemctl daemon-reload
- systemctl enable --now minio.service
# Installer le client mc et créer le bucket
- wget -q https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc
- chmod +x /usr/local/bin/mc
- sleep 5
- /usr/local/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin
- /usr/local/bin/mc mb local/terraform-state --with-versioning

Ce cloud-init réalise cinq opérations : installation du binaire MinIO, création d’un utilisateur système dédié, écriture d’un service systemd, installation du client mc (MinIO Client), et création du bucket terraform-state avec versioning activé pour recevoir le state.

Exemple de configuration plus propre côté shell :

Fenêtre de terminal
export AWS_ACCESS_KEY_ID="terraform-backend"
export AWS_SECRET_ACCESS_KEY="mot-de-passe-fort"
terraform init

Dans ce cas, supprimez access_key et secret_key du bloc backend pour éviter de laisser des secrets dans versions.tf.

Utilisez cette configuration pour déployer la VM MinIO avec un réseau statique :

# variables.tf — paramètres de la VM
variable "vm_name" {
description = "Nom de la VM MinIO"
type = string
default = "minio-state"
}
variable "base_image" {
description = "Chemin vers l'image de base Ubuntu"
type = string
default = "/chemin/vers/ubuntu-24.04-cloudimg.img"
}
variable "ssh_public_key" {
description = "Chemin vers la clé publique SSH"
type = string
default = "~/.ssh/id_ed25519.pub"
}
# main.tf — VM MinIO avec cloud-init
resource "libvirt_volume" "disk" {
name = "${var.vm_name}.qcow2"
pool = "default"
target = { format = { type = "qcow2" } }
create = { content = { url = var.base_image } }
}
resource "libvirt_cloudinit_disk" "init" {
name = "${var.vm_name}-cloud-init.iso"
user_data = templatefile("${path.module}/cloud-init.cfg", {
ssh_public_key = trimspace(file(pathexpand(var.ssh_public_key)))
})
network_config = file("${path.module}/network-config.cfg")
meta_data = jsonencode({
instance-id = var.vm_name
local-hostname = var.vm_name
})
}
resource "libvirt_domain" "vm" {
name = var.vm_name
type = "kvm"
memory = 1024
memory_unit = "MiB"
vcpu = 1
os = { type = "hvm", type_arch = "x86_64", type_machine = "q35" }
devices = {
disks = [
{
source = { file = { file = libvirt_volume.disk.path } }
target = { dev = "vda", bus = "virtio" }
driver = { name = "qemu", type = "qcow2" }
},
{
device = "cdrom"
driver = { name = "qemu", type = "raw" }
source = { file = { file = libvirt_cloudinit_disk.init.path } }
target = { dev = "sda", bus = "sata" }
read_only = true
}
]
interfaces = [{
model = { type = "virtio" }
source = { network = { network = "default" } }
}]
}
}

La configuration réseau attribue une IP statique à la VM pour que le endpoint MinIO soit prévisible :

# network-config.cfg
version: 2
ethernets:
enp1s0:
dhcp4: false
addresses: [192.168.122.50/24]
routes: [{to: default, via: 192.168.122.1}]
nameservers: {addresses: [192.168.122.1, 8.8.8.8]}
  1. Déployez la VM

    Fenêtre de terminal
    terraform init
    terraform apply -auto-approve

    Terraform crée le volume, l’image cloud-init et la VM. Résultat attendu :

    Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
    Outputs:
    minio_endpoint = "http://192.168.122.50:9000"
    minio_console = "http://192.168.122.50:9001"
    vm_name = "minio-state"
  2. Attendez la fin de cloud-init

    Cloud-init télécharge MinIO et crée le bucket. Patientez 1 à 2 minutes puis vérifiez :

    Fenêtre de terminal
    ssh terraform@192.168.122.50 "cloud-init status"
    status: done
  3. Vérifiez que MinIO fonctionne

    Fenêtre de terminal
    ssh terraform@192.168.122.50 "systemctl status minio --no-pager | head -6"
    ● minio.service - MinIO Object Storage
    Loaded: loaded (/etc/systemd/system/minio.service; enabled; preset: enabled)
    Active: active (running) since Tue 2026-03-31 11:51:09 UTC; 20s ago
    Main PID: 3210 (minio)
  4. Vérifiez que le bucket existe

    Fenêtre de terminal
    ssh terraform@192.168.122.50 "sudo mc alias set local http://127.0.0.1:9000 minioadmin minioadmin && sudo mc ls local/"
    Added `local` successfully.
    [2026-03-31 11:51:19 UTC] 0B terraform-state/

Créez un nouveau projet Terraform (ou modifiez un existant) pour utiliser le backend S3.

Le bloc backend se place dans le bloc terraform {}, avant les required_providers.

Commencez par exporter les identifiants dans votre shell :

Fenêtre de terminal
export AWS_ACCESS_KEY_ID="minioadmin"
export AWS_SECRET_ACCESS_KEY="minioadmin"

Ensuite, utilisez un bloc backend sans secrets en clair :

# versions.tf — avec backend S3 pointant vers MinIO
terraform {
required_version = ">= 1.11.0"
backend "s3" {
# Nom du bucket créé par cloud-init
bucket = "terraform-state"
# Chemin du state dans le bucket (organisez par projet)
key = "demo/terraform.tfstate"
# Région obligatoire, mais ignorée par MinIO
region = "us-east-1"
# Endpoint MinIO (remplace le endpoint AWS par défaut)
endpoints = {
s3 = "http://192.168.122.50:9000"
}
# Désactiver les vérifications spécifiques à AWS
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
use_path_style = true
skip_region_validation = true
}
required_providers {
libvirt = {
source = "dmacvicar/libvirt"
version = "~> 0.8"
}
}
}
provider "libvirt" {
uri = "qemu:///system"
}

Chaque paramètre a un rôle précis :

ParamètreRôle
bucketNom du bucket S3 qui contient le state
keyChemin du fichier state dans le bucket (permet plusieurs projets dans un bucket)
regionObligatoire pour le provider S3, mais ignoré par MinIO
endpoints.s3URL du serveur S3 (MinIO au lieu d’AWS)
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYIdentifiants lus depuis l’environnement au moment de terraform init
skip_credentials_validationDésactive la vérification des identifiants via l’API AWS IAM
skip_metadata_api_checkDésactive l’appel au service de métadonnées EC2
skip_requesting_account_idDésactive la résolution de l’ID de compte AWS
use_path_styleUtilise le format endpoint/bucket/key au lieu de bucket.endpoint/key
  1. Initialisez le projet

    Fenêtre de terminal
    terraform init

    Le message confirme la connexion au backend S3 :

    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.
  2. Créez une ressource de test

    main.tf
    resource "libvirt_volume" "test" {
    name = "backend-s3-demo.qcow2"
    pool = "default"
    target = { format = { type = "qcow2" } }
    create = { content = { url = "/chemin/vers/ubuntu-24.04-cloudimg.img" } }
    }
    output "volume_name" {
    value = libvirt_volume.test.name
    }
  3. Appliquez

    Fenêtre de terminal
    terraform apply -auto-approve
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    Outputs:
    volume_name = "backend-s3-demo.qcow2"
    volume_path = "/var/lib/libvirt/images/backend-s3-demo.qcow2"
  4. Vérifiez : pas de state local

    Fenêtre de terminal
    ls terraform.tfstate 2>&1
    ls: cannot access 'terraform.tfstate': No such file or directory

    Le state n’est plus sur votre disque — il est dans MinIO.

  5. Vérifiez : state dans MinIO

    Fenêtre de terminal
    ssh terraform@192.168.122.50 "sudo mc ls --recursive local/terraform-state/"
    [2026-03-31 11:52:26 UTC] 1.9KiB STANDARD demo/terraform.tfstate

Le state stocké dans MinIO est un fichier JSON identique à celui du backend local :

Fenêtre de terminal
ssh terraform@192.168.122.50 \
"sudo mc cat local/terraform-state/demo/terraform.tfstate" | python3 -m json.tool
{
"version": 4,
"terraform_version": "1.14.3",
"serial": 1,
"lineage": "12376cd7-123c-5134-1737-b49bcd0aaafd",
"outputs": {
"volume_name": {
"value": "backend-s3-demo.qcow2",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "libvirt_volume",
"name": "test",
"provider": "provider[\"registry.terraform.io/dmacvicar/libvirt\"]",
"instances": [
{
"attributes": {
"name": "backend-s3-demo.qcow2",
"path": "/var/lib/libvirt/images/backend-s3-demo.qcow2",
"pool": "default"
}
}
]
}
]
}

Comme vous pouvez le constater, la structure est identique au state local décrit dans Comprendre le state : même version, même serial, même lineage. Seul l’emplacement physique du fichier a changé.

Vous avez probablement des projets Terraform qui utilisent déjà le backend local. Terraform propose une commande de migration intégrée.

Vous avez un projet fonctionnel avec un terraform.tfstate local :

Fenêtre de terminal
ls terraform.tfstate
-rw-rw-r-- 1 bob bob 1790 mars 31 13:53 terraform.tfstate
  1. Ajoutez le bloc backend dans versions.tf

    Insérez le bloc backend "s3" { ... } (comme dans la section précédente) dans votre bloc terraform {}.

  2. Lancez la migration

    Fenêtre de terminal
    terraform init -migrate-state

    N’utilisez pas un simple terraform init ici : lors d’un changement de backend, le flag -migrate-state est celui qui dit explicitement à Terraform de reprendre l’état existant au lieu de repartir comme si le backend était neuf.

    Terraform détecte l’ancien state local et propose de le copier :

    Do you want to copy existing state to the new backend?
    Pre-existing state was found while migrating the previous "local" backend
    to the newly configured "s3" backend. No existing state was found in the
    newly configured "s3" backend. Do you want to copy this state to the new
    "s3" backend? Enter "yes" to copy and "no" to start with an empty state.
    Enter a value: yes

    Répondez yes pour copier le state.

    Successfully configured the backend "s3"! Terraform will automatically
    use this backend unless the backend configuration changes.
  3. Vérifiez que le state fonctionne

    Fenêtre de terminal
    terraform state list
    libvirt_volume.migration_test
    Fenêtre de terminal
    terraform plan
    No changes. Your infrastructure matches the configuration.
  4. Confirmez dans MinIO

    Fenêtre de terminal
    ssh terraform@192.168.122.50 "sudo mc ls --recursive local/terraform-state/"
    [2026-03-31 11:55:26 UTC] 1.7KiB STANDARD migration-demo/terraform.tfstate
  5. Constatez l’état local après migration

    Fenêtre de terminal
    ls -la terraform.tfstate*
    -rw-rw-r-- 1 bob bob 0 mars 31 13:55 terraform.tfstate
    -rw-rw-r-- 1 bob bob 1790 mars 31 13:55 terraform.tfstate.backup

    Le fichier terraform.tfstate local est vidé (0 octet). Terraform a créé un backup de l’ancien state au cas où la migration échouerait. Vous pouvez supprimer ces fichiers une fois la migration confirmée.

Vérifier et récupérer en cas d’échec de migration

Section intitulée « Vérifier et récupérer en cas d’échec de migration »

Avant de supprimer les artefacts locaux après une migration, vérifiez toujours les trois points suivants :

  1. terraform state list fonctionne bien avec le nouveau backend
  2. le fichier existe réellement dans MinIO avec mc ls --recursive
  3. votre terraform.tfstate.backup local est conservé tant que ces deux vérifications ne sont pas terminées

Si terraform init -migrate-state échoue ou si vous avez un doute sur la copie :

  • ne supprimez ni terraform.tfstate ni terraform.tfstate.backup
  • retirez temporairement le bloc backend pour revenir au backend local
  • relancez terraform init, puis vérifiez le state local avec terraform state list
  • faites une sauvegarde manuelle avec terraform state pull > state-avant-remigration.json
  • recommencez ensuite la migration avec terraform init -migrate-state

Avec un seul bucket, la clé (key) permet d’organiser les states par projet :

terraform-state/ ← bucket
├── production/
│ ├── reseau/terraform.tfstate
│ └── vms/terraform.tfstate
├── staging/
│ └── vms/terraform.tfstate
└── dev/
└── terraform.tfstate

Chaque projet utilise une key différente :

# Projet production/reseau
backend "s3" {
bucket = "terraform-state"
key = "production/reseau/terraform.tfstate"
# ... autres paramètres
}
# Projet production/vms
backend "s3" {
bucket = "terraform-state"
key = "production/vms/terraform.tfstate"
# ... autres paramètres
}
SymptômeCause probableSolution
Error: Failed to get existing workspaces lors de initMinIO injoignable ou bucket inexistantVérifier que MinIO tourne (systemctl status minio) et que le bucket existe (mc ls local/)
Access DeniedIdentifiants incorrectsVérifier access_key et secret_key
BucketRegionError ou erreur de régionskip_region_validation manquantAjouter skip_region_validation = true
timeout lors de initParamètres skip_* manquantsAjouter tous les skip_* listés dans l’exemple
Error: Backend configuration changedModification du bloc backend sans initRelancer terraform init -migrate-state
State vide après migrationRépondu « no » à la question de copieRetirer le backend, init, re-migrer avec yes
  • Le backend définit où Terraform stocke le state — en local (par défaut) ou à distance
  • Un backend distant permet le partage, la sauvegarde automatique et le verrouillage du state
  • Le backend S3 fonctionne avec tout serveur compatible S3 : AWS, MinIO, Ceph, Wasabi
  • MinIO est un serveur S3 open source que vous pouvez déployer en quelques minutes dans une VM
  • Activez le versioning du bucket pour pouvoir récupérer une ancienne version du state en cas d’écrasement
  • La migration d’un backend local vers S3 se fait avec terraform init -migrate-state — en gardant le backup local jusqu’aux vérifications finales
  • Les paramètres skip_* sont indispensables pour les serveurs S3 non-AWS (MinIO, Ceph…)
  • Organisez vos states dans le bucket avec des clés hiérarchiques : environnement/composant/terraform.tfstate

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn