Un environnement de recette sert pendant les heures où l'équipe travaille. Le reste du temps, il tourne pour personne : ses pods occupent leurs ressources, ses tâches planifiées s'exécutent contre des données que personne ne regarde, et sa base de données attend des requêtes qui ne viendront pas avant le lendemain matin. Une semaine compte 168 heures ; une équipe en utilise une cinquantaine.
Sur une plateforme de cinq applications Symfony hébergée sur Kubernetes, j'endors tous les stagings de 19 h à 8 h et pendant tout le week-end. Le mécanisme tient en un script et deux pipelines planifiés. Cet article détaille comment il fonctionne, ce qu'il faut mémoriser pour réveiller correctement un environnement, et la partie qu'on oublie toujours : prévenir les équipes qu'un staging muet n'est pas forcément cassé.
Ce que coûte un staging qui tourne 168 heures par semaine
Le calcul de départ est simple. Du lundi au vendredi, de 8 h à 19 h, cela fait 55 heures ouvrées par semaine. Les 113 heures restantes, soit les deux tiers du temps, le staging tourne sans utilisateur. Sur une plateforme de plusieurs applications, chacune avec son serveur web, ses consumers de messages, sa base et ses tâches planifiées, ces deux tiers représentent une part réelle de la capacité réservée.
Encore faut-il savoir ce qu'on économise vraiment, et cela dépend du mode de facturation :
- facturation à la consommation — sur un cluster dont les nœuds suivent la charge, des pods à zéro libèrent des nœuds, et la facture baisse en proportion
- quota ou capacité réservée — si l'hébergeur facture un quota fixe, éteindre les pods ne réduit pas la facture du mois. Le gain est ailleurs : la capacité libérée la nuit sert aux traitements lourds, et le quota peut être revu à la baisse au renouvellement
- stockage — les volumes persistants restent en place pendant le sommeil, c'est ce qui permet de retrouver les données au réveil. Leur coût, lui, ne bouge pas
Il y a aussi un gain qui ne figure sur aucune facture. Un staging endormi n'exécute pas ses tâches planifiées : pas d'envoi de mails de test à trois heures du matin, pas de synchronisation avec un service tiers qui consomme un quota d'API, pas de logs qui s'accumulent pour rien. Et il n'est pas exposé quand personne ne le surveille.
L'impact écologique suit la même logique que la facture. Un serveur allumé consomme de l'électricité même sans charge, et cette consommation au repos n'est pas négligeable. Quand l'extinction des pods libère réellement des nœuds, ce sont autant de machines qui cessent de tourner pour personne. À plus long terme, une capacité dimensionnée sur l'usage réel évite d'acheter du matériel supplémentaire, dont la fabrication pèse lourd dans l'empreinte du numérique. Avec un quota fixe, le gain est plus indirect : les serveurs restent allumés, mais la capacité libérée sert à d'autres traitements au lieu d'en appeler de nouveaux.
Avant de comparer les hébergeurs sur leurs tarifs, il vaut la peine de regarder combien d'heures on leur paie pour rien. C'est l'un des leviers évoqués dans le choix d'un hébergement sans exploser ses coûts : la facture dépend autant de l'usage qu'on fait des ressources que du prix catalogue.
Le mécanisme : deux pipelines planifiés et un script
Pas besoin d'un opérateur Kubernetes ni d'un outil dédié. La plateforme utilise déjà GitLab CI pour déployer, et GitLab sait lancer un pipeline à heure fixe : les pipeline schedules. J'en ai posé deux sur un petit dépôt dédié, avec un fuseau horaire explicite (Europe/Paris) pour ne pas décaler d'une heure à chaque changement d'heure :
0 8 * * 1-5— avec la variableSCALE_ACTION=up, du lundi au vendredi à 8 h0 19 * * 1-5— avecSCALE_ACTION=down, du lundi au vendredi à 19 h
Le vendredi soir, le down endort donc les stagings jusqu'au lundi matin.
Le job ne tourne que sur ces déclenchements, et sur un lancement manuel depuis l'interface :
variables:
SCALE_ACTION:
description: "up or down"
value: "up"
options: ["up", "down"]
SCALE_PROJECT:
description: "One staging namespace, or all of them"
value: "all"
options: ["all", "app-a-staging", "app-b-staging"]
scale:
stage: deploy
image: $CI_REGISTRY_IMAGE:latest
script:
- ./scale.sh
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
Le script fait deux choses par namespace : il ramène les Deployments à zéro réplique, et il suspend les CronJobs. Les deux ne demandent pas le même soin.
Les Deployments : mémoriser avant d'éteindre
Mettre un Deployment à zéro est trivial. Le remettre dans son état d'origine l'est moins : au réveil, il faut savoir qu'il tournait à deux répliques plutôt qu'à une. Cette information doit survivre au sommeil, et le meilleur endroit pour la garder est le Deployment lui-même, dans une annotation posée juste avant l'extinction :
scale_deployments() {
local name replicas
if [ "down" == "${SCALE_ACTION}" ]; then
kubectl get deployments -o json \
| jq -r '.items[] | "\(.metadata.name) \(.spec.replicas // 0)"' \
| while read -r name replicas; do
# Already asleep: overwriting the annotation with 0 would lose the real count.
[ "${replicas}" -gt 0 ] || continue
kubectl annotate deployment "${name}" "scaler/original-replicas=${replicas}" --overwrite
kubectl scale deployment "${name}" --replicas=0
done
else
kubectl get deployments -o json \
| jq -r '.items[] | "\(.metadata.name) \((.metadata.annotations // {})["scaler/original-replicas"] // "0")"' \
| while read -r name replicas; do
# A missing or zero annotation must not leave the application asleep.
if ! [[ "${replicas}" =~ ^[0-9]+$ ]] || [ "${replicas}" -le 0 ]; then
replicas=1
fi
kubectl scale deployment "${name}" --replicas="${replicas}"
done
fi
}
Cette annotation est tout l'état du système. Aucune base, aucun fichier ne garde la trace du nombre de répliques : si un autre outil la supprime, le réveil suivant retombe silencieusement sur une réplique. Il faut donc la nommer clairement, la documenter, et s'assurer qu'aucun outil de nettoyage ne l'élague sur ces namespaces.
Deux garde-fous protègent cet état. À l'endormissement, un Deployment déjà à zéro est ignoré :
un down manuel suivi du down planifié du soir écraserait sinon l'annotation par zéro.
Au réveil, une annotation absente ou nulle donne une réplique : mieux vaut une application qui tourne
en sous-capacité qu'une application qui reste endormie sans que personne ne comprenne pourquoi.
Les CronJobs : suspendre, sans rien mémoriser
Les tâches planifiées n'ont pas de répliques. Kubernetes leur offre un simple booléen, spec.suspend :
un CronJob suspendu ne lance plus de job, et reprend à la levée de la suspension.
scale_crons() {
local suspend=true
[ "up" == "${SCALE_ACTION}" ] && suspend=false
kubectl get cronjobs -o name | while read -r name; do
kubectl patch "${name}" -p "{\"spec\":{\"suspend\":${suspend}}}"
done
}
L'asymétrie entre les deux est voulue. Un booléen n'a pas d'état à préserver, un nombre de répliques en a un. La seule précaution côté CronJobs concerne les tâches qui doivent absolument tourner, comme une purge ou un export quotidien : il faut vérifier qu'elles sont planifiées en heures ouvrées, ou les sortir du périmètre.
Un namespace en échec ne bloque pas les autres
Le script parcourt la liste des stagings et traite chacun indépendamment. Si l'un est injoignable, il affiche l'erreur et passe au suivant : un namespace en panne ne doit pas laisser les autres allumés toute la nuit. La contrepartie est à connaître : un pipeline vert ne garantit pas que tous les stagings ont été traités. Il faut lire le log du job, ou mieux, retenir l'échec et faire sortir le script en erreur une fois la boucle terminée.
Côté droits, le job n'a besoin que de lister, annoter et mettre à l'échelle des Deployments, et de patcher des CronJobs. Un compte de service dédié, limité à ces verbes et aux seuls namespaces de staging, suffit. Ce n'est pas l'outil pour éteindre une production, et ses droits doivent le rendre impossible.
Le réveil : tout redémarre en même temps
Le réveil remet toutes les répliques en route et lève la suspension des CronJobs au même instant : le namespace redémarre d'un bloc, mais ses composants ne sont pas tous prêts au même moment. La base de données, qui tourne dans le même namespace, met une à deux minutes à remonter, le temps de rattacher son volume et de démarrer le moteur. Pendant cette fenêtre, les pods applicatifs, les consumers de messages et le premier cron de la journée peuvent échouer sur une base pas encore disponible ; Kubernetes les relance avec un délai croissant, et l'ensemble se stabilise seul en quelques minutes.
Pour lisser ce démarrage, il suffit d'ordonner le réveil :
- d'abord les dépendances — base de données, cache, attendre qu'elles soient prêtes (
kubectl rollout status) - ensuite les applications — serveurs web et consumers
- en dernier les CronJobs — une tâche planifiée ne doit pas partir avant que tout ce dont elle dépend ne soit debout
Des sondes de démarrage et de disponibilité bien réglées limitent aussi le bruit : un pod qui n'est pas prêt ne reçoit pas de trafic, et le déploiement n'est pas déclaré en échec pour autant. C'est le même sujet que celui d'un déploiement sans interruption : un pod ne doit se déclarer prêt que lorsqu'il peut vraiment servir.
Enfin, il faut prévoir le réveil hors horaires. Une démonstration un samedi, un développeur qui avance le soir,
une recette décalée : le même pipeline se lance à la main depuis l'interface de GitLab, en choisissant up
et le namespace concerné. Compter une à deux minutes avant que tout réponde, le temps que la base remonte.
Le dire aux équipes : un staging muet n'est pas cassé
La partie technique est la plus courte. La partie humaine demande plus d'attention, parce qu'un environnement qui s'éteint tout seul modifie ce que les équipes considèrent comme normal.
Un staging qui ne répond pas un dimanche, ou à 22 h, n'est pas cassé : il dort.
Sans cette phrase, écrite noir sur blanc dans la documentation de chaque projet, le premier développeur qui teste un correctif un soir ouvre un incident, et la personne d'astreinte cherche une panne qui n'existe pas. La même règle vaut pour un cron de staging qui n'a pas tourné cette nuit, ou pour un déploiement du soir dont les pods restent à zéro : le rollout s'est bien passé, le Deployment attend simplement son réveil.
L'inverse surprend tout autant. Un staging allumé un samedi n'est pas un down en échec :
quelqu'un l'a réveillé, et c'est permis. Comme les deux pipelines planifiés ne tournent qu'en semaine,
rien ne le rendort avant le lundi 19 h. On l'accepte, ou on l'éteint à la main en partant.
Avant de soupçonner le script, on regarde qui a lancé le dernier up dans la liste des pipelines.
Trois mesures rendent le dispositif lisible :
- une notification à chaque passage — un message dans le canal de l'équipe au coucher et au réveil de chaque staging. Elle doit rester accessoire : un échec d'envoi ne doit jamais faire échouer la mise en sommeil
- une procédure de réveil en trois lignes — où cliquer, quelles variables poser, combien de temps attendre. Chacun doit pouvoir réveiller un staging sans demander à personne
- les horaires à un seul endroit — dans les pipelines planifiés, pas recopiés dans un wiki qui finira par diverger
Conclusion
Endormir ses stagings ne demande ni outil payant ni chantier d'infrastructure : deux pipelines planifiés, un script, et une annotation pour se souvenir de l'état d'origine. Les deux tiers des heures de la semaine cessent de consommer des ressources pour personne. Ce qui fait réussir le dispositif n'est pas le script, mais ce qui l'entoure : un réveil ordonné, une procédure que chacun peut suivre, et une phrase répétée à toute l'équipe. Pour un dirigeant qui compare des hébergements, c'est aussi un rappel utile : entre cloud et on-premise, le prix de l'heure compte moins que le nombre d'heures facturées sans usage.
Message clé
Un staging n'a pas besoin de tourner quand personne ne travaille. L'éteindre est simple ; le réveiller proprement, et le dire à l'équipe, est ce qui demande du soin.
Vous voulez savoir ce que votre infrastructure consomme sans servir ? Un audit technique mesure l'usage réel de vos environnements et priorise les économies. Parlons-en.