Déployer un site statique sécurisé sur AWS S3 & CloudFront
Ce tutoriel met en place un site statique sur S3, gardé privé derrière CloudFront avec Origin Access Control, un certificat TLS géré par ACM, et une étape de déploiement qui invalide correctement le cache, le schéma sécurisé plutôt que le raccourci du bucket public.
Configuration du bucket : privé par défaut
Le bucket ne doit jamais être lisible publiquement ni avoir l'hébergement de site statique activé (cette fonctionnalité ne sert que du HTTP simple). Il n'existe que comme stockage privé que CloudFront est autorisé à lire.
resource "aws_s3_bucket" "site" {
bucket = "www.example-consulting.fr"
}
resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Origin Access Control est aujourd'hui le mécanisme recommandé par AWS pour permettre à CloudFront de lire un bucket privé, en remplacement de l'ancien Origin Access Identity (OAI), désormais déprécié. L'OAC signe chaque requête que CloudFront envoie à S3 avec SigV4.
resource "aws_cloudfront_origin_access_control" "site" {
name = "site-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}La bucket policy accorde un accès en lecture uniquement au principal de service CloudFront, restreint via AWS:SourceArn pour qu'aucune autre distribution ne puisse l'utiliser comme origine :
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::www.example-consulting.fr/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1EXAMPLEID"
}
}
}
]
}C'est le problème de l'œuf et de la poule : l'ARN de la distribution n'existe pas tant que la distribution n'est pas créée, et la distribution a besoin que l'OAC existe déjà. Terraform résout cela naturellement via les références entre ressources : assurez-vous simplement que la bucket policy dépend de la distribution, et non l'inverse.
La distribution CloudFront
La distribution relie le bucket et l'OAC, et configure le TLS, la redirection HTTP vers HTTPS, et le document par défaut.
resource "aws_cloudfront_distribution" "site" {
enabled = true
default_root_object = "index.html"
aliases = ["www.example-consulting.fr"]
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "s3-site-origin"
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
}
default_cache_behavior {
target_origin_id = "s3-site-origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # AWS managed : CachingOptimized
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.site.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
}viewer_protocol_policy = "redirect-to-https" impose le TLS pour les visiteurs ; sans cette ligne, CloudFront servira sans problème du HTTP en clair. default_root_object fait résoudre / vers index.html, mais pas /about/ vers /about/index.html ; il vous faut soit une petite CloudFront Function qui réécrit l'URI, soit un export Next.js avec des routes sans slash final et des clés S3 correspondantes.
Certificat ACM et domaine personnalisé
CloudFront n'accepte que des certificats ACM émis dans la région us-east-1, quelle que soit la région où vit le reste de votre infrastructure.
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
resource "aws_acm_certificate" "site" {
provider = aws.us_east_1
domain_name = "www.example-consulting.fr"
validation_method = "DNS"
}
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.site.domain_validation_options : dvo.domain_name => dvo
}
zone_id = aws_route53_zone.site.zone_id
name = each.value.resource_record_name
type = each.value.resource_record_type
records = [each.value.resource_record_value]
ttl = 60
}Une fois validé, pointez l'enregistrement alias Route53 du domaine vers la distribution, et vous obtenez un domaine personnalisé avec un certificat géré et auto-renouvelé : plus aucun fichier de certificat à faire tourner manuellement.
CloudFront met aussi les réponses d'erreur en cache : si votre OAC ou votre bucket policy est ne serait-ce que légèrement mal configuré, il va mettre en cache le 403 résultant pendant tout le TTL par défaut et continuer à le servir jusqu'à ce que vous invalidiez. Quand vous débuggez une erreur d'accès refusé juste après un changement de configuration, invalidez /* avant de conclure que le correctif n'a pas fonctionné.
L'étape de déploiement : synchroniser puis invalider
Les tutoriels "il suffit d'uploader sur S3" oublient cette étape, ce qui explique pourquoi un correctif poussé n'apparaît souvent pas au rechargement : les caches en périphérie de CloudFront ne savent pas que vos fichiers ont changé tant que vous ne le leur dites pas.
#!/usr/bin/env bash
set -euo pipefail
aws s3 sync ./out s3://www.example-consulting.fr \
--delete \
--cache-control "public,max-age=31536000,immutable" \
--exclude "*.html"
aws s3 sync ./out s3://www.example-consulting.fr \
--delete \
--cache-control "public,max-age=0,must-revalidate" \
--exclude "*" --include "*.html"
aws cloudfront create-invalidation \
--distribution-id "$DISTRIBUTION_ID" \
--paths "/*"Deux synchronisations, pas une seule. Les assets statiques hashés (les bundles JS/CSS issus d'un export statique Next.js) reçoivent une durée de cache d'un an, puisque leur nom de fichier change à chaque build. Les fichiers HTML reçoivent must-revalidate à la place, parce que ce sont eux que les visiteurs voient réellement et qu'ils doivent se mettre à jour immédiatement. Sans invalidation, index.html est précisément le fichier le plus susceptible d'être obsolète juste après un déploiement.
Les invalidations ne sont plus gratuites au-delà des 1 000 premiers chemins par mois, mais à la fréquence de déploiement d'un site personnel ou d'une petite structure, /* reste suffisamment bon marché : pas la peine de calculer une liste précise des fichiers modifiés. N'optimisez ce point que lorsque vous déployez des dizaines de fois par jour.
En-têtes de cache et behaviors
La cache policy de la distribution et les en-têtes Cache-Control définis pendant la synchronisation travaillent ensemble, pas l'un contre l'autre : sous la policy managée CachingOptimized, CloudFront respecte par défaut les en-têtes de l'origine, et ne retombe sur son propre TTL que lorsque l'origine n'en spécifie aucun, c'est pour cela que l'étape de synchronisation fixe des en-têtes explicites sur chaque objet.
Vue d'ensemble
Le Terraform ici prend quelques heures à écrire une fois ; ensuite c'est un module qu'on copie et qu'on renomme.
Envie de faire tourner ça en production ?
Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.
Ce tutoriel
- Concepts clés et architecture principale
- Extraits de code illustratifs
- Le raisonnement derrière chaque décision
Mentorat individuel
- Des sessions de travail sur votre propre environnement
- Des réponses directes aux cas particuliers que vous rencontrez
- Un retour sur votre implémentation réelle
- Un accompagnement continu pendant que vous la construisez