Le FinOps a dépassé le stade du tableur mensuel
Pendant des années, la gestion des coûts cloud s'est résumée à un export mensuel, un tableau croisé dynamique et une réunion tendue où quelqu'un demande pourquoi la facture a pris 12%. Ce modèle s'effondre dès que la plateforme devient réellement élastique : node pools autoscalés, interruptions spot, fonctions serverless facturées à la milliseconde, nœuds GPU loués le temps d'un entraînement, et une dizaine d'équipes qui se partagent quelques clusters Kubernetes. Le signal de facturation devient à forte cardinalité, bruité et différé — exactement les conditions où l'œil humain cesse de repérer les dérives.
Deux évolutions ont changé la donne. D'abord la spécification FOCUS de la FinOps Foundation, qui fournit un schéma de facturation neutre : la détection d'anomalies et la prévision n'ont plus besoin d'un parseur par fournisseur. Ensuite la maturité des détecteurs managés (AWS Cost Anomaly Detection, alertes d'anomalies Azure Cost Management, détection d'anomalies GCP) combinée aux moteurs d'allocation open source (OpenCost, Kubecost), qui rendent réaliste le bouclage complet : détecter, attribuer, prévoir, refacturer.
Cet article décrit comment construire cette boucle de façon pragmatique, en distinguant honnêtement ce que le machine learning apporte réellement de ce qu'une bonne baseline statistique fait mieux.
Détection d'anomalies : ce que l'IA apporte vraiment
Une anomalie de coût, ce n'est pas « la dépense a augmenté ». C'est « la dépense a augmenté d'une manière non expliquée par la saisonnalité connue, la croissance connue ou un déploiement connu ». Toute la difficulté d'ingénierie tient dans cette nuance.
Les seuils statiques échouent parce que la dépense cloud n'est pas stationnaire : saisonnalité hebdomadaire (runners CI au repos le week-end), mensuelle (batchs de clôture), croissance structurelle. Les règles en pourcentage par rapport à la semaine précédente génèrent tellement de faux positifs que l'équipe coupe le canal Slack en un mois — la spirale classique de la fatigue d'alerte.
| Approche | Points forts | Limites | Quand l'utiliser |
|---|---|---|---|
| Seuil budgétaire statique | Garde-fou financier dur, engagements prépayés | Aveugle à la saisonnalité et à l'élasticité | Coupe-circuit au niveau compte uniquement |
| Z-score robuste / MAD sur la dépense quotidienne | Peu coûteux, explicable, sans dépendance | Saisonnalité forte, séries à faible volume | Première itération, par service × compte |
| Décomposition saisonnière (STL, ETS, Prophet) | Motifs hebdo/mensuels, séparation de la tendance | Changements de régime brutaux, séries creuses | FinOps mature, 6+ mois d'historique |
| Détection managée du fournisseur | Aucune infra, granularité native | Corrélation multi-cloud, attribution Kubernetes | Couverture de base sur tous les comptes |
| Gradient boosting avec variables exogènes | Corréler coût, déploiements, trafic, tenants | Explicabilité, démarrage à froid, maintenance | Grandes plateformes avec équipe data |
En pratique, le gain principal ne vient pas d'un modèle plus sophistiqué, mais de la granularité et du contexte. Détecter un saut de 30% sur un compte AWS entier ne sert à rien ; détecter que le traitement de données NAT Gateway d'une seule région, pour un seul tag d'allocation, a triplé hier à 14h UTC, c'est actionnable. La détection doit tourner par tuple (service, région, tag), jamais sur l'agrégat.
Un point de départ robuste et explicable sur des données quotidiennes au format FOCUS :
import pandas as pd, numpy as np
# df : colonnes FOCUS ChargePeriodStart, ServiceName, x_Team, BilledCost
daily = (df.groupby([pd.Grouper(key="ChargePeriodStart", freq="D"),
"ServiceName", "x_Team"])["BilledCost"]
.sum().reset_index())
def flag(group, window=28, k=4.0, min_delta=50.0):
s = group.set_index("ChargePeriodStart")["BilledCost"].asfreq("D", fill_value=0)
# baseline sur le même jour de semaine : saisonnalité hebdo gérée simplement
base = s.shift(1).rolling(window).apply(lambda w: np.median(w[-28::7]), raw=True)
mad = s.shift(1).rolling(window).apply(
lambda w: np.median(np.abs(w - np.median(w))), raw=True)
score = (s - base) / (1.4826 * mad + 1e-9)
group = group.assign(baseline=base.values, score=score.values)
return group[(group.score > k) & (group.BilledCost - group.baseline > min_delta)]
anomalies = daily.groupby(["ServiceName", "x_Team"], group_keys=False).apply(flag)
Notez le garde-fou min_delta : un service qui passe de 2 à 12 € est un pic ×6 sans aucun intérêt financier. Toute alerte d'anomalie doit porter un impact mensuel estimé si rien n'est corrigé, sinon les équipes ne peuvent pas trier.
Le second indispensable est la corrélation avec les événements de changement. Joignez les horodatages d'anomalie à l'historique de synchronisation Argo CD, aux événements de déploiement GitHub ou aux logs de terraform apply. « La dépense NAT Gateway en eu-west-3 a dépassé 4σ deux heures après le rollout de payments-api v2.14.0 » transforme une alerte finance en ticket d'ingénierie avec un propriétaire.
Prévision par ML : le bon horizon, la bonne question
Prévoir la dépense cloud est plus simple que prévoir le chiffre d'affaires, car une large part de la facture est inertielle : instances engagées, stockage qui ne fait que croître, socle de calcul permanent. Les questions utiles sont bien plus étroites que « combien dépenserons-nous l'an prochain » :
- Atterrissage fin de mois (7 à 25 jours) : essentiellement une extrapolation d'un mois partiellement observé. Un modèle simple conditionné sur le jour du mois et le jour de la semaine bat un LSTM générique quasi systématiquement.
- Couverture d'engagements (1 à 12 mois) : prévoir le plancher stable de consommation on-demand par famille d'instances, pas la dépense totale. C'est ce qui pilote les décisions de Savings Plans, Reserved Instances ou remises d'engagement.
- Coût unitaire (trimestriel) : prévoir le coût par unité métier (par client, par commande, par inférence) pour alimenter les modèles de prix et de marge.
Deux disciplines séparent une prévision crédible d'une décoration de dashboard. D'abord le backtesting : évaluation à origine glissante, erreur rapportée par service. Si vous ne savez pas énoncer votre erreur de prévision, aucun DAF ne devrait l'utiliser. Ensuite la décomposition : séparer la dépense engagée (déterministe), la consommation socle (prévisible) et la consommation élastique (la seule qui nécessite vraiment un modèle). Prévoir l'agrégat masque l'endroit où se trouve l'incertitude.
Publiez aussi des intervalles de prédiction plutôt qu'une courbe unique. La conversation budgétaire change radicalement quand l'ingénierie dit « le P50 est à X, le P90 à Y, et l'écart est piloté par le calendrier d'entraînement des modèles ».
FinOps Kubernetes : l'allocation, le vrai point dur
Kubernetes est l'endroit où l'attribution FinOps casse, parce que la facture cloud s'arrête au nœud. Le fournisseur vous facture une m6i.4xlarge ; il ignore que 40% de cette machine exécute les indexeurs de l'équipe search. Il faut un moteur d'allocation qui joint les métriques Prometheus des workloads au prix des nœuds.
OpenCost (projet CNCF en incubation, moteur sous-jacent de Kubecost) est le standard de fait. Son modèle est simple : pour chaque pod, calculer l'allocation CPU, mémoire, GPU, stockage et réseau dans le temps, la valoriser au coût horaire du nœud, puis agréger par namespace, label, contrôleur ou annotation.
La décision de conception la plus structurante est la base d'allocation :
| Base | Effet | Risque |
|---|---|---|
| max(requests, usage) | Par défaut ; facture la capacité réservée bloquée | Les équipes sur-réservent et le paient — c'est précisément le but |
| Usage seul | « Juste » pour les charges en rafale | Sur-réserver devient gratuit ; l'idle du cluster explose |
| Requests seuls | Parfaitement prévisible, pousse au rightsizing | Pénalise les jobs légitimement bursty |
Utilisez max(requests, usage). L'incitation est la bonne : réserver de la capacité coûte de l'argent qu'on l'utilise ou non — exactement comme la facturation du nœud sous-jacent.
La seconde décision concerne les coûts idle et mutualisés. Un cluster n'est jamais rempli à 100% : marge de sécurité, DaemonSets, ingress, service mesh, agents d'observabilité. Soit vous laissez l'idle non alloué (honnête, mais personne ne le possède donc personne ne le corrige), soit vous le redistribuez au prorata de la part allouée de chaque namespace (crée une pression sur le bin-packing). Notre recommandation : afficher les deux. Refacturer au prorata, mais exposer la ligne idle explicitement, avec le taux de remplissage comme KPI de l'équipe plateforme.
Une requête minimale de coût par namespace sur les métriques exportées par OpenCost :
sum by (namespace) (
avg_over_time(container_cpu_allocation[1h])
* on (node) group_left()
avg_over_time(node_cpu_hourly_cost[1h])
)
+
sum by (namespace) (
avg_over_time(container_memory_allocation_bytes[1h]) / 1024^3
* on (node) group_left()
avg_over_time(node_ram_hourly_cost[1h])
)
Et la gouvernance qui rend tout cela exploitable : l'allocation ne vaut que ce que valent vos labels. Imposez-les à l'admission plutôt que de courir après les propriétaires :
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-cost-labels
spec:
validationFailureAction: Enforce
rules:
- name: check-owner-labels
match:
any:
- resources:
kinds: [Namespace]
validate:
message: "Tout namespace doit porter cost-center, team et env"
pattern:
metadata:
labels:
cost-center: "?*"
team: "?*"
env: "?*"
Showback d'abord, chargeback ensuite
Le chargeback — déplacer réellement de l'argent entre budgets internes — est une décision organisationnelle, pas technique, et il échoue quand la donnée n'est pas encore digne de confiance. Commencez par le showback : chaque équipe voit son coût alloué mensuel, sa tendance, sa part d'idle et ses trois principales pistes d'optimisation. Pas de facture, donc pas de débat sur 3% d'erreur d'attribution.
Passez au chargeback seulement quand trois conditions sont réunies : une couverture de labels que vous êtes prêt à défendre publiquement, une méthodologie d'allocation documentée et stable (y compris la répartition de l'idle et des services mutualisés), et un processus de contestation. Changer la formule d'allocation en cours d'année sans note de migration détruit la crédibilité plus vite que n'importe quelle mauvaise surprise de facturation.
Unit economics : coût par client, par requête, par inférence
C'est là que le FinOps cesse d'être une fonction de réduction de coûts pour devenir une fonction produit. La dépense absolue est un mauvais signal : une facture qui double pendant que la base clients triple, c'est une excellente nouvelle. Ce dont la direction a besoin, c'est d'un dénominateur.
- Coût par tenant / par compte : indispensable pour la marge brute SaaS et pour repérer les clients sur lesquels vous perdez de l'argent.
- Coût par transaction (commande, appel API, document traité) : la métrique d'efficacité d'ingénierie ; elle doit décroître avec l'échelle.
- Coût pour 1 000 inférences ou par token : devenu la métrique dominante des fonctionnalités IA, où un seul utilisateur intensif peut distordre la marge.
- Coût par environnement : la source la plus rapide de gains, car le hors-production est rarement optimisé.
Sur une plateforme multi-tenant où les tenants partagent les mêmes pods, l'allocation nécessite un signal applicatif. Exposez un compteur d'usage par tenant depuis l'application, puis répartissez le coût du namespace au prorata :
-- modèle dbt : cost_per_tenant.sql
with ns_cost as (
select date_day, namespace, allocated_cost
from {{ ref('k8s_namespace_cost_daily') }}
),
usage as (
select date_day, namespace, tenant_id,
sum(weighted_requests) as units -- ex. requests * cpu_ms
from {{ ref('app_tenant_usage_daily') }}
group by 1,2,3
)
select u.date_day, u.tenant_id,
sum(c.allocated_cost * u.units
/ sum(u.units) over (partition by u.date_day, u.namespace)) as tenant_cost
from usage u join ns_cost c using (date_day, namespace)
group by 1,2
Pondérez correctement l'inducteur : un simple compteur de requêtes est trompeur si un endpoint est 100 fois plus lourd qu'un autre. Utilisez des millisecondes CPU, des octets stockés ou des tokens consommés — ce qui pilote réellement la ressource facturée. Joignez ensuite le coût par tenant au revenu contractuel, et vous obtenez une marge brute par client rafraîchie quotidiennement.
Une architecture de référence qui reste maintenable
Le motif qui tient dans la durée est volontairement ennuyeux :
- Ingestion : exports de coûts fournisseurs (CUR 2.0 / FOCUS, exports Azure, export BigQuery GCP) vers du stockage objet ; données d'allocation OpenCost exportées quotidiennement vers le même lac.
- Normalisation : une table unique au format FOCUS pour tous les fournisseurs, plus une table de dimensions équipes / centres de coûts / tenants versionnée dans Git, pas dans un tableur.
- Modélisation : modèles dbt pour le coût alloué quotidien, les métriques unitaires et les prévisions, tous versionnés et testés.
- Détection : job d'anomalies sur la table normalisée, complété par les détecteurs natifs des fournisseurs en filet de sécurité.
- Diffusion : alertes dans le canal Slack de l'équipe propriétaire, avec impact et cause probable ; dashboards dans l'outil que les ingénieurs utilisent déjà.
Résistez à la tentation du portail FinOps maison. Une donnée de coût qui vit là où les ingénieurs travaillent — Grafana, Backstage, leurs checks de PR — est suivie d'effet ; un portail séparé est mis en favori une fois puis oublié.
Les anti-patterns à éviter
Envoyer les alertes d'anomalie à un canal FinOps central plutôt qu'à l'équipe propriétaire. Optimiser la longue traîne de la facture en ignorant les trois premiers services. Traiter l'idle du cluster comme un problème d'équipe applicative plutôt que comme un KPI de plateforme. Publier une prévision sans métrique d'erreur. Et le plus fréquent : déployer le chargeback avant que la couverture de labels soit fiable, ce qui transforme chaque discussion de coût en débat sur la donnée au lieu d'un débat sur l'architecture.
Bien menée, l'approche FinOps augmentée par l'IA ne consiste pas à trouver un modèle qui réduirait magiquement la facture. Elle consiste à comprimer le délai entre un événement de coût et l'ingénieur capable de le corriger — d'un mois à quelques heures — et à donner au produit une marge par client sur laquelle il peut réellement agir.
