Skip to content
Des environnements de preview par pull request sur AWS, sans Vercel
← ← Retour aux Réflexions Cloud

Des environnements de preview par pull request sur AWS, sans Vercel

La revue de code a un plafond. Un relecteur peut lire un diff et raisonner dessus, mais il ne peut pas cliquer dessus. Pour un produit avec une interface, le commentaire de revue le plus utile est « je l'ai ouvert, j'ai essayé le parcours, et la deuxième étape perd l'état du formulaire », et vous n'obtenez ce commentaire que si le relecteur a une URL. Vercel a bâti une entreprise sur ce constat. Si votre application est un front-end statique, utilisez Vercel et arrêtez de lire.

La nôtre ne l'est pas. La plateforme que nous faisons tourner pour un client américain, ce sont trois applications Next.js plus une couche d'API, des Lambdas, des tables DynamoDB et une base Aurora, le tout défini en CDK et déployé sur AWS. Une preview front-end seule pointée vers le backend de dev partagé est un mensonge : elle montre au relecteur la nouvelle interface qui parle à l'ancienne API. Nous voulions tout le stack par pull request, et nous voulions que ça coûte environ un dollar. Voici ce que nous avons construit.

Ce qui est par pull request, et ce qui ne l'est pas

L'astuce, c'est de décider ce qui doit être dupliqué. Tout dupliquer est lent à créer et cher à conserver. Ne rien dupliquer, c'est Vercel. La ligne que nous avons tracée :

Par pull request · stack pr-123 Service App Runner · 0,25 vCPU / 0,5 Go Fonctions Lambda (toutes) Tables DynamoDB · à la demande · DESTROY Base Postgres pr_123 sur le cluster partagé créé en ~6 min · détruit au merge ou à la fermeture Partagé · compte de dev VPC · sous-réseaux · l'unique NAT gateway Cluster Aurora Serverless v2 (auto-pause) Dépôts ECR · projets CodeBuild Secrets Manager · clés KMS · WAF recherchés par nom depuis le stack PR, jamais créés par lui

Tout ce qui a un prix à l'heure et un temps de création lent est partagé : le VPC, le NAT, le cluster Aurora. Tout ce qui est gratuit au repos et rapide à créer est par PR : App Runner à taille minimale, les Lambdas, les tables DynamoDB. La base de données est le compromis. Un cluster séparé par PR prendrait 10 minutes et coûterait 40 $ par mois au plancher de 0,5 ACU ; une base séparée sur le cluster partagé, c'est un CREATE DATABASE et ça ne coûte rien. Les migrations tournent sur pr_123 à la création du stack, exactement comme en production, donc une PR qui change le schéma a une preview avec son propre schéma.

Le côté CDK

L'environnement existait déjà comme valeur de contexte CDK : dev, staging, prod. Une preview est une quatrième variante, pr, avec un numéro.

// bin/app.ts
const env = app.node.tryGetContext('env') ?? 'dev';
const pr = app.node.tryGetContext('pr');           // "123" ou undefined
const suffix = pr ? `-pr-${pr}` : '';

new PlatformStack(app, `platform-${env}${suffix}`, {
  env: accounts[env === 'pr' ? 'dev' : env],
  config: {
    ...configs[env === 'pr' ? 'dev' : env],
    removalPolicy: pr ? RemovalPolicy.DESTROY : configs[env].removalPolicy,
    warmInstances: pr ? 1 : configs[env].warmInstances,
    instanceSize: pr ? 'small' : configs[env].instanceSize,
    databaseName: pr ? `pr_${pr}` : 'app',
    previewToken: pr ? Secret.fromSecretNameV2(...).secretValue : undefined,
  },
});

Le stack lui-même ne sait pas qu'il est une preview. Il reçoit une configuration où la politique de suppression dit DESTROY, l'instance est petite, le nom de la base contient un numéro. Les ressources partagées sont importées avec Vpc.fromLookup, DatabaseCluster.fromDatabaseClusterAttributes et Repository.fromRepositoryName, ce que le stack de dev faisait déjà pour les références entre stacks. Le diff pour supporter les previews faisait moins de cent lignes, et l'essentiel était la plomberie de configuration ci-dessus.

Le côté GitHub Actions

Deux workflows. Le premier tourne sur pull_request avec les types opened, synchronize et reopened :

jobs:
  preview:
    runs-on: ubuntu-latest
    permissions: { id-token: write, contents: read, pull-requests: write }
    concurrency: preview-${{ github.event.number }}
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<dev-account>:role/github-deploy-dev
          aws-region: us-east-1
      - run: ./scripts/build-images.sh --tag pr-${{ github.event.number }}-${{ github.sha }}
      - run: npx cdk deploy platform-pr-${{ github.event.number }} --context env=pr --context pr=${{ github.event.number }} --require-approval never
      - run: ./scripts/comment-preview-url.sh ${{ github.event.number }}

Le second tourne sur pull_request avec le type closed, et c'est une seule étape : cdk destroy du même nom de stack, suivi de DROP DATABASE pr_123 via une petite Lambda qui a l'accès réseau au cluster. Mergée ou abandonnée, la PR emporte son environnement avec elle.

Le script de commentaire lit l'URL App Runner dans les sorties du stack et la poste une fois, puis édite le même commentaire à chaque push pour que la PR ne se remplisse pas de bruit de bot. L'URL est le nom d'hôte *.awsapprunner.com. Nous ne créons pas de domaines personnalisés pour les previews : une validation de certificat par PR ajoute cinq minutes et n'apporte rien.

Du push à l'URL : environ six minutes

Étape Temps
Checkout, prise de rôle 20 s
Build de trois images dans CodeBuild, en parallèle ~3 min
cdk deploy sur un nouveau stack, création du service App Runner incluse ~2,5 min
Migrations sur pr_123 10 s
Commentaire sur la PR 2 s

Aux pushes suivants, le stack existe, donc seul le tag d'image change et App Runner fait une mise à jour progressive : environ quatre minutes. Pas les quarante secondes de Vercel, mais assez rapide pour qu'un relecteur qui demande un changement le voie dans la même session.

PR ouverteou push build des imagestag pr-123-<sha> cdk deploy pr-123compte dev · DESTROY URL sur la PRun commentaire, édité mergée / ferméedestroy + drop db chaque push : même stack, nouveau tag d'image, mise à jour progressive en ~4 min balayeur TTL : tout stack pr-* de plus de 7 jours est détruit quoi qu'il arrive

Les trois choses qui l'empêchent de devenir un chaos

Un balayeur. Les workflows échouent. Un événement closed se perd quand GitHub a un incident, ou quand quelqu'un supprime la branche depuis la CLI. Une Lambda planifiée liste les stacks CloudFormation nommés platform-pr-*, vérifie la date de création et détruit tout ce qui a plus de sept jours. Elle a réellement servi quatre fois. Sans elle, les previews mortes s'accumulent à 5 $ par mois chacune, en silence.

Un token de preview. Les previews sont sur des URL publiques avec des données d'apparence réelle. Un middleware Next.js vérifie un cookie posé par /preview?token=…, le token étant lu depuis un secret partagé à l'exécution, pas figé au build. Les relecteurs cliquent une fois sur le lien du commentaire de PR, qui porte le token, puis naviguent normalement. Ce n'est pas une frontière de sécurité contre un attaquant déterminé, mais ça tient les robots et le partage accidentel à l'écart, et les données derrière sont des fixtures, jamais de la production.

Des fixtures, pas des données de prod. La base par PR est peuplée depuis un fichier de fixtures versionné dans le dépôt : une poignée de comptes, de commandes, les états qui comptent pour la revue. Copier des données de production dans les previews est le moyen le plus rapide de les faire fuiter, et ça rend aussi les previews lentes à créer. Si un relecteur a besoin d'un état précis, il l'ajoute aux fixtures, et toute preview future l'aura.

Ce que ça coûte

Élément Par PR, ouverte 3 jours
App Runner, 0,25 vCPU / 0,5 Go, surtout au repos ~0,50 $
DynamoDB à la demande, quelques milliers de requêtes ~0,01 $
Lambda ~0,00 $
Base de données sur le cluster partagé 0 $
CodeBuild, 3 images × ~4 builds ~1,20 $
Total ~1,70 $

Avec vingt pull requests par mois, c'est environ 35 $, dont CodeBuild fait l'essentiel. Le cluster Aurora partagé en dev existait déjà et se met en pause tout seul ; les previews le maintiennent éveillé un peu plus, ce qui est réel mais difficile à mesurer. Comparé à un siège Vercel Pro par développeur, c'est moins cher, et contrairement à Vercel, ça prévisualise aussi l'API et la couche de données.

Quand ne pas faire ça

Si votre backend est stable et que vos PR sont presque toujours du front-end, une preview front-end seule contre le dev partagé convient et est bien plus simple. Si votre stack prend vingt minutes à créer parce qu'il a un ALB, une instance RDS et un cluster Redis, les environnements par PR seront trop lents pour être utiles, et vous devriez regarder les namespaces sur un cluster partagé ou les bases éphémères avec branching. Et si vous avez plus d'une poignée de développeurs, surveillez les limites de débit de l'API CloudFormation : vingt cdk deploy concurrents dans un compte les atteindront.

Pour une équipe de trois, qui livre plusieurs pull requests par jour sur un stack qui se crée en six minutes, c'est le meilleur investissement en expérience développeur que nous ayons fait. Les revues sont devenues plus rapides, et « ça marche sur ma machine » a cessé d'être un argument, parce que la machine est la même.

Vous voulez des previews pour votre propre stack, sur votre propre compte AWS ? Parlons-en.