Aller au contenu

Les bonnes pratiques pour l'initialisation d'un nouveau projet Terraform (tf.state)

Je te montre les best practices pour ton Terraform State, en minimisant les erreurs manuelles, le leak de secrets, la gestion du locking. Bref, comment gérer le partage du tf.state dans une équipe de dév.

Cas concret avec un remote backend sur du AWS Bucket S3, avec le verrouillage natif de S3.


J'ai souhaité récemment me faire un petit blog sur le net et l'heberger sur du AWS. J'ai voulu faire les choses propres et me faire mon infra as code avec Terraform. Mais avant de pouvoir plan et apply mes nouvelles ressources dont j'ai besoin, j'ai du me confronté à l'initialisation d'un projet Terraform.


Et si tu veux faire les choses bien, je peux te montrer quelques good practices concernant la gestion de ton tf.state ;)

Je te montre ça sur du AWS, mais tu peux faire exactement la même chose sur les autres clouds provider. Seul le nom des ressources vont changer de l'un à l'autre !

Pré-requis

Tools :

  • Terraform

tf.state

C'est quoi ?

Quand tu vas définir la création, la mise à jour ou la suppression de ressource via des fichiers Terraform, lui même doit savoir dans quel ordre réaliser ces instructions. Les ressources ayant des specifications différentes, et des dépénendances entre elle plus ou moins forte, Terraform va se faire un bon gros fichier JSON resencent l'ensemble de ces instructions dans le bon ordre.

Quels sont les soucis de ce tf.state ?

Aucun pour une utilisation simple. Realisant moi même en solo ce blog, je peux très bien faire un simple terraform init et commencer à dév mon infra sans soucis, le tout en local. Mais quid sur un vrai projet avec plusieurs dev travaillant ensemble ? C'est là ou les choses vont se compliquer.

  • Partager le tf.state à plusieurs : Si chacun des membres de l'équipe doit mettre à jour cet infra, ils doivent pouvoir avoir librement accès à ce fichier
  • Verouiller le tf.state : Soucis de bosser à plusieurs, c'est que ce fichier ne doit être utiliser que par une seule et unique personne sur un instant T. Si tu essayes de faire un accès concurrent à ce fichier tu peux finir avec une corruption du tf.state et mettre en peril ton infra
  • Isoler le tf.state : Plus ton infra grandi, plus les plan/apply risque d'être long. Isoler en une multitude de plus petits tf.state te permet de travailler plus vite, et surtout en cas de soucis de ne pas casser toute ton infra.

Comment partager ce tf.state ?

Git ? Non !

Tu le sais déjà, mais on va être obliger à un moment donné d'utiliser Git. Mais cela n'est pas possible car cela engendre plusieurs problèmes :

  • Erreur manuelle: on oublie trop souvent de pull le latest change sur ton repo Git, ça peut donc t'amener très rapidement à des régréssions d'infra
  • Verouillage: Impossible de verouiller un fichier à un accès concurrentiel, qui empêcherais donc 2 membres d'une même équipe d'y accéder au même moment
  • Secret: Et enfin le plus important, c'est que ce fichier tf.state contient tout les infos de ton infra en texte clair. Ce qui signifie que tout secret, token, ou données sensible peut être leak sur ton git.

Remote backend ? OUI !

C'est le feu car ça résolve les 3 problématiques que je viens de vous exposer :

  • Erreur manuelle: Terraform va automatiquement charger le tf.state a chaque fois que tu effecue un terraform plan ou apply, et automatiquement renvoyer sur le remote backend le nouveau tf.state fraichement crée
  • Verouillage: la plupart des remotes backend supportent nativement le locking. Et si quelqu'un d'autre à aquéri le lock à un instant T, tu vas devoir attendre qu'il ait fini. Il existe une commande, plutôt cet argument ci: -lock-timeout= qui te permet de dire à terraform d'attendre jusqu'a cette période là pour effectuer ta commande
  • Secret: La plupart des remotes backend supportent aussi nativement l'encryptage, de configurer les permissions d'accès et donc de définir quel utilisateur à le droit

Exemple avec un AWS S3 bucket

Dans mon cas on va faire avec un service managé par AWS qui te rend ultra simple cet ajout là, le S3. Qu'apporte t-il ?

  • Service managé, donc ballek de maintenir une infra supplémentaire pour ce cas d'usage
  • Niveau durabilité et disponibilité, on est à 99,99.....%, donc tu crains pas d'avoir des soucis pour y acceder à tout heure
  • Support l'encryptage, ce qui est plutôt cool avec les données sensibles stocké dedans le tf.state. Exemple avec de l'AES-256 niveau encryption côté Amazon, et on utilise du TLS pour la communication AWS <-> Terraform
  • Verrouillage natif via un lockfile S3 (use_lockfile, Terraform ≥ 1.10), sans table DynamoDB à maintenir
  • Versionnage: te permet de définir des incrementations de versions du tf.state, pratique donc de rollback sur une ancienne version si tu casses ton infra !
  • Pas bien cher, tu va fit avec le free tier si tu veux tester pour t'entrainer !

Let's code !

Définition du provider

On commence par ajouter le provider :

1
2
3
4
# provider.tf
provider "aws" {
    region = "eu-west-3"
}

Authentication du provider

Je le montre pas ici, mais tu dois permettre à Terraform de s'authentifier sur ton compte AWS. N'utilise jamais de clés d'accès du compte root : configure un profil IAM Identity Center avec aws configure sso, puis connecte-toi avec aws sso login. Terraform récupère alors des credentials temporaires via le profil (variable AWS_PROFILE, ou le profil default).

Création du S3 Bucket

On défini notre S3 bucket. Il va nous permettre de garder sur disque notre tf.state.

1
2
3
4
5
6
7
8
9
# state_s3.tf
resource "aws_s3_bucket" "s3_state" {
  bucket = local.bucket_state_name
  tags   = local.commonTags

  lifecycle {
    prevent_destroy = true
  }
}

lifecycle

Je rajoute un hook ici, une sécurité si je fais par mégarde un terraform destroy. Sans trop faire gaffe je pourrais delete le contenu de tout le bucket. Ici on n'empêche d'apply le destroy


J'ai utilisé des variables locales dans le code précédent, rien de particulier ici à préciser mais je te donne mes valeurs à titre d'example:

1
2
3
4
5
6
7
8
9
# locals.tf
locals {
  bucket_state_name = "bastienmaurice-website-infra-state"

  commonTags = {
    createdBy     = "terraform"
    repositoryFor = "bastienmaurice-website"
  }
}

Ajout du versionning

On active le versionning sur notre bucket. S3 conserve alors chaque version précédente du tf.state à chaque nouvelle écriture, suite à une mise à jour de notre infrastructure. Pratique en cas de rollback à une version antérieur, si on casse tout à un certains moment ! 😉

1
2
3
4
5
6
7
8
# state_s3.tf
resource "aws_s3_bucket_versioning" "s3_versionning" {
  bucket = aws_s3_bucket.s3_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

Activation de l'encryptage

Histoire d'augmenter le niveau de sécurité, on encrypte le contenu du bucket S3. Super important, car tout token, secret et autre donnée sensible se retrouve en plain text sur le tf.state. Bonjour le leak si une persionne malveillante arrive sur ton bucket ! 😅

# state_s3.tf
resource "aws_s3_bucket_server_side_encryption_configuration" "s3_encryption" {
  bucket = aws_s3_bucket.s3_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

Rendre le S3 privé

On va rendre le bucket en mode privée afin d'éviter que de mauvaises personnes mal-intentioné récup des infos sensible :

1
2
3
4
5
6
7
8
9
# state_s3.tf
resource "aws_s3_bucket_public_access_block" "s3_access" {
  bucket = aws_s3_bucket.s3_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Gestion du locking

Depuis Terraform 1.10, le backend S3 sait verrouiller le tf.state tout seul : avec use_lockfile = true, Terraform pose un fichier .tflock à côté du state grâce aux écritures conditionnelles de S3. Aucune ressource supplémentaire à créer, on l'active directement dans la configuration du backend un peu plus bas.

Et DynamoDB ?

Avant Terraform 1.10, le verrouillage passait par une table DynamoDB (dynamodb_table dans le backend). Cette option est désormais dépréciée : si tu as un backend existant, ajoute use_lockfile = true, puis retire dynamodb_table et la table une fois la migration faite.

Initialisation de l'infra - part1

A partir de là, tu peux commencer à faire un terraform init, puis terraform plan et enfin terraform apply. Tu vas avoir ton S3 Bucket de créé.

Question

Bizarre, je ne vois aucun tf.state dans mon bucket qui défini mon infra actuelle


Et c'est normal. On à encore la sauvegarde du tf.state en local, vu que ton provider initiallement crée au début du tutoriel target ton local.

From Local Backend to Remote Backend

On touche du doigt la fin du chapitre. C'est ici que l'on va modifier notre configuration de notre provider, afin de lui dire d'aller target le backend disponible en ligne sur notre S3

# providers.tf
terraform {
  required_version = ">= 1.10" # nécessaire pour use_lockfile

  required_providers { # Nouveau bloc
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  backend "s3" { # Nouveau bloc
    bucket = "bastienmaurice-website-infra-state"
    key    = "terraform/state/terraform.tfstate"
    region = "eu-west-3"

    use_lockfile = true # verrouillage natif S3, sans DynamoDB
    encrypt      = true
  }
}

Initialisation de l'infra - part2

Tu vas pouvoir refaire un second terraform init. A partir de là terraform va te dire que tu as déjà un tf.state en local, et va te demander si tu souhaites desormais l'avoir sur ton s3 en partant de ton actuel. Tape yes et Finito, tu as bien ton tf.state sur AWS et qui suit les bonnes pratiques :)

Conclusion & résumé

Comme tu as pu le voir, tu as quelques manips à faire : 1. Ecrire une première partie de l'infra sous terraform pour créer ton bucket S3 avec un tf.state en local 2. Ajouter un remote backend et le configure pour qu'il puisse utiliser le S3 et ta DB précédement crée, et refaire un terraform init pour copier ton tf.state local que tu as déjà utilisé.


Petit tips, si tu veux supprimer ton infra existante, tâche de faire ces étapes à l'envers ; à savoir :

  1. Supprimer le bloc backend "s3" de la configuration terraform, et lancer terraform init -migrate-state pour rapatrier ton tf.state en local
  2. Retirer le prevent_destroy du bucket et ajouter force_destroy = true, puis terraform apply. Sans ça, Terraform refuse le destroy, et un bucket versionné non vide renvoie une erreur BucketNotEmpty
  3. Lancer ton terraform destroy pour supprimer ton bucket S3 et toutes les versions du tf.state qu'il contient

Automatiser vos déploiements Terraform en CI/CD

Maintenant que votre remote backend est en place, découvrez comment automatiser vos terraform plan et apply de façon sécurisée sans clés d'accès permanentes grâce à OIDC :
👉 Configurer une GitHub Action pour déployer son Terraform sur AWS via OIDC