← Retour au blog

7 septembre 2026

Ma découverte de l'infrastructure as code, avec Terraform

J'avais déjà de l'expérience avec Docker et la conteneurisation, un vrai atout dans mon profil. En discutant de l'hébergement de ce site avec un collègue, il m'a conseillé d'aller plus loin avec Terraform : c'est lui qui m'a fait découvrir l'infrastructure as code.

TerraformAWSCloudflareIaC

Le déclic

J'avais déjà une base solide en infrastructure grâce à Docker et à la conteneurisation, mais je ne connaissais rien à l'infrastructure as code. En discutant de l'hébergement de ce site avec un collègue, il m'a suggéré Terraform : décrire l'infrastructure dans des fichiers versionnés plutôt que de la construire à la main, pour que la configuration devienne un vrai artefact du projet, au même titre que le code de l'application.

Ça m'a tout de suite parlé comme un prolongement naturel de ce que je savais déjà faire avec Docker : là où un Dockerfile décrit un conteneur, un fichier Terraform décrit une infrastructure entière.

La logique derrière plan et apply

Terraform réconcilie en permanence trois choses : les fichiers .tf (ce que je veux), le fichier d'état terraform.tfstate (ce que Terraform pense qui existe), et la réalité chez AWS ou Cloudflare (ce qui existe vraiment).

  • terraform plan compare les trois et affiche le différentiel sans rien modifier.
  • terraform apply exécute ce différentiel, puis met à jour son fichier d'état.

Toute la mécanique du reste du projet découle de cette boucle.

1. Le provider AWS

Tout fichier Terraform commence par déclarer quels plugins (providers) il utilise, et avec quelle version.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "eu-west-1"
}

Je fige la version pour éviter qu'une mise à jour du provider ne change silencieusement le comportement d'une ressource entre deux apply. Le region du provider devient ensuite la région par défaut de tout ce qui suit.

2. Le bucket S3

resource "aws_s3_bucket" "website" {
  bucket = "baptistev-portfolio"
}

resource "aws_s3_bucket_public_access_block" "website" {
  bucket = aws_s3_bucket.website.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document { suffix = "index.html" }
  error_document { key = "404.html" }
}

Il faut ici trois ressources pour un seul bucket, parce qu'un bucket S3 bloque tout accès public par défaut, volontairement. Un site statique a besoin de l'inverse, donc lever ce blocage et activer le mode hébergement de site sont deux actes explicites et séparés, pas une case à cocher unique.

3. Le domaine bloqué, et la bascule vers Cloudflare

En essayant d'acheter baptistev.dev via Route 53, AWS a refusé l'achat à cause de sa politique anti-fraude sur les nouveaux comptes. J'ai basculé vers Cloudflare comme registrar, ce qui verrouille aussi le domaine sur les serveurs DNS de Cloudflare : impossible de les déléguer ailleurs. Cloudflare est donc devenu le fournisseur DNS du projet, pas seulement le registrar.

data "cloudflare_zone" "website" {
  name = "baptistev.dev"
}

J'utilise ici un data et non un resource, parce que la zone existe déjà, créée automatiquement par Cloudflare à l'achat du domaine. Un resource dirait à Terraform de créer une zone qui existe déjà. data se contente de la lire, pour pouvoir y référencer des enregistrements ensuite.

4. Le certificat HTTPS, obligatoirement en us-east-1

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

resource "aws_acm_certificate" "website" {
  provider          = aws.us_east_1
  domain_name       = "baptistev.dev"
  validation_method = "DNS"
}

resource "cloudflare_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.website.domain_validation_options :
    dvo.domain_name => {
      name  = dvo.resource_record_name
      type  = dvo.resource_record_type
      value = dvo.resource_record_value
    }
  }

  zone_id = data.cloudflare_zone.website.id
  name    = each.value.name
  type    = each.value.type
  content = each.value.value
}

resource "aws_acm_certificate_validation" "website" {
  provider        = aws.us_east_1
  certificate_arn = aws_acm_certificate.website.arn
}

J'ai ajouté un second provider parce que les certificats ACM utilisables par CloudFront doivent obligatoirement être créés dans us-east-1, quelle que soit la région du reste de l'infrastructure. Le bloc provider avec un alias permet à une seule ressource de cibler cette région sans changer la région par défaut du projet.

Le for_each sert à transformer une liste dynamique en autant d'enregistrements Cloudflare, sans écrire un bloc par enregistrement à la main : ACM ne connaît les enregistrements DNS à créer pour prouver la propriété du domaine qu'une fois le certificat demandé.

5. CloudFront devant S3

resource "aws_cloudfront_distribution" "website" {
  enabled = true
  aliases = ["baptistev.dev"]

  origin {
    domain_name = aws_s3_bucket_website_configuration.website.website_endpoint
    origin_id   = "s3-website"

    custom_origin_config {
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.2"]
      http_port              = 80
      https_port             = 443
    }
  }

  default_cache_behavior {
    target_origin_id       = "s3-website"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods         = ["GET", "HEAD"]
    cached_methods          = ["GET", "HEAD"]
    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate_validation.website.certificate_arn
    ssl_support_method  = "sni-only"
  }
}

J'ai mis CloudFront devant, parce qu'un point d'hébergement de site S3 ne sert que du HTTP brut, jamais de HTTPS. L'origine parle donc à S3 en HTTP simple (origin_protocol_policy = "http-only"), mais viewer_protocol_policy = "redirect-to-https" force chaque visiteur vers HTTPS, CloudFront assurant la terminaison TLS avec le certificat créé à l'étape précédente.

6. Le dernier fil : pointer le domaine

resource "cloudflare_record" "root" {
  zone_id = data.cloudflare_zone.website.id
  name    = "baptistev.dev"
  type    = "CNAME"
  content = aws_cloudfront_distribution.website.domain_name
}

Cette dernière ressource referme la chaîne : navigateur, Cloudflare, CloudFront, S3. Tant qu'elle n'existe pas, tout le reste fonctionne, mais reste injoignable depuis le nom de domaine réel.

Ce que ça m'a confirmé

La différence entre resource et data, et le fait qu'un projet Terraform puisse piloter plusieurs fournisseurs cloud à la fois et les faire communiquer, ne sont pas restés des notions abstraites après avoir construit ça : ce sont littéralement les étapes 3 et 4 ci-dessus.

Où ça en est aujourd'hui

Le site tourne derrière ce montage complet : Cloudflare pour le DNS, CloudFront pour le HTTPS et le cache, S3 pour le contenu statique. Terraform ne gère que l'infrastructure : publier un changement de contenu passe par un script séparé (npm run deploy) qui reconstruit le site, le synchronise vers S3, puis invalide le cache CloudFront.

La suite

Pour l'instant, ce déploiement se lance à la main. La prochaine étape est une pipeline CI/CD qui le déclenche automatiquement à chaque push sur la branche principale. Je reviendrai compléter ce billet une fois que ce sera en place et vérifié.