TP : Sécuriser le tfstate avec un backend S3 minimal
Prérequis
Environnement technique
- AWS CLI installé et configuré.
- OpenTofu ou Terraform installé.
- jq installé pour lire et filtrer les sorties JSON.
- Accès à un terminal Bash ou Zsh.
- Accès à un compte AWS de lab.
- Permissions suffisantes pour créer, configurer et supprimer un bucket S3.
Installer jq
Sur Debian / Ubuntu :
sudo apt updatesudo apt install -y jqSur macOS avec Homebrew :
brew install jqInstallation des outils
Sur Debian / Ubuntu (ou WSL2 Ubuntu)
sudo apt-get updatesudo apt-get install -y curl wget unzip git jq python3Installer AWS CLI si nécessaire :
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"unzip awscliv2.zipsudo ./aws/installrm -rf aws awscliv2.zipInstaller OpenTofu :
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.shchmod +x install-opentofu.shsudo ./install-opentofu.sh --install-method debrm -f install-opentofu.shSur Fedora / RHEL / Rocky Linux / AlmaLinux
sudo dnf install -y curl wget unzip git jq python3Installer AWS CLI si nécessaire :
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"unzip awscliv2.zipsudo ./aws/installrm -rf aws awscliv2.zipInstaller OpenTofu :
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.shchmod +x install-opentofu.shsudo ./install-opentofu.sh --install-method rpmrm -f install-opentofu.shSur macOS
brew install awsclibrew install opentofubrew install jqSur Windows
Sur Windows, l’environnement recommandé est WSL2 avec Ubuntu :
wsl --versionwsl --install -d UbuntuSi WSL n’est pas disponible :
winget install --exact --id Amazon.AWSCLIwinget install --exact --id OpenTofu.Tofuwinget install --exact --id jqlang.jqwinget install --exact --id Git.GitVérification des outils
aws --versiontofu version || terraform versionjq --versionPermissions AWS nécessaires
sts:GetCallerIdentitys3:CreateBuckets3:DeleteBuckets3:HeadBuckets3:ListBuckets3:GetObjects3:PutObjects3:DeleteObjects3:ListBucketVersionss3:GetObjectVersions3:DeleteObjectVersions3:PutBucketPublicAccessBlocks3:GetBucketPublicAccessBlocks3:PutBucketVersionings3:GetBucketVersionings3:PutBucketEncryptions3:GetBucketEncryptions3:PutBucketPolicys3:GetBucketPolicys3:GetBucketLocations3:ListObjectVersionsPrécaution
Ce TP crée des ressources AWS réelles dans un compte de lab.
Il crée un bucket S3 et exécute des apply Terraform.
Le TP peut générer des coûts si les ressources ne sont pas supprimées.
Ne pas exécuter ce TP dans un compte de production.
Ressources créées
| Ressource | Quantité |
|---|---|
| Bucket S3 | 1 |
| Objet tfstate | 1 |
| Objet lockfile temporaire | 0 ou 1 pendant les opérations |
Objectifs
À la fin de ce TP, l’apprenant doit être capable de :
- Créer un bucket S3 dédié au stockage du tfstate.
- Activer le chiffrement SSE-S3 sur le bucket.
- Activer le versioning S3 pour conserver les versions du state.
- Bloquer tout accès public au bucket.
- Configurer un backend S3 dans Terraform.
- Activer le verrouillage natif S3 avec
use_lockfile = true. - Migrer un state local vers le backend S3.
- Vérifier que le state est correctement stocké et protégé.
- Générer une policy IAM minimale pour le backend.
- Nettoyer proprement le bucket et les fichiers locaux.
Commandes
1. Créer l’arborescence du TP
mkdir -p tp-tfstate-s3-minimalcd tp-tfstate-s3-minimal
mkdir -p bootstrap app scripts reportsfind . -maxdepth 2 -type d | sortRésultat attendu :
../app./bootstrap./reports./scripts2. Vérifier l’identité AWS utilisée
aws sts get-caller-identity3. Définir la région AWS par défaut
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-eu-west-3}"export AWS_PAGER=""echo "Région AWS CLI par défaut : ${AWS_DEFAULT_REGION}"4. Définir les variables du TP
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"export TP_ID="$(date +%Y%m%d%H%M%S)"export TFSTATE_BUCKET="formation-tfstate-${AWS_ACCOUNT_ID}-${TP_ID}"
export BOOTSTRAP_DIR="bootstrap"export APP_DIR="app"export REPORTS_DIR="reports"
export TFSTATE_KEY="formation/tfstate-s3-minimal/terraform.tfstate"export TFSTATE_LOCK_KEY="formation/tfstate-s3-minimal/terraform.tfstate.tflock"
echo "Compte AWS : ${AWS_ACCOUNT_ID}"echo "Bucket tfstate : ${TFSTATE_BUCKET}"echo "Clé tfstate : ${TFSTATE_KEY}"cat > scripts/env.sh <<EOFexport AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION}"export AWS_PAGER=""export TP_ID="${TP_ID}"export AWS_ACCOUNT_ID="${AWS_ACCOUNT_ID}"export TFSTATE_BUCKET="${TFSTATE_BUCKET}"export BOOTSTRAP_DIR="bootstrap"export APP_DIR="app"export REPORTS_DIR="reports"export TFSTATE_KEY="formation/tfstate-s3-minimal/terraform.tfstate"export TFSTATE_LOCK_KEY="formation/tfstate-s3-minimal/terraform.tfstate.tflock"EOFecho "Bucket : ${TFSTATE_BUCKET}" | tee reports/00-variables.txt5. Sélectionner Terraform ou OpenTofu
cat > scripts/select-iac-cli.sh <<'EOF'#!/usr/bin/env bashset -euo pipefail
if command -v tofu >/dev/null 2>&1; then echo "tofu"elif command -v terraform >/dev/null 2>&1; then echo "terraform"else echo "Aucun outil IaC trouvé : installer OpenTofu ou Terraform." >&2 exit 1fiEOFchmod +x scripts/select-iac-cli.shexport IAC_CLI="$(./scripts/select-iac-cli.sh)"echo "Outil IaC détecté : ${IAC_CLI}"${IAC_CLI} version6. Créer un diagnostic des outils
{ echo "# Diagnostic outils TP tfstate S3" echo echo "## AWS CLI" aws --version 2>/dev/null || echo "Non disponible" echo echo "## Outil IaC" ${IAC_CLI} version 2>/dev/null || echo "Non disponible" echo echo "## jq" jq --version 2>/dev/null || echo "Non disponible" echo echo "## Compte AWS" aws sts get-caller-identity} | tee reports/01-tools-diagnostic.txtCréation du bucket S3 sécurisé
7. Créer le code Terraform de bootstrap
cat > bootstrap/main.tf <<'EOF'terraform { required_version = ">= 1.5.0"
required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }}
variable "aws_region" { type = string}
variable "tfstate_bucket" { type = string}
provider "aws" { region = var.aws_region}
resource "aws_s3_bucket" "tfstate" { bucket = var.tfstate_bucket
tags = { Name = var.tfstate_bucket Usage = "terraform-state" Environment = "training" ManagedBy = "terraform" }}
resource "aws_s3_bucket_public_access_block" "tfstate" { bucket = aws_s3_bucket.tfstate.id
block_public_acls = true ignore_public_acls = true block_public_policy = true restrict_public_buckets = true}
resource "aws_s3_bucket_ownership_controls" "tfstate" { bucket = aws_s3_bucket.tfstate.id
rule { object_ownership = "BucketOwnerEnforced" }}
resource "aws_s3_bucket_versioning" "tfstate" { bucket = aws_s3_bucket.tfstate.id
versioning_configuration { status = "Enabled" }}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" { bucket = aws_s3_bucket.tfstate.id
rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } bucket_key_enabled = true }}
resource "aws_s3_bucket_policy" "tfstate_tls_only" { bucket = aws_s3_bucket.tfstate.id
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "DenyInsecureTransport" Effect = "Deny" Principal = "*" Action = "s3:*" Resource = [ aws_s3_bucket.tfstate.arn, "${aws_s3_bucket.tfstate.arn}/*" ] Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] })
depends_on = [ aws_s3_bucket_public_access_block.tfstate ]}
output "tfstate_bucket" { value = aws_s3_bucket.tfstate.bucket}
output "tfstate_bucket_arn" { value = aws_s3_bucket.tfstate.arn}EOFLa ressource aws_s3_bucket_acl est volontairement absente. Avec object_ownership = "BucketOwnerEnforced", les ACL S3 sont désactivées et aws_s3_bucket_acl serait en erreur.
cat > bootstrap/terraform.tfvars <<EOFaws_region = "${AWS_DEFAULT_REGION}"tfstate_bucket = "${TFSTATE_BUCKET}"EOF8. Initialiser et valider le bootstrap
cd bootstrap${IAC_CLI} init${IAC_CLI} validatecd ..cat > reports/02-bootstrap-validation.txt <<EOFDossier : ${BOOTSTRAP_DIR}Outil IaC : ${IAC_CLI}Bucket prévu : ${TFSTATE_BUCKET}Région : ${AWS_DEFAULT_REGION}Validation : OKEOF9. Créer le bucket S3 sécurisé
cd bootstrap${IAC_CLI} apply -auto-approvecd ..cd bootstrap${IAC_CLI} output -json | tee ../reports/03-bootstrap-outputs.jsoncd ..aws s3api head-bucket --bucket "${TFSTATE_BUCKET}"echo "Code de retour head-bucket : $?"aws s3api get-bucket-location \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/04-bucket-location.jsonVérifications de sécurité du bucket
10. Vérifier le blocage public
aws s3api get-public-access-block \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/05-public-access-block.jsonRésultat attendu :
{ "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "IgnorePublicAcls": true, "BlockPublicPolicy": true, "RestrictPublicBuckets": true }}11. Vérifier le versioning
aws s3api get-bucket-versioning \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/06-bucket-versioning.jsonRésultat attendu :
{ "Status": "Enabled"}12. Vérifier le chiffrement
aws s3api get-bucket-encryption \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/07-bucket-encryption.jsonRésultat attendu :
{ "ServerSideEncryptionConfiguration": { "Rules": [ { "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } } ] }}13. Vérifier la policy HTTPS obligatoire
aws s3api get-bucket-policy \ --bucket "${TFSTATE_BUCKET}" \ --query Policy \ --output text \ | jq . \ | tee reports/08-bucket-policy.jsonLa policy doit contenir DenyInsecureTransport avec aws:SecureTransport = false.
14. Vérifier que le bucket est vide avant migration
aws s3api list-objects-v2 \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/09-bucket-objects-before-migration.jsonRésultat attendu :
{ "KeyCount": 0}Création d’un projet Terraform local
15. Créer le projet applicatif local
cat > app/main.tf <<'EOF'terraform { required_version = ">= 1.5.0"}
resource "terraform_data" "demo" { input = { name = "tfstate-s3-minimal" environment = "training" }}
output "demo_id" { value = terraform_data.demo.id}EOFCe projet utilise terraform_data. Il ne crée aucune ressource cloud.
16. Initialiser le projet avec un state local
cd app${IAC_CLI} init${IAC_CLI} validate${IAC_CLI} apply -auto-approvecd ..ls -lh app/terraform.tfstate | tee reports/10-local-tfstate-file.txtcd app${IAC_CLI} output | tee ../reports/11-app-output-local-state.txtcd ..Configuration du backend S3
17. Créer la configuration backend S3
cat > app/backend.tf <<EOFterraform { backend "s3" { bucket = "${TFSTATE_BUCKET}" key = "${TFSTATE_KEY}" region = "${AWS_DEFAULT_REGION}" encrypt = true use_lockfile = true }}EOFcat app/backend.tfuse_lockfile = true active le verrouillage natif S3 sans dépendance à DynamoDB. Cette fonctionnalité est disponible dans OpenTofu et Terraform récents.
18. Migrer le state local vers S3
cd app${IAC_CLI} init -migrate-state -force-copycd ..ls -la app | tee reports/12-app-files-after-migration.txtLe fichier local terraform.tfstate peut rester comme backup de migration. Le state actif est désormais sur S3.
19. Vérifier le state distant
aws s3api list-objects-v2 \ --bucket "${TFSTATE_BUCKET}" \ --prefix "formation/tfstate-s3-minimal/" \ | tee reports/13-bucket-objects-after-migration.jsonaws s3 ls "s3://${TFSTATE_BUCKET}/formation/tfstate-s3-minimal/" \ | tee reports/14-s3-ls-state-prefix.txtRésultat attendu : terraform.tfstate doit être visible.
20. Vérifier le fonctionnement du backend S3
cd app${IAC_CLI} plan | tee ../reports/15-app-plan-after-s3-backend.txt${IAC_CLI} state list | tee ../reports/16-app-state-list.txtcd ..Résultat attendu :
terraform_data.demoTest du versioning du state
21. Modifier la ressource locale
python3 - <<'PY'from pathlib import Path
path = Path("app/main.tf")content = path.read_text()content = content.replace( 'environment = "training"', 'environment = "training-updated"')path.write_text(content)print("Modification appliquée.")PYgrep -n "environment" app/main.tf22. Appliquer la modification
cd app${IAC_CLI} apply -auto-approvecd ..23. Vérifier les versions du fichier state
aws s3api list-object-versions \ --bucket "${TFSTATE_BUCKET}" \ --prefix "${TFSTATE_KEY}" \ | tee reports/17-state-object-versions.jsonjq '[.Versions[]? | select(.Key == "'"${TFSTATE_KEY}"'")] | length' \ reports/17-state-object-versions.json \ | tee reports/18-state-version-count.txtRésultat attendu : au moins 2 versions.
Vérification de non-publication
24. Tester l’absence d’accès public
export STATE_PUBLIC_URL="https://${TFSTATE_BUCKET}.s3.${AWS_DEFAULT_REGION}.amazonaws.com/${TFSTATE_KEY}"echo "${STATE_PUBLIC_URL}" | tee reports/19-state-public-url.txtcurl -I "${STATE_PUBLIC_URL}" \ | tee reports/20-public-access-test.txt || trueRésultat attendu :
HTTP/1.1 403 ForbiddenPolitique IAM minimale recommandée
25. Générer une policy IAM minimale pour le backend
cat > reports/21-iam-policy-minimale-tfstate.json <<EOF{ "Version": "2012-10-17", "Statement": [ { "Sid": "ListTfstateBucket", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": "arn:aws:s3:::${TFSTATE_BUCKET}", "Condition": { "StringLike": { "s3:prefix": [ "formation/tfstate-s3-minimal/*" ] } } }, { "Sid": "ReadWriteTfstateObject", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::${TFSTATE_BUCKET}/${TFSTATE_KEY}", "arn:aws:s3:::${TFSTATE_BUCKET}/${TFSTATE_LOCK_KEY}" ] } ]}EOFjq . reports/21-iam-policy-minimale-tfstate.jsonCette policy limite les actions au préfixe du state et aux objets nécessaires au state et au lockfile.
Rapport de synthèse
26. Créer un rapport de synthèse local
{ echo "# Rapport TP : Sécuriser le tfstate avec S3 minimal" echo echo "## Environnement" echo echo "| Élément | Valeur |" echo "|---|---|" echo "| TP | tp-tfstate-s3-minimal |" echo "| Région AWS | ${AWS_DEFAULT_REGION} |" echo "| Compte AWS | ${AWS_ACCOUNT_ID} |" echo "| Outil IaC | ${IAC_CLI} |" echo "| Bucket tfstate | ${TFSTATE_BUCKET} |" echo "| Clé tfstate | ${TFSTATE_KEY} |" echo "| Clé lockfile | ${TFSTATE_LOCK_KEY} |" echo echo "## Ressources créées" echo echo "| Ressource | Rôle |" echo "|---|---|" echo "| Bucket S3 | Stockage distant du tfstate |" echo "| Versioning S3 | Conservation des versions du state |" echo "| Chiffrement SSE-S3 | Chiffrement au repos |" echo "| Block Public Access | Blocage des accès publics |" echo "| Bucket policy TLS | Refus des accès non HTTPS |" echo echo "## Sécurisations vérifiées" echo echo "| Contrôle | Fichier de preuve |" echo "|---|---|" echo "| Block Public Access | reports/05-public-access-block.json |" echo "| Versioning | reports/06-bucket-versioning.json |" echo "| Chiffrement | reports/07-bucket-encryption.json |" echo "| Policy HTTPS | reports/08-bucket-policy.json |" echo "| Migration du state | reports/13-bucket-objects-after-migration.json |" echo "| Versions du state | reports/17-state-object-versions.json |" echo "| Test accès public | reports/20-public-access-test.txt |" echo echo "## Backend Terraform configuré" echo echo '```hcl' echo 'terraform {' echo ' backend "s3" {' echo " bucket = \"${TFSTATE_BUCKET}\"" echo " key = \"${TFSTATE_KEY}\"" echo " region = \"${AWS_DEFAULT_REGION}\"" echo ' encrypt = true' echo ' use_lockfile = true' echo ' }' echo '}' echo '```'} > reports/rapport-tp-tfstate-s3-minimal.md27. Afficher le rapport
cat reports/rapport-tp-tfstate-s3-minimal.md28. Lister les fichiers générés
find . -maxdepth 3 -type f | sort \ | tee reports/22-generated-files.txtNettoyage
29. Recharger les variables si nécessaire
Si le nettoyage est effectué dans un nouveau terminal :
cd tp-tfstate-s3-minimalsource scripts/env.shexport IAC_CLI="$(./scripts/select-iac-cli.sh)"30. Détruire la ressource applicative
cd app${IAC_CLI} destroy -auto-approvecd ..31. Supprimer les fichiers locaux du projet applicatif
rm -rf app/.terraform app/.terraform.lock.hclrm -f app/terraform.tfstate app/terraform.tfstate.backup32. Vider le bucket S3 avec versions
cat > scripts/empty-versioned-bucket.sh <<'EOF'#!/usr/bin/env bashset -euo pipefail
BUCKET="${1:-}"
if [ -z "$BUCKET" ]; then echo "Usage: $0 <bucket-name>" >&2 exit 1fi
echo "Suppression des versions d'objets dans le bucket : $BUCKET"
aws s3api list-object-versions \ --bucket "$BUCKET" \ --output json \ > /tmp/s3-versions.json
jq -c '.Versions[]? | {Key: .Key, VersionId: .VersionId}' /tmp/s3-versions.json \ | while read -r object; do KEY=$(echo "$object" | jq -r '.Key') VERSION_ID=$(echo "$object" | jq -r '.VersionId') aws s3api delete-object \ --bucket "$BUCKET" \ --key "$KEY" \ --version-id "$VERSION_ID" >/dev/null done
jq -c '.DeleteMarkers[]? | {Key: .Key, VersionId: .VersionId}' /tmp/s3-versions.json \ | while read -r object; do KEY=$(echo "$object" | jq -r '.Key') VERSION_ID=$(echo "$object" | jq -r '.VersionId') aws s3api delete-object \ --bucket "$BUCKET" \ --key "$KEY" \ --version-id "$VERSION_ID" >/dev/null done
rm -f /tmp/s3-versions.jsonecho "Bucket vidé."EOFchmod +x scripts/empty-versioned-bucket.sh./scripts/empty-versioned-bucket.sh "${TFSTATE_BUCKET}"aws s3api list-object-versions \ --bucket "${TFSTATE_BUCKET}" \ | tee reports/23-bucket-versions-after-empty.json33. Détruire le bootstrap
cd bootstrap${IAC_CLI} destroy -auto-approvecd ..aws s3api head-bucket --bucket "${TFSTATE_BUCKET}" 2>&1 \ && echo "Le bucket existe encore" \ || echo "Bucket bien supprimé"34. Supprimer les fichiers d’initialisation du bootstrap
rm -rf bootstrap/.terraform bootstrap/.terraform.lock.hclrm -f bootstrap/terraform.tfstate bootstrap/terraform.tfstate.backup35. Vérifier qu’aucun state ne reste
find . -name "terraform.tfstate*" -type fRésultat attendu : aucun fichier terraform.tfstate ne doit être présent.
36. Supprimer le dossier du TP
Se placer dans le dossier parent avant d’exécuter cette commande.
cd ..rm -rf tp-tfstate-s3-minimalRésultat attendu
À la fin du TP, les éléments suivants doivent avoir été validés :
| Élément | Validation |
|---|---|
| Bucket S3 | Bucket dédié au tfstate créé |
| Accès public | Block Public Access activé |
| Versioning | Versioning activé |
| Chiffrement | SSE-S3 activé |
| Transport | Accès non HTTPS refusé |
| Backend S3 | backend "s3" configuré |
| Verrouillage | use_lockfile = true configuré |
| Migration | State local migré vers S3 |
| Vérification | Objet terraform.tfstate visible dans S3 |
| Historique | Plusieurs versions du state disponibles |
| Nettoyage | Bucket, state et fichiers locaux supprimés |
Aucune instance EC2, aucune fonction Lambda et aucun coût supérieur au stockage S3 ne doivent être générés pendant ce TP.