Services de conteneurs managés¶
Toutes les équipes n'exploitent pas Kubernetes, et toutes ne veulent pas maintenir une machine virtuelle. Azure Container Apps, Google Cloud Run et AWS ECS Fargate exécutent les mêmes images Turbo EA sans cluster à opérer, avec le PostgreSQL managé du même cloud. Cette page fournit pour chacun un modèle prêt à l'emploi sous deploy/ et dit clairement ce que chaque plateforme sait et ne sait pas faire. Si vous avez un cluster, la page Kubernetes et cloud et le chart Helm conviennent mieux ; sur un hôte unique, Docker Compose reste la voie la plus simple. Tout ce que dit Exploitation et mises à niveau des sauvegardes, des mises à niveau et de la garde de SECRET_KEY s'applique ici sans changement. Chacune des trois plateformes dispose aussi d'un module Terraform qui crée le même groupe de conteneurs ainsi que la base de données managée — voir Terraform.
AWS App Runner est volontairement écarté : il n'accepte plus de nouveaux clients depuis avril 2026 et n'a jamais pris en charge les sidecars ni les volumes persistants.
La forme commune¶
flowchart LR
U[Navigateur / client MCP] --> LB[Ingress de la plateforme ou équilibreur<br/>TLS se termine ici]
subgraph G[Un groupe de conteneurs — Container App / service Cloud Run / tâche ECS]
N[nginx de bordure :8920] --> F[frontend :8080]
N --> B[backend :8000 — une instance]
N -. optionnel .-> M[serveur MCP :8001]
M --> B
end
LB --> N
B --> P[(PostgreSQL managé)]
B --> V[(/app/data persistant<br/>Azure Files · Filestore · EFS)]
Les trois modèles construisent la même chose :
- Un groupe de conteneurs, des sidecars sur
localhost. Le nginx de bordure, le frontend, le backend et le serveur MCP optionnel tournent en sidecars dans un même espace de noms réseau ; la bordure relaie vershttp://127.0.0.1:8000,:8080et:8001. Une URL publique, un cycle de vie, un déploiement. - La bordure écoute sur 8920. Son port par défaut est 8080, que l'image du frontend occupe déjà dans le même espace de noms ; chaque modèle définit donc
NGINX_HTTP_PORT=8920et y dirige l'ingress de la plateforme. La bordure conserve tous les en-têtes de sécurité, la limite de 512 Mo pour les imports d'espace de travail, les réglages du flux d'événements et le routage/mcp— rien côté plateforme ne la remplace. - Un backend, jamais mis à l'échelle, jamais à zéro. Le backend porte un état interne au processus (le bus d'événements temps réel, le limiteur de débit, le cache des permissions) et exécute des boucles d'arrière-plan ; il tourne donc en exactement une instance avec le CPU alloué en permanence : nombre d'instances minimal et maximal de un sur chaque plateforme.
- Les déploiements se chevauchent sur deux des trois plateformes. Container Apps et Cloud Run gardent l'ancienne instance en service jusqu'à ce que la nouvelle soit prête : pendant quelques secondes à quelques minutes, deux backends tournent côte à côte à chaque déploiement. Le backend prend donc un verrou consultatif PostgreSQL autour de ses migrations et de son amorçage au démarrage : la seconde instance attend, constate que le schéma est déjà à jour et continue. Les boucles d'arrière-plan tournent encore en double pendant cette fenêtre ; elles sont idempotentes. ECS arrête l'ancienne tâche avant de lancer la nouvelle (une à deux minutes d'indisponibilité par déploiement) et n'a pas besoin de cette précaution.
- Un
/app/datapersistant, détenu par l'uid 1000, contient les extensions installées, les fichiers envoyés et les bundles de transfert d'espace de travail. Les fiches et les diagrammes vivent dans PostgreSQL. - TLS se termine à la bordure de la plateforme.
TURBO_EA_TLS_ENABLEDrestefalse; unpublicUrlcommençant parhttps://marque le cookie de sessionsecureet alimente CORS. - Les secrets viennent du coffre de la plateforme — secrets Container Apps ou Key Vault, Secret Manager, Secrets Manager — jamais en clair dans le modèle.
- Les tags d'image sont le numéro de version.
2.141.0exécute les images2.141.0sur chaque plateforme.
Ce que chaque plateforme sait et ne sait pas faire¶
| Azure Container Apps | Google Cloud Run | AWS ECS Fargate | |
|---|---|---|---|
/app/data persistant |
Azure Files (SMB) monté avec uid=1000 |
Filestore via NFS uniquement — 100 Gio en régional (deux régions) ou 1 Tio ailleurs ; Cloud Storage FUSE n'est pas POSIX, évaluation seulement | EFS via un point d'accès (uid/gid 1000) |
| Arrêter l'ancienne instance avant la nouvelle | Non en mode révision unique ; oui avec plusieurs révisions et une désactivation manuelle | Non — les révisions se chevauchent toujours | Oui (minimumHealthyPercent 0, maximumPercent 100) |
| Flux d'événements (SSE longue durée) | Coupé toutes les 240 s par l'ingress ; le navigateur se reconnecte | Jusqu'à 3600 s par requête, puis reconnexion | Délai d'inactivité de l'équilibreur jusqu'à 4000 s |
| Plus gros envoi (l'import d'espace de travail va jusqu'à 512 Mo) | Non documenté par Microsoft — testez un import de 512 Mo avant de compter dessus | 32 Mio par requête en HTTP/1 | Aucune limite de plateforme |
| TLS et domaine personnalisé | Certificat managé sur l'app | Équilibreur externe global + NEG serverless + certificat géré par Google | Certificat ACM sur l'ALB |
| Secrets | Secrets de l'app ou références Key Vault | Secret Manager | Secrets Manager |
| Shell dans un conteneur | az containerapp exec |
aucun | ECS Exec |
| Système de fichiers racine en lecture seule | indisponible | pas un réglage | possible, mais désactive ECS Exec (désactivé dans le modèle) |
Azure Container Apps¶
Prérequis.
- Un groupe de ressources et un Azure Database for PostgreSQL Flexible Server joignable depuis l'environnement : intégré au VNet (son propre sous-réseau délégué dans le même VNet) ou joignable via un point de terminaison privé. TLS est exigé par le serveur et négocié automatiquement ; le nom d'utilisateur est le simple nom du rôle.
- Pour un accès privé à la base, un sous-réseau d'au moins
/27délégué àMicrosoft.App/environments, passé eninfrastructureSubnetId. Ne le laissez vide que pour une évaluation contre un serveur joignable publiquement. - Un compte de stockage avec un partage de fichiers pour
/app/data:az storage account create -g turbo-ea -n turboeadata -l westeurope --sku Standard_LRS az storage share-rm create --storage-account turboeadata --name turbo-ea-data --quota 50 - Deux secrets —
SECRET_KEY(openssl rand -base64 48) et le mot de passe de la base — passés en paramètres sécurisés, ou référencés depuis Key Vault (voir le commentaire dans le modèle).
Déployer. Modifiez deploy/azure-container-apps/main.bicepparam, exportez les trois secrets qu'il lit dans l'environnement, puis lancez :
export TURBO_EA_SECRET_KEY="$(openssl rand -base64 48)"
export TURBO_EA_POSTGRES_PASSWORD='…'
export TURBO_EA_STORAGE_KEY="$(az storage account keys list -n turboeadata --query '[0].value' -o tsv)"
az deployment group create -g turbo-ea \
-f deploy/azure-container-apps/main.bicep -p deploy/azure-container-apps/main.bicepparam
Le déploiement renvoie le FQDN par défaut de l'app. Au premier lancement, mettez publicUrl à https://<ce FQDN> ; une fois un domaine personnalisé lié avec un certificat managé (az containerapp hostname add puis az containerapp hostname bind --validation-method CNAME), mettez publicUrl à ce domaine et redéployez — l'URL du navigateur doit correspondre à publicUrl pour les cookies et CORS. Le premier utilisateur inscrit devient administrateur.
Mises à niveau. Changez imageTag et redéployez. En mode révision unique (défaut du modèle), l'ancienne et la nouvelle réplique se chevauchent un instant ; le verrou de démarrage du backend rend cela sûr. Pour un arrêt-puis-démarrage strict, passez l'app en mode révisions multiples, désactivez la révision en cours, puis déployez :
az containerapp revision set-mode -g turbo-ea -n turbo-ea --mode Multiple
az containerapp revision deactivate -g turbo-ea -n turbo-ea --revision <actuelle>
az deployment group create … # puis activez la nouvelle révision et dirigez-y le trafic
Limites à connaître. L'ingress ferme chaque requête après 240 secondes : le flux d'événements se reconnecte toutes les quatre minutes — l'application le tolère, cela se voit comme un court délai après chaque reconnexion. Microsoft ne documente aucune limite de taille de requête ; testez un import d'espace de travail de taille réaliste avant de compter dessus. Container Apps n'offre ni système de fichiers racine en lecture seule ni réglages de contexte de sécurité ; les images tournent déjà avec un utilisateur non root. Les sondes plafonnent failureThreshold à 10, d'où la sonde de démarrage du backend toutes les 30 secondes pour un budget de cinq minutes.
Google Cloud Run¶
Prérequis.
- Un VPC et un sous-réseau pour la sortie VPC directe ; le service atteint Cloud SQL et Filestore par ce biais.
- Une instance Cloud SQL pour PostgreSQL avec une IP privée dans ce VPC (accès aux services privés). Le modèle se connecte en
host:port, sans proxy. - Une instance Filestore pour
/app/data— la seule option persistante et pleinement POSIX sur Cloud Run. Son partage appartient à root à la création ; lancez une fois un job qui le rend accessible en écriture à l'uid 1000 avant le premier déploiement :gcloud filestore instances create turbo-ea-data --zone=europe-west1-b --tier=BASIC_HDD \ --file-share=name=share,capacity=1TB --network=name=default gcloud run jobs create chown-data --image=alpine --region=europe-west1 \ --network=default --subnet=default --add-volume=name=data,type=nfs,location=FILESTORE_IP:/share \ --add-volume-mount=volume=data,mount-path=/data --command=chown --args=-R,1000:1000,/data gcloud run jobs execute chown-data --region=europe-west1 --wait - Deux secrets Secret Manager,
turbo-ea-secret-keyetturbo-ea-postgres-password, et un compte de service d'exécution avecroles/secretmanager.secretAccessoretroles/cloudsql.client. - Cloud Run ne peut pas tirer directement depuis
ghcr.io. Créez une fois un dépôt distant Artifact Registry, puis référencez les images à travers lui comme le fait le modèle :gcloud artifacts repositories create ghcr --repository-format=docker --location=europe-west1 \ --mode=remote-repository --remote-docker-repo=https://ghcr.io
Déployer. Remplacez chaque espace réservé UPPER_CASE dans deploy/cloud-run/service.yaml — projet, région, réseau, IP Filestore, IP privée Cloud SQL, URL publique — et appliquez-le :
gcloud run services replace deploy/cloud-run/service.yaml --region europe-west1
Pour le nom d'hôte public, placez un équilibreur de charge d'application externe global devant le service avec un NEG serverless et un certificat géré par Google (gcloud compute network-endpoint-groups create … --network-endpoint-type=serverless --cloud-run-service=turbo-ea), pointez le DNS dessus, mettez TURBO_EA_PUBLIC_URL, ALLOWED_ORIGINS et MCP_PUBLIC_URL dans le manifeste à ce nom d'hôte, appliquez à nouveau, puis passez l'annotation d'ingress à internal-and-cloud-load-balancing pour que l'URL run.app cesse de répondre. Les mappages de domaine sur Cloud Run sont encore en aperçu et déconseillés en production.
Mises à niveau. Changez les tags d'image et appliquez à nouveau le manifeste. La nouvelle révision démarre pendant que l'ancienne sert encore ; le verrou de démarrage du backend empêche les deux de migrer en même temps, et Cloud Run n'offre pas d'option « arrêter l'ancienne d'abord ».
Limites à connaître. Cloud Run rejette les corps de requête au-delà de 32 Mio en HTTP/1 : un import de transfert d'espace de travail plus grand échoue sur Cloud Run — faites les gros imports sur Kubernetes ou une VM. Le flux d'événements est fermé après 3600 secondes puis reconnecté. La taille minimale de Filestore est le principal poste de coût de cette configuration ; un bucket Cloud Storage monté via FUSE est bon marché mais pas POSIX (pas de verrouillage, la dernière écriture gagne) : correct pour un essai, mauvais pour des extensions installées en production ; un volume en mémoire perd /app/data à chaque révision.
AWS ECS Fargate¶
Prérequis.
- Un VPC avec deux sous-réseaux publics (équilibreur) et deux sous-réseaux privés (tâche, cibles de montage EFS) disposant d'un accès NAT pour tirer les images depuis
ghcr.io. - Une instance RDS pour PostgreSQL dans les sous-réseaux privés. Passez son groupe de sécurité en
DbSecurityGroupIdet la pile ouvre le port 5432 depuis la tâche ; sinon ouvrez-le vous-même avec la sortieTaskSecurityGroupId. - Un certificat ACM pour le nom d'hôte public, dans la même région.
- Deux secrets Secrets Manager contenant
SECRET_KEYet le mot de passe de la base en chaînes brutes (pour un secret JSON géré par RDS, ajoutez:password::à son ARN dans le paramètre).
Déployer.
aws cloudformation deploy --template-file deploy/ecs-fargate/template.yaml \
--stack-name turbo-ea --capabilities CAPABILITY_IAM \
--parameter-overrides VpcId=vpc-… PublicSubnetIds=subnet-a,subnet-b PrivateSubnetIds=subnet-c,subnet-d \
PublicUrl=https://ea.example.com ImageTag=2.141.0 DbHost=turbo-ea.abc.eu-central-1.rds.amazonaws.com \
DbPasswordSecretArn=arn:aws:secretsmanager:… SecretKeySecretArn=arn:aws:secretsmanager:… \
CertificateArn=arn:aws:acm:… DbSecurityGroupId=sg-…
Pointez le nom d'hôte sur la sortie AlbDnsName (CNAME ou alias Route 53) et ouvrez-le. La pile crée le cluster, un système de fichiers EFS chiffré avec un point d'accès détenu par l'uid 1000, l'équilibreur avec un écouteur HTTPS et une redirection HTTP, et un service qui exécute une tâche.
Mises à niveau. Redéployez avec un nouvel ImageTag. Le service arrête la tâche en cours avant de lancer la remplaçante — un vrai arrêt-puis-démarrage, le backend n'est donc jamais dupliqué, au prix d'une à deux minutes d'indisponibilité par déploiement.
Exploitation. EFS est sauvegardé par AWS Backup (le modèle active la politique par défaut) ; associez ses points de restauration à vos instantanés RDS. aws ecs execute-command --cluster turbo-ea --task <id> --container backend --interactive --command sh ouvre un shell dans n'importe quel conteneur. Le délai d'inactivité de l'équilibreur est porté à 4000 secondes pour le flux d'événements, et il n'impose aucune limite de taille de corps.
Dépannage¶
| Symptôme | Cause et remède |
|---|---|
| Le nginx de bordure ne devient jamais sain alors que les journaux du backend sont bons | Dans une disposition en sidecars, les variables d'upstream doivent pointer sur 127.0.0.1, et NGINX_HTTP_PORT doit correspondre au port visé par l'ingress de la plateforme (8920 dans chaque modèle). |
| Le backend journalise another Turbo EA instance holds the startup lock — waiting | Attendu un instant pendant un déploiement sur Container Apps ou Cloud Run. Si cela ne disparaît jamais, l'ancienne révision est bloquée : désactivez-la (Container Apps) ou supprimez-la (Cloud Run). |
Permission denied sous /app/data |
Le volume n'appartient pas à l'uid 1000 : vérifiez les options de montage Azure Files, lancez le job chown Filestore ou contrôlez l'utilisateur POSIX du point d'accès EFS. |
| La connexion boucle ou l'API répond 401 dans le navigateur | publicUrl (et les valeurs TURBO_EA_PUBLIC_URL / ALLOWED_ORIGINS qui en découlent) ne correspond pas à l'URL de la barre d'adresse. |
| Les mises à jour temps réel marquent une pause toutes les quatre minutes sur Container Apps | Le délai de requête de l'ingress ; le navigateur se reconnecte et rien n'est perdu. |
| Un import d'espace de travail échoue à 32 Mio sur Cloud Run | La limite HTTP/1 de la plateforme sur les corps de requête ; faites les gros imports sur Kubernetes ou une VM. |
Le backend journalise too many connections |
L'offre managée plafonne les connexions sous DB_POOL_SIZE + DB_MAX_OVERFLOW ; réduisez le pool (budget de connexions). |