Aller au contenu
Serverless Containers en production

Serverless Containers en production

200 Pratiquer ⏱ 1 h 15 cloudscalewayserverlessdocker

À la fin, vous saurez

  • Organiser les conteneurs serverless par environnement et partager la configuration au niveau de l'espace de noms
  • Relier un conteneur serverless à une base de données par un réseau privé, et expliquer ce que ce lien permet et ne permet pas
  • Choisir entre les bacs à sable v1 et v2 en fonction du comportement de l'application
  • Régler la concurrence, les bornes de mise à l'échelle et les sondes, et prévoir l'effet d'un démarrage à froid
  • Rendre un conteneur privé, lui donner un nom de domaine et le déclencher par un horaire
  • Déployer depuis la CI et reconnaître une version en échec avant qu'elle ne coupe le service

Prérequis

Testé avec scaleway-cli 2.62.0 serverless-containers-api v1 , vérifié le 5 octobre 2026

Pourquoi

La préproduction de Signalements coûte cher pour ce qu'elle fait. Deux instances tournent jour et nuit, week-end compris, pour une équipe qui y teste une demi-heure par jour et des recetteurs de métropoles qui s'y connectent quelques fois par semaine. Le reste du temps, elles attendent. C'est exactement le cas que le serverless promet de régler : ne payer que lorsque quelqu'un appelle l'application.

Le cours précédent a fait l'essai, dans IaaS, PaaS, serverless et la responsabilité partagée : l'image de Signalements a tourné quelques minutes sur Serverless Containers, sans base de données, et l'on a vu le contrat du serverless (application sans état, démarrage à froid, données ailleurs). Cette leçon passe de l'essai à un service qu'une équipe exploite. Il faut maintenant :

  • séparer préproduction et production, et partager la configuration commune ;
  • joindre la base PostgreSQL sans l'exposer à Internet ;
  • savoir quand une version est saine, et ce qui se passe quand elle ne l'est pas ;
  • maîtriser le nombre d'instances, donc la facture et la charge sur la base ;
  • donner un vrai nom de domaine, et fermer ce qui doit l'être ;
  • déployer depuis la CI, et observer.

Chacun de ces besoins se heurte à une limite documentée du service. Ces limites ne sont pas des défauts cachés : elles sont écrites dans la documentation de Scaleway, et elles décident, plus que les fonctionnalités, de ce que l'on peut confier au service. La leçon les présente au moment où on les rencontre.

Les concepts

Ce que l'on règle, et à quel niveau

Un espace de noms (namespace) regroupe des conteneurs d'une région et d'un projet. Il porte des variables d'environnement et des secrets partagés par tous ses conteneurs ; une variable définie au niveau du conteneur l'emporte sur celle du même nom définie au niveau de l'espace de noms, et la configuration de Serverless Containers l'emporte sur une variable fixée dans le Dockerfile. Un espace de noms par environnement et par projet est l'organisation naturelle : sig-preprod dans le projet signalements-preprod, sig-prod dans signalements-prod (leçon 1).

Pour chaque conteneur, les réglages qui comptent en production se rangent en cinq familles :

FamilleParamètres (CLI)Ce qu'ils décident
Ressourcesmvcpu-limit, memory-limit-bytes, local-storage-limit-bytesCe que reçoit chaque instance, et ce que coûte chaque seconde d'exécution
Mise à l'échellemin-scale, max-scale, scaling-option.*Combien d'instances, selon quel critère
Santéstartup-probe.*, liveness-probe.*Quand une instance reçoit du trafic, quand elle n'en reçoit plus
Expositionprivacy, https-connections-only, protocol, domainesQui peut appeler, et comment
Exécutionsandbox, command, args, private-network-id, timeoutDans quel environnement le code tourne, et ce qu'il peut joindre

Sans précision, une instance reçoit 1 000 mvCPU (un vCPU) et 2 048 Mo de mémoire. La documentation borne ces valeurs entre 70 et 6 000 mvCPU et entre 128 et 12 228 Mo par conteneur.

La mise à l'échelle : concurrence, CPU ou mémoire

Une instance est une copie du conteneur en cours d'exécution. Le service en démarre et en arrête selon un seul critère, à choisir :

  • la concurrence (scaling-option.concurrent-requests-threshold) : le nombre de requêtes qu'une instance traite en même temps avant que l'on en démarre une autre. C'est le critère par défaut, avec un seuil de 80 requêtes simultanées par instance, qui est aussi le maximum ;
  • l'usage du processeur ou de la mémoire (cpu-usage-threshold, memory-usage-threshold), en pourcentage. Ces deux modes imposent min-scale à au moins 1 : pas de mise à zéro.

Deux durées gouvernent la descente : une instance inutilisée est arrêtée après 30 secondes sans requête, jusqu'à ce qu'il en reste une ; la dernière est arrêtée après 15 minutes d'inactivité, si min-scale vaut 0. C'est la mise à l'échelle à zéro (scale to zero) : plus rien ne tourne, plus rien ne se paie.

À l'autre bout, quand max-scale est atteint, les nouvelles requêtes attendent dans une file ; quand la file est pleine, le service répond 503. max-scale n'est donc pas seulement un plafond de dépense : c'est la capacité maximale de l'application, et il protège aussi ce qui est derrière. Pour Signalements, chaque instance ouvre des connexions à PostgreSQL ; cent instances lancées par un pic de trafic pourraient épuiser les connexions de la base bien avant d'épuiser la plateforme.

Le démarrage à froid

Quand aucune instance n'est disponible, la première requête attend que le service télécharge l'image, démarre le conteneur, puis attende qu'il écoute sur son port : c'est le démarrage à froid (cold start). La documentation donne les leviers, dans cet ordre : une image légère (sous 1 Go non compressé recommandé), une application qui démarre vite (pas de téléchargement ni de connexion lente au démarrage), le bac à sable v2, et, si la latence compte, une instance toujours prête (min-scale=1), facturée en permanence. Un déclencheur horaire peut aussi garder une instance chaude pendant les heures de travail seulement, sans payer les nuits (voir la pratique). Attacher un réseau privé allonge un peu le démarrage : il faut brancher l'instance au réseau et lui réserver une adresse.

Deux bacs à sable

Les conteneurs de plusieurs clients partagent les mêmes machines. Le service isole chacun dans un bac à sable (sandbox), au choix :

Bac à sable v1Bac à sable v2 (par défaut)
Technologie (FAQ de Scaleway)Kata Containers : chaque conteneur dans une micro-machine virtuellegVisor : un noyau écrit en Go, en espace utilisateur, intercepte les appels système
Démarrage à froidPlus lentPlus rapide
Appels système LinuxTousUne sélection (liste publiée par gVisor)
/tmp et /dev/dev limité à 64 Mo en mémoireEn mémoire, sans plafond propre : compté dans la mémoire du conteneur
Sous-processusCopie à l'écriture normalePas de copie à l'écriture : la mémoire croît avec le nombre de processus forkés
HorlogeDérive documentée d'environ deux secondes par 24 heures d'exécution continuePas de dérive signalée

La quatrième et la cinquième lignes concernent directement Signalements, dont l'image lance gunicorn avec deux workers, deux processus obtenus par fork du processus maître. En v2, chaque worker coûte sa pleine mémoire, imports Python compris. La documentation de Scaleway le dit pour ce cas précis : une application qui forke plusieurs sous-processus chargeant beaucoup de modules Python est mieux servie par le bac à sable v1. L'autre voie est de modifier l'image pour un seul worker avec plusieurs threads, ce que montre l'exemple Flask de la documentation (--workers 1 --threads 8).

Les sondes

Depuis l'API v1, deux sondes décident de la santé d'une instance :

  • la sonde de démarrage (startup probe) vérifie que l'application a démarré ; tant qu'elle n'a pas réussi, la sonde de vie ne tourne pas. Elle exige que l'application écoute sur toutes les interfaces (0.0.0.0), pas sur 127.0.0.1 ;
  • la sonde de vie (liveness probe) vérifie ensuite, à intervalle régulier, que l'instance peut recevoir du trafic. Par défaut : toutes les 10 secondes, délai d'une seconde, 30 échecs avant de déclarer l'instance en erreur. Le service ne redémarre pas une instance en erreur : il cesse de lui envoyer du trafic.

Chaque sonde est soit TCP (le port accepte-t-il une connexion ?), soit HTTP (un chemin répond-il ?). La documentation suggère d'utiliser la sonde HTTP pour vérifier « que tous les prérequis sont remplis, comme la connexion à la base ». Pour Signalements, c'est un choix à peser : la route GET /sante teste la base. Si PostgreSQL devient indisponible, toutes les instances échouent ensemble à la sonde, le service cesse de router vers chacune, et l'application disparaît, alors qu'elle aurait pu répondre une erreur propre ou servir des pages qui n'ont pas besoin de la base. C'est le même débat que celui des sondes de Kubernetes : une sonde qui teste une dépendance partagée transforme la panne d'une dépendance en panne totale. La pratique fait un choix et le justifie.

Le réseau privé, dans un seul sens

Depuis août 2025, tout espace de noms peut rattacher ses conteneurs à un réseau privé, et le routage VPC est pleinement fonctionnel depuis le 15 avril 2026. La documentation précise ce que cela permet :

  • le trafic sortant du conteneur vers une ressource du réseau privé passe par l'interface privée : Signalements peut joindre le point d'accès privé de PostgreSQL ;
  • la résolution DNS passe par le serveur DNS du VPC, qui résout les noms *.internal ;
  • chaque instance reçoit sa propre adresse dans le réseau privé, attribuée automatiquement ; on ne peut pas la réserver à l'avance, et un conteneur très sollicité consomme beaucoup d'adresses ;
  • le trafic entrant depuis le réseau privé n'est pas pris en charge au 5 octobre 2026 : une instance du réseau privé ne peut pas appeler le conteneur par une adresse privée. Le point d'accès public reste le seul, et la documentation indique qu'il ne peut pas encore être désactivé ;
  • un conteneur se rattache à un seul réseau privé.

Le rattachement est gratuit. Il change une chose importante pour la sécurité : la base peut rester sans point d'accès public, ce qui était impossible avant, puisque les adresses des instances serverless sont imprévisibles et ne permettent pas d'écrire une liste d'accès.

Ce qui n'existe pas

Trois absences pèsent plus que les fonctionnalités :

  • pas de versions ni de retour arrière. Quand un déploiement échoue, l'ancienne version continue de répondre pendant 24 heures au plus, puis la version en erreur et l'ancienne sont supprimées, et le service ne répond plus jusqu'au prochain déploiement réussi. Un échec de déploiement le vendredi soir devient une coupure le samedi soir ;
  • pas d'intégration avec Secret Manager : les secrets du conteneur sont des variables d'environnement secrètes, propres au service (leçon 8) ;
  • pas d'adresse IP fixe, ni en entrée ni en sortie, et donc pas de répartiteur de charge de Scaleway devant un conteneur (il n'accepte que des adresses comme cibles).

S'y ajoutent des restrictions précises : l'image doit être construite pour linux/amd64 ; HTTP/1.0 n'est pas accepté (HTTP/1.1 et HTTP/2 le sont) ; les ports sortants 25 et 465 sont bloqués, sauf vers le service d'e-mail transactionnel de Scaleway (leçon 10) ; les ports 8008, 8012, 8013, 8022, 8112, 9090 et 9091 sont réservés à la plateforme ; les variables dont le nom commence par SCW_ sont réservées ; chaque instance ne peut lancer que 20 résolutions DNS complètes par seconde.

En pratique

Les commandes ont été vérifiées avec l'aide de la CLI scw 2.62, qui parle à l'API v1 de Serverless Containers ; les champs extraits avec jq sont ceux de la structure Container du SDK Go. La leçon ne reproduit pas de sortie.

Préparer les variables et l'image

$ PROJET=$(scw account project list name=signalements-preprod -o json \
    | jq -r '.[] | select(.name == "signalements-preprod") | .id')
$ PN_ID=$(scw vpc private-network list project-id="$PROJET" region=fr-par -o json \
    | jq -r '.[] | select(.name == "pn-signalements") | .id')
$ echo "$PROJET $PN_ID"

Le filtre name= des commandes de liste cherche une sous-chaîne : le select de jq garde le nom exact.

L'image doit être dans le registre de Scaleway du même projet, pour des déploiements fiables (la documentation déconseille les registres externes en production, à cause de leurs limites de débit) et construite pour linux/amd64. La commande suivante copie l'index multi-architecture publié sur GitHub vers le registre de Scaleway, sans télécharger les couches sur le poste :

$ docker buildx imagetools create \
    --tag rg.fr-par.scw.cloud/signalements-<suffixe>/signalements:1.2.0 \
    ghcr.io/lyneko-formation/signalements:1.2.0

Il faut s'être authentifié au préalable sur le registre (leçon 7). Le service choisit la variante linux/amd64 de l'index ; une image construite seulement pour arm64, depuis un Mac à processeur Apple par exemple, échoue au déploiement.

Un espace de noms par environnement

$ NS_ID=$(scw container namespace create name=sig-preprod project-id="$PROJET" \
    environment-variables.APP_ENV=preprod \
    tags.0=app=signalements tags.1=env=preprod \
    region=fr-par --wait -o json | jq -r .id)

La variable APP_ENV sera vue par tous les conteneurs de l'espace de noms. Créer l'espace de noms crée aussi, côté IAM, une application nommée serverless-namespace-fr-par-<identifiant> et une politique du même nom, avec les jeux de permissions ContainerRegistryFullAccess et MessagingAndQueuingFullAccess : c'est l'identité dont la plateforme se sert pour tirer les images et gérer les identifiants des déclencheurs. Repérez-la (scw iam application list) : elle apparaîtra dans vos revues d'accès (leçon 15), et la supprimer casserait les déploiements.

La chaîne de connexion, en secret

La base sig-db du projet de préproduction a un point d'accès sur pn-signalements (cours précédent, leçon 6). Récupérez son adresse et son port, puis composez la chaîne de connexion sans la taper en clair :

$ DB_ID=$(scw rdb instance list project-id="$PROJET" region=fr-par -o json \
    | jq -r '.[] | select(.name == "sig-db") | .id')
$ scw rdb instance get "$DB_ID" region=fr-par -o json \
    | jq -r '.endpoints[] | select(.private_network != null) | "\(.ip):\(.port)"'
$ read -rs -p "DATABASE_URL : " DATABASE_URL; echo

read -rs lit la valeur sans l'afficher ni l'inscrire dans l'historique. Elle passera ensuite en argument à scw, donc sera visible quelques instants dans la liste des processus de votre poste : un risque résiduel acceptable sur un poste personnel, pas sur une machine partagée. La leçon 8 montre comment la CI peut lire cette valeur dans Secret Manager au lieu de la recevoir d'une personne.

Créer le conteneur

$ CT_ID=$(scw container container create \
    namespace-id="$NS_ID" name=signalements \
    image=rg.fr-par.scw.cloud/signalements-<suffixe>/signalements:1.2.0 \
    port=8000 \
    sandbox=v1 \
    mvcpu-limit=560 memory-limit-bytes=1GB \
    min-scale=0 max-scale=3 \
    scaling-option.concurrent-requests-threshold=20 \
    private-network-id="$PN_ID" \
    environment-variables.APP_VERSION=1.2.0 \
    secret-environment-variables.DATABASE_URL="$DATABASE_URL" \
    startup-probe.tcp=true \
    liveness-probe.tcp=true liveness-probe.interval=10s liveness-probe.failure-threshold=6 \
    https-connections-only=true \
    tags.0=app=signalements tags.1=env=preprod \
    region=fr-par --wait -o json | jq -r .id)
$ unset DATABASE_URL

Chaque choix se justifie :

  • port=8000 : le port sur lequel gunicorn écoute dans l'image. La valeur par défaut est 8080 ; la plateforme la transmet aussi dans la variable PORT, que Signalements ignore. Le conteneur est joint de l'extérieur sur 80 et 443 quel que soit ce port.
  • sandbox=v1 : l'image lance deux workers gunicorn par fork. En v2, leur mémoire s'additionne sans partage ; v1 conserve la copie à l'écriture, au prix d'un démarrage à froid plus lent. Quand l'équipe reconstruira l'image avec un seul worker et des threads, elle pourra passer en v2.
  • mvcpu-limit=560, memory-limit-bytes=1GB : un peu plus d'un demi-vCPU et un gigaoctet par instance, suffisants pour une API légère. Les ressources sont fixées par instance : c'est le nombre d'instances qui donne la capacité.
  • min-scale=0, max-scale=3 : la préproduction s'éteint au bout de quinze minutes sans visite, et ne dépasse jamais trois instances, donc six workers et au plus quelques dizaines de connexions à la base.
  • concurrent-requests-threshold=20 : deux workers synchrones ne traitent que deux requêtes à la fois ; au-delà, les requêtes attendent dans l'instance. Un seuil de 80 laisserait 78 requêtes en attente avant de démarrer une seconde instance. 20 est un compromis ; la bonne valeur se mesure, en observant la latence quand le nombre de requêtes simultanées augmente.
  • private-network-id : le trafic vers 172.16.20.x (le point d'accès privé de la base) passe par le réseau privé.
  • secret-environment-variables.DATABASE_URL : la valeur n'est plus réaffichée par la console ni par l'API. La documentation signale qu'une valeur sur plusieurs lignes est mal injectée et recommande alors de l'encoder en base64 ; une URL de connexion tient sur une ligne.
  • Sondes TCP : la sonde de démarrage attend que gunicorn écoute ; la sonde de vie vérifie que le port répond toutes les 10 secondes, et retire l'instance après six échecs, soit une minute. On n'utilise pas /sante comme sonde de vie, pour qu'une indisponibilité de PostgreSQL produise des erreurs 500 visibles plutôt qu'un service qui disparaît entièrement. La santé applicative, base comprise, est surveillée par une alerte dans Cockpit (leçon 14).
  • https-connections-only=true : les appels en HTTP clair sont refusés.

Le format de liveness-probe.interval et la conversion de 1GB en octets suivent les conventions de la CLI (durées et tailles lisibles). Vérifiez le résultat :

$ scw container container get "$CT_ID" region=fr-par -o json \
    | jq '{status, public_endpoint, sandbox, min_scale, max_scale, private_network_id}'
$ URL=$(scw container container get "$CT_ID" region=fr-par -o json \
    | jq -r '.public_endpoint | if startswith("http") then . else "https://" + . end')
$ curl -s "$URL/sante"

Le champ public_endpoint remplace, dans l'API v1, l'ancien domain_name (guide de migration) ; le filtre jq ajoute le préfixe https:// s'il n'y est pas, pour ne pas dépendre de sa forme exacte. Si la réponse indique que la base est joignable, le chemin par le réseau privé fonctionne.

Note

Le schéma de l'API v1 contient déjà des champs private_endpoint et default_public_endpoint_enabled. Au 5 octobre 2026, la documentation indique que l'appel entrant par le réseau privé et la désactivation du point d'accès public sont « en cours de développement ». Ne vous fiez pas à la présence d'un champ dans le SDK pour conclure qu'une fonction est disponible.

Observer un démarrage à froid

Laissez la préproduction sans visite plus de quinze minutes, puis mesurez :

$ curl -s -o /dev/null -w 'connexion %{time_connect}s, premier octet %{time_starttransfer}s\n' "$URL/sante"
$ curl -s -o /dev/null -w 'connexion %{time_connect}s, premier octet %{time_starttransfer}s\n' "$URL/sante"

La première requête porte le démarrage à froid dans son temps jusqu'au premier octet ; la seconde, servie par l'instance démarrée, ne le porte plus. L'écart mesure ce que vos utilisateurs subissent le lundi matin. Les journaux montrent le démarrage de gunicorn :

$ scw container container logs "$CT_ID" time-span=30m region=fr-par

Garder la préproduction chaude aux heures de bureau

Plutôt que min-scale=1, facturé nuit et week-end, un déclencheur horaire appelle le conteneur toutes les dix minutes en semaine, de 8 h à 19 h, heure de Paris :

$ scw container trigger create container-id="$CT_ID" name=garder-au-chaud \
    cron-config.schedule='*/10 8-18 * * 1-5' \
    cron-config.timezone=Europe/Paris \
    destination-config.http-path=/sante \
    destination-config.http-method=get \
    region=fr-par

Comme l'instance ne s'arrête qu'après quinze minutes d'inactivité, un appel toutes les dix minutes la garde en vie. Les déclencheurs horaires utilisent le format cron Unix ; le fuseau est explicite, ce qui évite le décalage d'une heure aux changements d'heure. La méthode HTTP s'écrit en minuscules dans la CLI. Ce même mécanisme sert aux vraies tâches planifiées, comme un rapport quotidien appelé sur une route dédiée.

Un domaine pour les recetteurs

Les recetteurs des métropoles retiennent mieux preprod.signalements.exemple.fr qu'un nom généré. Chez votre fournisseur DNS (leçon 10 pour celui de Scaleway), créez un enregistrement CNAME de ce nom vers la valeur de public_endpoint, vérifiez qu'il se résout, puis déclarez-le :

$ scw container domain create container-id="$CT_ID" \
    hostname=preprod.signalements.exemple.fr region=fr-par

La plateforme vérifie que le nom pointe bien vers le conteneur, puis obtient un certificat Let's Encrypt par un défi HTTP-01. Si ce défi échoue pendant trois minutes, le domaine passe en error, sans certificat. On ne peut pas fournir son propre certificat. Pour un domaine racine (exemple.fr sans sous-domaine), il faut un fournisseur DNS qui sache aplatir un CNAME ou proposer un enregistrement ALIAS.

Warning

Basculer un nom déjà en service (aujourd'hui un A vers les instances) ne se fait pas sans coupure : tant que le domaine n'est pas déclaré sur le conteneur, les clients qui ont déjà la nouvelle résolution reçoivent des 404 ; et il ne peut pas être déclaré tant que le DNS ne pointe pas vers lui. La documentation le décrit comme un problème de l'œuf et de la poule, et propose un CDN (leçon 11) pour servir une copie pendant la bascule. Pour la production, prévoyez-le.

Déployer depuis la CI

Depuis l'API v1, toute mise à jour du conteneur le redéploie : le paramètre redeploy est déprécié, et la commande deploy de la CLI, qui construit une image à partir des sources avec des buildpacks, n'est pas nécessaire quand la CI construit déjà l'image. L'étape de déploiement se réduit à :

$ scw container container update "$CT_ID" \
    image=rg.fr-par.scw.cloud/signalements-<suffixe>/signalements:"$VERSION" \
    environment-variables.APP_VERSION="$VERSION" \
    region=fr-par --wait
$ test "$(scw container container get "$CT_ID" region=fr-par -o json | jq -r .status)" = ready

L'identité de la CI est l'application sig-deploiement du cours précédent, à laquelle il faut ajouter le droit de modifier les conteneurs du projet de préproduction (leçon 15 pour le choix des jeux de permissions). Le déploiement est progressif : les anciennes instances servent le trafic jusqu'à ce que les nouvelles soient prêtes. Le contrôle du statut final est indispensable : une mise à jour qui finit en error laisse l'ancienne version répondre pendant 24 heures au plus, et le pipeline doit échouer tout de suite, pas le lendemain soir. Le champ error_message du conteneur explique l'échec.

Fermer ce qui doit l'être

Signalements doit rester joignable par le public. Mais le service de vignettes de la leçon 6, ou un outil d'administration, n'ont pas de raison de l'être. Un conteneur privé exige l'en-tête X-Auth-Token portant la clé secrète d'une application IAM qui a le jeu de permissions ContainersPrivateAccess sur le projet :

$ scw container container update "$CT_ID_ADMIN" privacy=private region=fr-par --wait
$ curl -s -o /dev/null -w '%{http_code}\n' "$URL_ADMIN/"

Sans en-tête, la réponse est 403. Les anciens jetons JWT propres au service (scw container token create) sont dépréciés au profit d'IAM.

Nettoyer

$ scw container namespace delete "$NS_ID" region=fr-par

Supprimer l'espace de noms supprime ses conteneurs, déclencheurs et domaines. L'image reste dans le registre (leçon 7).

Sous le capot

Deux façons d'isoler. Le bac à sable v1 s'appuie, selon la FAQ de Scaleway, sur Kata Containers : chaque conteneur tourne dans une micro-machine virtuelle avec son propre noyau Linux, ce qui offre la compatibilité complète avec les appels système et une isolation matérielle, mais coûte le démarrage d'un noyau. Le v2 s'appuie sur gVisor : un noyau réimplémenté en Go, qui tourne en espace utilisateur et répond lui-même aux appels système de l'application, sans les transmettre au noyau de la machine. Le démarrage est plus rapide, la surface exposée au noyau de l'hôte plus petite, mais seuls les appels système que gVisor implémente sont disponibles, et certains mécanismes du noyau, comme la copie à l'écriture entre processus forkés selon la documentation de Scaleway, ne se comportent pas comme sous Linux. Le choix du bac à sable est donc un choix de compatibilité autant que de performance.

Le chemin d'une requête. La requête arrive sur la passerelle de la plateforme, qui termine TLS et ajoute des en-têtes : X-Forwarded-For (l'adresse du client en premier), X-Forwarded-Proto et X-Request-ID, un identifiant unique que l'application peut journaliser et transmettre pour corréler les traces. D'autres en-têtes internes apparaissent (X-Envoy-External-Address, K-Proxy-Request) ; la documentation précise qu'ils ne sont pas garantis. L'autoscaler compte les requêtes en cours par instance ; au-delà du seuil, il démarre une instance ; si max-scale est atteint, la requête attend.

Le réseau privé, par instance. Chaque instance reçoit une adresse du réseau privé, réservée à son démarrage. La route vers les préfixes du VPC passe par cette interface ; le reste du trafic sortant passe par l'accès Internet de la plateforme, avec des adresses source imprévisibles. Le flux entrant, lui, n'arrive que par la passerelle publique. On a donc une architecture asymétrique : le conteneur peut parler au réseau privé, le réseau privé ne peut pas lui parler.

La facturation. Le service compte, pour chaque instance, la mémoire et les vCPU alloués multipliés par sa durée de vie, en gigaoctets-secondes et en vCPU-secondes. L'exemple de la FAQ, au 5 octobre 2026 et donné à titre d'illustration, applique 0,0000020 € par Go-s et 0,0000100 € par vCPU-s au-delà d'une franchise mensuelle de 400 000 Go-s et 200 000 vCPU-s ; le trafic entrant et sortant et le stockage éphémère ne sont pas facturés. Une instance de préproduction à 0,56 vCPU et 1 Go qui tourne dix heures par jour ouvré, environ 790 000 secondes par mois, consomme environ 790 000 Go-s et 442 000 vCPU-s : quelques euros, à vérifier sur la page de tarifs à la date du calcul. Les journaux et métriques avec la rétention par défaut (31 jours pour les métriques, 7 jours pour les journaux) sont inclus.

Pièges courants

Le conteneur ne démarre jamais. Trois causes couvrent l'essentiel : un port qui ne correspond pas au port d'écoute (8080 par défaut, 8000 pour Signalements) ; une application qui écoute sur 127.0.0.1, que la sonde de démarrage ne voit pas ; une image arm64. Le champ error_message et les journaux tranchent.

Le conteneur consomme trop de mémoire, ou s'arrête sur un manque de mémoire. En v2, chaque processus forké compte entièrement, et tout ce qui est écrit dans /tmp est en mémoire. Un traitement d'image qui écrit des fichiers temporaires de 200 Mo dans /tmp consomme 200 Mo de mémoire. Passez en v1, ou écrivez ailleurs, ou augmentez la mémoire.

Le service a disparu un samedi. Un déploiement a échoué la veille, personne ne l'a vu, et les 24 heures de grâce sont écoulées. Le pipeline doit vérifier le statut ready et échouer sinon ; une alerte sur l'état du conteneur doublera ce contrôle (leçon 14).

La base est saturée de connexions. max-scale élevé, seuil de concurrence bas, plusieurs workers par instance : le produit des trois donne le nombre de connexions possibles. Calculez-le, et comparez-le à la limite de la base (leçon 3).

Des variables qui n'arrivent pas. Une valeur sur plusieurs lignes (une clé PEM, par exemple) est mal injectée ; encodez-la en base64. Un nom qui commence par SCW_ est réservé.

L'horloge dérive. En v1, après une journée d'exécution continue, l'horloge peut avoir deux secondes d'écart. C'est sans effet pour Signalements, mais pas pour un service qui signe des jetons à courte durée de validité ou compare des horodatages entre machines.

Un client HTTP/1.0. Certains vieux outils de supervision parlent encore HTTP/1.0, que le service refuse. Le symptôme ressemble à une panne alors que le conteneur va bien.

Un e-mail qui ne part pas. Les ports 25 et 465 sont bloqués en sortie. Envoyez par le service d'e-mail transactionnel (leçon 10), ou par le port de soumission 587 d'un fournisseur.

Sécurité

Le point d'accès public ne se ferme pas. Au 5 octobre 2026, on ne peut ni désactiver l'adresse publique d'un conteneur, ni le joindre par le réseau privé. Un service interne doit donc être privé au sens IAM, avec une application dédiée qui porte ContainersPrivateAccess et rien d'autre, et une clé qui expire. Une API publique comme Signalements doit faire sa propre authentification, et se protéger elle-même contre les abus (limitation de débit à partir de X-Forwarded-For, en ne faisant confiance qu'à la première adresse ajoutée par la plateforme).

Les secrets du conteneur sont lisibles par qui peut modifier le conteneur. Les variables secrètes ne sont pas réaffichées, mais toute identité qui peut mettre à jour le conteneur peut les remplacer, et le code qui tourne dedans les lit en clair dans son environnement (avec les risques de fuite dans les journaux décrits à la leçon 8). La séparation des droits passe par des projets distincts pour la préproduction et la production.

L'identité créée avec l'espace de noms. L'application serverless-namespace-... porte ContainerRegistryFullAccess et MessagingAndQueuingFullAccess. C'est la plateforme qui l'utilise, mais c'est une identité avec des droits d'écriture sur vos images : elle doit apparaître dans vos inventaires et vos revues d'accès.

L'isolation est celle du fournisseur. La FAQ indique qu'aucun antivirus ne tourne dans les environnements des clients, que l'isolation repose sur Kata Containers et gVisor, que le stockage éphémère n'est pas chiffré au repos au niveau du système de fichiers du conteneur, et que du personnel de l'équipe peut accéder à l'infrastructure pour un débogage, « jamais sans l'autorisation explicite du client ». C'est la part du fournisseur dans la responsabilité partagée ; la sécurité de l'image (dépendances à jour, utilisateur non privilégié, voir le cours Construire des images de conteneurs) reste la vôtre.

La traçabilité. Depuis le 26 août 2026, les opérations sur les conteneurs, espaces de noms, domaines et déclencheurs sont enregistrées dans Audit Trail : qui a déployé quelle image, et quand (leçon 15).

En production

  • Production et préproduction ne se règlent pas pareil. En production : min-scale au moins à 1, voire 2, pour supprimer le démarrage à froid et tolérer la perte d'une instance ; max-scale dimensionné sur les pics mesurés et sur la capacité de la base ; alerte sur l'état du conteneur et sur le taux de réponses 5xx.
  • Les quotas sont ceux de l'organisation. La somme de la mémoire de tous les conteneurs à leur échelle maximale ne peut pas dépasser 600 Gio par organisation ; max-scale est plafonné à 200 et le nombre d'espaces de noms à 100 par projet. Un max-scale généreux sur plusieurs conteneurs consomme ce quota même si ces instances ne démarrent jamais.
  • Le retour arrière est un redéploiement. Faute de versions, gardez dans la CI la référence de l'image précédente (par son empreinte) pour pouvoir la redéployer en une commande.
  • Quand redescendre vers des instances ou Kubernetes. Besoin d'une adresse fixe, d'un accès entrant privé, d'un répartiteur de charge de Scaleway, de plusieurs ports, de traitements de plus d'une heure par requête, ou d'un contrôle fin du réseau : le serverless n'est pas le bon outil aujourd'hui. Pour un service web sans état, au trafic irrégulier, il l'est souvent.
  • Chez Lyneko, les applications de production tournent sur Kapsule, où ces contraintes n'existent pas ; Serverless Containers est le bon candidat pour les environnements éphémères, les préproductions peu utilisées et les petits services internes.

Exercices

1. Combien d'instances ? (niveau 200). La préproduction reçoit un pic de 50 requêtes simultanées, chacune durant 400 ms. Avec un seuil de concurrence de 20 et max-scale=3, combien d'instances démarrent, et que se passe-t-il pour les requêtes au-delà ? Même question avec le seuil par défaut. Combien de connexions à PostgreSQL au plus, si chaque worker garde une connexion ?

Solution

Avec un seuil de 20, il faudrait trois instances pour 50 requêtes (20 + 20 + 10) : max-scale=3 suffit, les trois démarrent (avec un démarrage à froid pour celles qui n'existaient pas). Dans chaque instance, deux workers traitent deux requêtes à la fois, les autres attendent dans l'instance : la latence augmente, mais rien n'est refusé. Avec le seuil par défaut de 80, une seule instance reçoit les 50 requêtes, dont 48 attendent : la latence explose sans qu'aucune instance supplémentaire ne démarre. Connexions : trois instances × deux workers = six connexions au plus, ce qui est très loin des limites de la base. Le réglage du seuil doit refléter la capacité réelle de l'instance, ici deux requêtes à la fois.

2. Le bon bac à sable (niveau 200). Pour chacun de ces services, choisissez v1 ou v2 et justifiez : (a) une API en Go, un seul processus, très sollicitée en journée ; (b) Signalements dans son image actuelle ; (c) un service qui convertit des documents en PDF avec LibreOffice en écrivant des fichiers temporaires dans /tmp ; (d) un service qui signe des jetons valables 30 secondes et tourne en continu.

Solution

(a) v2 : un seul processus, pas de fork, et le démarrage plus rapide compte pour un service à forte variation de charge. (b) v1, à cause des workers forkés, tant que l'image n'est pas modifiée. (c) v1, ou v2 avec beaucoup de mémoire et des fichiers temporaires écrits hors de /tmp : en v2, /tmp est en mémoire, et LibreOffice lance aussi des processus ; c'est surtout un candidat à vérifier contre la liste des appels système de gVisor. (d) v2 : la dérive d'horloge documentée de v1, deux secondes par journée continue, est gênante pour des jetons de 30 secondes.

3. Le vendredi soir (niveau 200). Un vendredi à 18 h, le pipeline met à jour l'image du conteneur de production avec une version qui plante au démarrage. L'étape de déploiement n'a pas d'option --wait et ne vérifie rien. Décrivez ce qui se passe jusqu'au samedi 18 h 30, puis corrigez l'étape du pipeline.

Solution

La mise à jour est acceptée, la nouvelle version échoue (sonde de démarrage), le conteneur passe en error. L'ancienne version continue de servir le trafic : rien n'est visible. Le samedi vers 18 h, les 24 heures de grâce sont écoulées : les deux versions sont supprimées, et le service ne répond plus jusqu'à un nouveau déploiement réussi. Correction : scw container container update ... --wait, puis un test du champ status qui fait échouer le pipeline s'il ne vaut pas ready, en affichant error_message ; et, en complément, une alerte Cockpit sur l'état du conteneur, pour couvrir les modifications faites hors pipeline.

Récapitulatif

  • Un espace de noms par environnement et par projet porte la configuration commune ; les variables du conteneur l'emportent sur celles de l'espace de noms.
  • La mise à l'échelle suit un seul critère (concurrence, CPU ou mémoire) ; la concurrence par défaut est 80 ; descente après 30 s, à zéro après 15 min ; au-delà de max-scale, file d'attente puis 503.
  • Le démarrage à froid se réduit par une image légère, un démarrage rapide, le bac à sable v2, une instance minimale ou un déclencheur horaire aux heures utiles.
  • v1 (Kata Containers) : compatibilité complète, démarrage plus lent, dérive d'horloge ; v2 (gVisor) : démarrage rapide, /tmp en mémoire, pas de copie à l'écriture entre processus forkés.
  • La sonde de démarrage exige une écoute sur 0.0.0.0 ; la sonde de vie retire une instance sans la redémarrer. Ne faites pas dépendre la sonde de vie d'une ressource partagée sans l'avoir décidé.
  • Le réseau privé sert au trafic sortant (la base sans point d'accès public) ; pas d'entrée privée, pas de désactivation du point d'accès public, au 5 octobre 2026.
  • Pas de versions : un déploiement en échec laisse l'ancienne version 24 h, puis plus rien. La CI attend ready et échoue sinon.
  • Un conteneur privé s'appelle avec X-Auth-Token et une application IAM portant ContainersPrivateAccess.

Pour aller plus loin

  • La page des limites et restrictions de Serverless Containers, à relire avant chaque nouvel usage : elle évolue à chaque version du service.
  • La documentation de gVisor sur la compatibilité des appels système, si votre application fait autre chose que servir du HTTP.
  • Le cours Kapsule : Kubernetes managé chez Scaleway, pour les cas où les limites de cette leçon pèsent trop.
  • La leçon suivante, qui confie à Serverless Functions et à Serverless Jobs ce qui n'est pas une API web : les vignettes des photos et les tâches de nuit.
Voir ma constellation →

Sources