Nous faisons tourner trois applications Next.js sur AWS App Runner depuis mars 2026, réparties sur des comptes de dev, de staging et de production, pour une plateforme cliente avec de vrais utilisateurs et de l'argent réel qui y circule. Six mois, c'est assez long pour se heurter aux angles vifs. Voici le bilan que nous aurions aimé lire avant de faire ce choix.
Version courte : App Runner est le bon choix pour une petite équipe qui veut des conteneurs sur AWS sans avoir à posséder un ALB, une configuration VPC et le cycle de vie des définitions de tâches ECS. C'est le mauvais choix si vous avez besoin d'un contrôle fin sur le réseau, de requêtes longues, ou d'un comportement de déploiement qui va au-delà d'« échanger cette image contre celle-là ». Nous referions ce choix pour ce projet, avec les réserves ci-dessous écrites dès la première page du runbook.
Entre quelles options nous choisissions
La plateforme avait été initialement prévue pour Amplify Hosting. Quand nous avons reconstruit l'infrastructure en CDK, trois options étaient sur la table :
- ECS Fargate derrière un ALB. Le standard du secteur. Contrôle total, plomberie complète : VPC, sous-réseaux, security groups, target groups, règles de listener, définitions de tâches, auto-scaling du service. Nous en avons parlé dans un guide séparé.
- Amplify Hosting. Zéro opération pour Next.js, mais il veut posséder l'intégration Git et le build, et son runtime côté serveur est une boîte noire qu'on ne peut pas ajuster.
- App Runner. Pousser une image vers ECR, obtenir une URL HTTPS. Pas d'ALB, pas de VPC nécessaire pour le chemin public, auto-scaling natif, une ressource CloudFormation qu'on peut gérer depuis CDK comme n'importe quelle autre.
App Runner l'a emporté sur la surface opérationnelle. Pour une équipe de deux ou trois personnes qui écrivent aussi le code applicatif, chaque morceau de réseau que l'on ne possède pas est un morceau qu'on n'a pas à déboguer à 23h.
Le bon côté
HTTPS et une URL dès le départ. Chaque service reçoit un nom d'hôte *.awsapprunner.com avec un certificat géré. Les domaines personnalisés se réduisent à un CNAME plus un enregistrement de validation. Nous n'avons pas touché à ACM pour les noms d'hôte des applications.
La ressource CloudFormation est complète. AWS::AppRunner::Service expose l'identifiant de l'image, la taille de l'instance, la configuration d'auto-scaling, le health check, les variables d'environnement et les secrets. Cela signifie que le déploiement se fait à l'intérieur de CloudFormation quand le tag de l'image change, ce qui a un avantage subtil : le rôle d'exécution de CloudFormation détient la permission iam:PassRole que l'utilisateur humain qui déploie n'a pas besoin d'avoir. Les déploiements simultanés se sérialisent au niveau de la stack, si bien que deux ingénieurs ne peuvent pas écraser mutuellement leur déploiement.
L'injection des secrets est bien faite. runtimeEnvironmentSecrets récupère les valeurs depuis Secrets Manager au démarrage du conteneur et accorde au rôle de l'instance la permission GetSecretValue exactement sur ces ARN. Les valeurs sensibles n'atterrissent jamais dans le template. Nous l'utilisons pour les identifiants de base de données, les jetons d'API tiers et la clé privée du web push.
L'auto-scaling est ennuyeux dans le bon sens du terme. La configuration par défaut (min. 1, max. 25, 100 requêtes simultanées par instance) convenait très bien aux applications internes. Pour l'application publique, nous avons fixé un minimum de deux instances chaudes, ce qui a absorbé le trafic du jour de lancement sans montée en charge à froid.
Redimensionner une instance tient en une ligne. Quand le calcul des revenus côté serveur de l'application financière s'est révélé être du JavaScript limité par le CPU, et qu'une demi-vCPU faisait mettre 20 secondes à une page pour s'afficher, nous sommes passés à 1 vCPU / 2 Go dans config.ts. Déploiement, terminé. Le même changement sur ECS, c'est une nouvelle révision de définition de tâche plus une mise à jour du service plus une attente.
Le mauvais côté
Chaque point ci-dessous nous a coûté au moins un après-midi.
Un timeout de passerelle de 120 secondes, non configurable. App Runner ferme toute requête qui dépasse 120 secondes avec un 504. Notre application financière avait une grille de données qui, lorsque le retard de matérialisation dépassait environ 250 lignes, retombait sur un calcul complet à la lecture et dépassait la limite. La correction venait de notre côté (matérialiser de façon incrémentale, ajouter une route de santé qui signale « not ready » pendant le réchauffement des caches), mais c'est le genre de contrainte qu'on veut connaître avant de concevoir ses routes d'API. Tout ce qui est long doit aller dans une Lambda ou une file d'attente, pas derrière App Runner.
Redéployer le même tag d'image ne fait rien. Nous épinglons chaque service sur une image taguée avec le SHA git. Si vous reconstruisez le même SHA avec une configuration de build différente, la chaîne du tag reste inchangée, CloudFormation ne voit aucune différence sur ImageIdentifier, et App Runner continue de servir l'image qu'il a déjà récupérée — même si le tag dans ECR pointe désormais vers un digest différent. Cela nous a mordus en basculant un feature flag via le contexte CDK sans changement de code. La règle désormais : chaque déploiement fait avancer le SHA. Committez le changement de configuration.
L'environnement de build versus l'environnement d'exécution, c'est votre problème. Next.js intègre les variables NEXT_PUBLIC_* au moment du build. Les variables d'environnement d'exécution sur le service App Runner ne peuvent pas les modifier. Notre script de déploiement lit désormais l'environnement de build à partir du template CloudFormation fraîchement synthétisé et le transmet par build à CodeBuild, puis vérifie en cherchant dans les chunks JavaScript servis. Avant que nous fassions cela, une variable publique modifiée mettait deux déploiements à atteindre le navigateur.
Une nouvelle instance prend environ une minute. Récupérer l'image, démarrer Node, réchauffer les caches. Avec un health check TCP, l'instance entre en rotation dès que le port s'ouvre, ce qui, pour notre application financière, signifiait servir une première requête froide et lente. Passer à un health check HTTP sur une route dédiée /api/health, qui renvoie 503 tant que le réchauffement n'est pas terminé, a réglé le problème. Faites-le dès le premier jour.
Le réglage par défaut, ce sont les health checks TCP. Voir ci-dessus. C'est un piège pour tout ce qui réchauffe des caches au démarrage.
Pas de GitHub Actions ? Alors pas de déploiement App Runner depuis la console non plus. Comme CloudFormation possède le service, le bouton « Deploy » de la console est en pratique hors limites : tout changement manuel est écrasé par la prochaine mise à jour de la stack. Nous avons dû construire notre propre script de déploiement pour le cas où la CI n'était pas disponible (pour des raisons de facturation, pendant un moment). C'est un script shell de 300 lignes avec un worktree épinglé sur la branche distante, un envoi git archive vers S3, des jobs CodeBuild parallèles et un déploiement CDK. Ça fonctionne, mais c'est à nous de le maintenir.
Le pipeline qui en a résulté
Les builds Docker se font dans CodeBuild plutôt que sur des laptops, en partie parce que les builds cross-architecture sur Apple Silicon sont pénibles, et en partie parce que cela garde le flux « construire l'image pour App Runner » à l'intérieur du compte, avec un rôle IAM qui ne peut pousser que vers les dépôts ECR de ce compte. Trois builds tournent en parallèle. Synth s'exécute avant les builds, parce qu'un template défectueux doit échouer en quelques secondes, pas après quinze minutes de Docker.
Coût
Nous ne pouvons pas publier la facture du client, mais nous pouvons en décrire la forme. App Runner facture la mémoire d'instance provisionnée tant qu'une instance existe (chaude, inactive), et le vCPU seulement pendant qu'elle traite activement des requêtes. Pour les applications internes à taille minimale avec une seule instance, c'est une erreur d'arrondi. Pour l'application publique avec deux instances chaudes de 2 vCPU / 4 Go, c'est la plus grosse ligne isolée de la facture de calcul, plus grande que Lambda et Aurora réunis, mais encore moins chère qu'une configuration ECS Fargate équivalente une fois qu'on y ajoute le tarif horaire de l'ALB et la passerelle NAT dont on aurait besoin pour des sous-réseaux privés.
La comparaison honnête : App Runner est légèrement plus cher par heure-vCPU que Fargate, et considérablement moins cher par heure-ingénieur.
Verdict
| Score | Remarque | |
|---|---|---|
| Mise en place et support CDK | 5/5 | Ressource CloudFormation complète, secrets bien gérés |
| Opérations au quotidien | 4/5 | Tranquille ; le réglage par défaut du health check est la seule chose que nous changerions |
| Modèle de déploiement | 3/5 | L'épinglage par tag d'image est déterministe, mais le piège du même tag est impitoyable |
| Contraintes sur les requêtes | 2/5 | Le timeout de 120 secondes est strict ; à anticiper |
| Coût | 4/5 | Plus cher par vCPU que Fargate, bien moins cher par ingénieur |
| Referions-nous ce choix | Oui | Pour une petite équipe qui déploie du Next.js sur AWS |
Si votre charge de travail est une application web avec des requêtes de quelques secondes tout au plus, que votre équipe est petite, et que vous préférez écrire du code produit plutôt que du code réseau, App Runner est l'un des services AWS offrant le meilleur rapport qualité-prix. Si vous avez besoin de websockets à longue durée de vie, de requêtes de plus de deux minutes, ou d'un accès uniquement privé avec un contrôle fin, allez directement vers ECS Fargate et acceptez la plomberie.
Besoin d'aide pour décider, ou pour migrer une application existante ? Nous pouvons vous aider — nous avons fait les deux dans les deux sens.