Efficience IT
·Outils

Déployer Nuxt.js avec GitLab CI, S3 et CloudFront

Par Louis-Arnaud Catoire

Déployer un site statique généré par Nuxt.js sur une infrastructure AWS ne se résume pas à pousser des fichiers sur un bucket. Derrière cette apparente simplicité se cachent des décisions d'architecture qui impactent la performance, la sécurité, le coût et la résilience de votre plateforme. Cet article parcourt le chemin complet : de la mise en place initiale S3 + CloudFront jusqu'aux stratégies de déploiement multi-environnement, en passant par les considérations d'architecture CDN à grande échelle.

Le socle AWS : S3 et CloudFront

Le principe est limpide. S3 stocke les fichiers statiques (HTML, CSS, JS, images). CloudFront les distribue via un réseau de points de présence répartis mondialement. Un utilisateur IAM dédié permet à GitLab CI d'interagir avec ces services de manière programmatique.

Configuration du bucket S3

La création du bucket suit une séquence précise. Dans la console S3, créez un compartiment dont le nom sera stocké dans la variable AWS_BUCKET_NAME. Débloquez les accès publics, puis activez l'hébergement de site statique dans les propriétés en spécifiant index.html comme document d'index.

La stratégie de compartiment doit autoriser la lecture publique :

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Sid": "PublicReadGetObject",
           "Effect": "Allow",
           "Principal": "*",
           "Action": "s3:GetObject",
           "Resource": "arn:aws:s3:::nuxt-website-efficience-it/*"
       }
   ]
}

Une erreur 404 sur index.html après la création est le comportement attendu : le bucket est fonctionnel, mais vide.

Distribution CloudFront

CloudFront agit comme couche de cache entre les utilisateurs et le bucket S3. Au-delà du gain de performance, cette architecture réduit significativement les coûts : chaque requête servie par le cache évite un appel facturé vers S3.

Lors de la création de la distribution, rattachez-la au bucket S3, forcez la redirection HTTP vers HTTPS et associez votre nom de domaine avec son certificat SSL. Conservez l'identifiant de distribution dans AWS_CLOUDFRONT_INVALIDATION_ID : il sera indispensable pour purger le cache lors des déploiements.

Utilisateur IAM gitlab-runner

Le principe du moindre privilège s'applique ici. Créez un utilisateur IAM dédié avec uniquement deux permissions : AmazonS3FullAccess pour la synchronisation des fichiers et CloudFrontInvalidation pour la purge du cache. Générez une clé d'accès programmatique qui alimentera les variables AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY dans GitLab.

Un point de vigilance pour les environnements de production : préférez des politiques IAM restreintes au bucket spécifique plutôt que AmazonS3FullAccess, qui donne accès à tous les buckets du compte.

Pipeline GitLab CI : build et déploiement

Le stage de build

Le build génère les fichiers statiques à partir du code source Nuxt.js. L'artifact produit (le dossier /dist) sera consommé par le stage de déploiement.

stages:
   - build
cache:
   paths:
       - node_modules/

build_site:
   image: node:lts
   before_script:
       - npm install
   script:
       - npm run generate --fail-on-error --quiet --no-optional
   artifacts:
       paths:
           - dist

Le cache sur node_modules/ est essentiel. Sans lui, chaque pipeline réinstalle l'intégralité des dépendances. Sur un projet Nuxt.js typique, cela représente facilement deux à trois minutes économisées par exécution. À l'échelle d'une équipe qui pousse régulièrement, le gain cumulé est considérable.

Le stage de déploiement

Le déploiement effectue deux opérations atomiques : la synchronisation des fichiers vers S3, puis l'invalidation du cache CloudFront.

stages:
   - deploy

variables:
   AWS_DEFAULT_REGION: eu-central-1

deploy_s3:
   image: python:latest
   stage: deploy
   before_script:
       - pip install awscli
   script:
       - aws s3 sync dist s3://${AWS_BUCKET_NAME} --delete --cache-control "max-age=31536000" --expires 2030-01-01T00:00:00Z
       - aws cloudfront create-invalidation  --distribution-id ${AWS_CLOUDFRONT_INVALIDATION_ID} --paths "/*"

Le flag --delete supprime du bucket les fichiers absents du build local, garantissant une synchronisation stricte. Les en-têtes cache-control et expires demandent aux navigateurs de mettre en cache les assets pendant un an, ce qui maximise la performance côté client. Puisque Nuxt.js génère des noms de fichiers hashés pour le CSS et le JS, les anciens fichiers ne seront jamais servis par erreur.

Variables CI/CD

Dans GitLab, sous Settings > CI/CD > Variables, injectez les quatre secrets : AWS_CLOUDFRONT_INVALIDATION_ID, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY et AWS_BUCKET_NAME. Marquez-les comme protégées et masquées pour éviter toute fuite dans les logs de pipeline.

Déploiement multi-environnement par branche

Un pipeline de production ne suffit pas. Une équipe structurée a besoin d'environnements de prévisualisation pour valider les changements avant la mise en production. Le principe : un couple bucket/CloudFront par environnement, et le pipeline qui route automatiquement le déploiement selon la branche.

La branche master déclenche le déploiement en production. La branche dev déploie automatiquement sur l'environnement de développement. Les branches de feature offrent un déploiement manuel via l'interface GitLab, permettant de prévisualiser un changement sans polluer l'environnement de dev.

Le fichier final .gitlab-ci.yml

stages:
   - build
   - deploy
cache:
   paths:
       - node_modules/
.build_site:
   stage: build
   image: node:lts
   before_script:
       - npm install
   artifacts:
       paths:
           - dist

build_site_prod:
   extends: .build_site
   script:
       - npm run generate --fail-on-error --quiet --no-optional
   only:
       - master

build_site_dev:
   extends: .build_site
   script:
       - npm run generate --fail-on-error --quiet --no-optional
   only:
       - dev

build_site_branch:
   extends: .build_site
   script:
       - npm run generate --fail-on-error --quiet --no-optional
   when: manual
   except:
       - master
       - dev

variables:
   AWS_DEFAULT_REGION: eu-central-1

.deploy_s3:
   image: python:latest
   stage: deploy
   before_script:
       - pip install awscli

deploy_s3_branch:
   needs:
       - build_site_branch
   extends: .deploy_s3
   when: on_success
   except:
       - master
       - dev
   script:
       - aws s3 sync dist s3://dev.exemple.com --delete --cache-control "max-age=31536000" --expires 2030-01-01T00:00:00Z
       - aws cloudfront create-invalidation  --distribution-id ${AWS_CLOUDFRONT_INVALIDATION_ID_DEV} --paths "/*"
   environment:
       name: developement
       url: https://dev.exemple.com

deploy_s3_dev:
   needs:
       - build_site_dev
   extends: .deploy_s3
   when: on_success
   only:
       - dev
   script:
       - aws s3 sync dist s3://dev.exemple.com --delete --cache-control "max-age=31536000" --expires 2030-01-01T00:00:00Z
       - aws cloudfront create-invalidation  --distribution-id ${AWS_CLOUDFRONT_INVALIDATION_ID_DEV} --paths "/*"
   environment:
       name: developement
       url: https://dev.exemple.com

deploy_s3_prod:
   extends: .deploy_s3
   only:
       - master
   before_script:
       - pip install awscli
   script:
       - aws s3 sync dist s3://www.exemple.com --delete --cache-control "max-age=31536000" --expires 2030-01-01T00:00:00Z
       - aws cloudfront create-invalidation  --distribution-id ${AWS_CLOUDFRONT_INVALIDATION_ID_PROD} --paths "/*"
   environment:
       name: production
       url: https://www.exemple.com

L'utilisation des templates YAML (.build_site, .deploy_s3) évite la duplication et facilite la maintenance. Le mot-clé needs crée des dépendances explicites entre jobs, permettant à GitLab d'optimiser l'ordonnancement du pipeline.

Stratégies d'invalidation de cache

L'invalidation --paths "/*" est la méthode la plus simple mais aussi la plus coûteuse. AWS facture au-delà de 1000 invalidations par mois. Sur un projet avec des déploiements fréquents, la facture peut surprendre.

Deux approches alternatives existent. La première consiste à invalider uniquement les fichiers modifiés en comparant le build courant avec le contenu du bucket via aws s3 sync --dryrun, puis en ciblant les chemins concernés. La seconde, plus élégante, repose sur le versioning des assets : puisque Nuxt.js génère des noms hashés, seul index.html et les fichiers à chemin fixe nécessitent une invalidation. L'invalidation ciblée de quelques fichiers est gratuite et instantanée.

Architecture CDN : aller au-delà du setup initial

Déploiement blue-green avec CloudFront

Pour les sites à fort trafic, le déploiement blue-green élimine tout temps d'indisponibilité. Le principe : maintenir deux buckets S3 (blue et green), déployer sur le bucket inactif, puis basculer l'origin de la distribution CloudFront. En cas de problème, le rollback est immédiat puisque l'ancien bucket reste intact.

CloudFront supporte nativement les origin groups avec failover automatique. Si l'origin primaire retourne une erreur 5xx, CloudFront bascule transparentement sur l'origin secondaire. Cette mécanique, combinée au blue-green, offre une résilience remarquable.

Multi-région et latence

CloudFront distribue déjà le contenu mondialement, mais le bucket S3 source reste dans une seule région. Pour les audiences géographiquement dispersées, la réplication cross-region de S3 rapproche les fichiers source des edge locations CloudFront, réduisant la latence lors des cache misses.

Le choix de la région primaire impacte aussi le coût des transferts de données. Les régions européennes et américaines offrent des tarifs plus avantageux que l'Asie-Pacifique pour les transferts sortants vers CloudFront.

Infrastructure as Code avec Terraform

À l'échelle, la création manuelle de buckets et de distributions via la console devient un frein. Terraform permet de décrire l'ensemble de l'infrastructure (S3, CloudFront, IAM, Route 53, ACM) dans des fichiers versionnés. Chaque environnement devient reproductible, auditable et destructible en une commande. Le state Terraform, stocké lui-même dans un bucket S3 dédié, constitue la source de vérité de l'infrastructure.

Cette approche transforme radicalement le workflow : créer un nouvel environnement de preview par merge request ne nécessite plus d'intervention manuelle dans la console AWS, mais un simple terraform apply paramétré par le nom de la branche.

Optimisation des coûts

Trois leviers principaux permettent de maîtriser la facture AWS sur ce type d'architecture. Le premier est la classe de prix CloudFront : la classe 100 (Amérique du Nord et Europe uniquement) coûte significativement moins que la distribution mondiale. Le second est la politique de cache : un TTL élevé sur les assets immutables réduit les requêtes origin et donc les coûts S3. Le troisième est le lifecycle management sur S3 : configurer des règles de suppression automatique des anciens fichiers évite l'accumulation silencieuse de données obsolètes.

Pour aller plus loin