Construire une attribution des coûts adaptée à l'organisation
Vous guidez une personne (et, lorsque l'organisation en a une, un partenaire financier) à travers une construction qui est pour l'essentiel du ciment entre un schéma standard (FOCUS) et un runtime serverless. Les motifs sont fixes ; la forme est la leur. Votre travail consiste à trouver leur forme dans leurs données, à l'écrire et à appliquer les motifs — jamais à installer un produit générique.
L'article explique pourquoi chaque motif est le bon : https://mooracle.io/blog/build-custom-finops-tool/ (Parties 1–7). Lorsque l'utilisateur demande « pourquoi », pointez vers la partie ; gardez la session sur le « comment ».
Invariants
Ceux-ci ne varient jamais selon le profil. Vérifiez le code généré par rapport à eux avant de le montrer.
- Chaque éclatement conserve son total.
SUM(sortie) == SUM(entrée)par colonne de coût. Les répartiteurs déplacent de l'argent ; ils n'en créent ni n'en détruisent jamais. - Allouer au moment de la requête. Les catégories de l'organisation s'attachent dans une vue, via une table de
correspondance. N'écrivez jamais
equipedans une ligne d'entrepôt. EffectiveCostpour le chargeback.BilledCostrapproche les factures,ListCostmesure les économies. Ne les mélangez jamais.- Reconstruire tout le mois. Chaque partition
(billing_period, provider)est effacée et réécrite à partir des données brutes à chaque livraison. Pas de deltas, pas de fusions. - Limite à hier. Rien daté d'aujourd'hui ou plus tard n'entre dans l'entrepôt.
- Chaque éclatement est une seule expansion. Une ligne qui se divise rejoint ses pondérations à l'intérieur du
chargement régulier et devient une ligne par pondération ; les pondérations d'une clé additionnent à 1,0. Les répartitions
équitables, Kubernetes et les fournisseurs fonctionnent tous ainsi, via une table
split_weights. Rien n'est exclu du chargement et rajouté plus tard, donc il n'y a pas d'exclusion à oublier, et rien n'est compté deux fois. - Chaque exécution prouve ses nombres. Raw contient exactement les fichiers que le manifeste du
fournisseur liste, et une vérification bloquante des totaux compare la facture avec les deux tables de base
et échoue l'exécution (et efface ce qu'elle a écrit) lorsqu'elles divergent. Une fois les vues existantes,
chaque exécution et chaque clôture vérifient également que
services_monthlysert exactementbase_monthly(la vérification des vues, qui échoue sans rien effacer). - Ne jamais perdre de coût ; le routage peut échouer. Lorsqu'une répartition ne peut pas être faite en toute sécurité, le coût reste entier là où il a été facturé. Une répartition ignorée est visible ; un coût perdu est silencieux.
- Les lectures brutes sont en mois exact.
WHERE billing_period = 'YYYY-MM', avec la limite de balayage du moteur activée (une limite d'octets de groupe de travail sur Athena). Les lectures de tout l'historique sont autorisées uniquement surbase_monthly(mégaoctets). - Classifier une colonne partagée une fois, dans une vue.
x_namespacecontient à la fois les espaces de travail Kubernetes et SaaS ;split_byindique lequel. Ne jamais déduire de la forme de la valeur.
Fichiers que vous conservez dans le dépôt de l'utilisateur
Créez finops/ dans la Phase 0, ou là où l'utilisateur le souhaite. Enregistrez le chemin comme la première
D-001 ; ci-dessous, finops/ signifie ce chemin.
-
finops/profile.yaml— la forme de l'organisation. Commencez à partir deassets/profile.template.yaml. Chaque phase le lit ; rien d'autre ne contient les décisions spécifiques à l'organisation. -
finops/DECISIONS.md— journal d'ajouts uniquement. Ne modifiez jamais une entrée ; remplacez-la.## D-007 Enterprise Support est partagé (2026-10-17) Source : D5 du 2026-09 ; Q-S1 répondue par <utilisateur> Décision : répartir par dépense first-party du même mois, par payeur Remplace : —Les résultats de découverte, réponses, hypothèses, approbations et décisions négatives (« essayé une table de pondérations quotidiennes ; annulé car le mix ne varie pas au sein d'un mois ; ne pas reproposer ») vont tous ici. Les champs
evidencedu profil pointent vers ces identifiants. -
finops/STATUS.md— une ligne par phase : état, résultat de la porte avec les nombres, le SHA du commit auquel la porte a passé, et les champs de profil dont la phase dépend.| Phase | État | Porte | Au commit | Dépend de | | 3 tables de base | passé | facture 2026-09 120 000,00 = mensuel 120 000,00 | a1b2c3d | providers, dimensions, routing.key, finance, period_charges, shared_costs |
Boucle d'exécution
Entrée
finops/STATUS.mdexiste → reprendre : lire STATUS, DECISIONS et le profil. Pour chaque phase passée,git diff <son SHA> -- finops/profile.yaml; si un champ dont elle dépend a changé, rouvrir cette phase et chaque phase suivante qui lit sa sortie (1 → 2 → 3 ; 3 → 4, A, B, 5 ; 4 → 5). Réexécuter la dernière porte passée sur les données actuelles (les données bougent aussi). Continuer à partir de la première phase non passée.- L'utilisateur signale un changement (réorganisation, nouvelle famille de comptes, nouveau fournisseur, nouveau cluster) → rafraîchir : réexécuter les requêtes de découverte correspondantes, comparer avec le profil, redemander uniquement ce qui a bougé, revérifier, rouvrir les phases dépendantes.
- L'utilisateur veut d'abord voir le système fonctionner, ou ne souhaite pas encore donner accès à la facturation → la
version d'essai (
references/trial.md) : un mois synthétique sur DuckDB, sans identifiants, vérifié contre un corrigé. Rien de cela n'est reporté dans une construction réelle. - Sinon → démarrer ci-dessous.
1. Découvrir avant de demander
Lisez references/discovery.md et exécutez ses requêtes sur le dernier mois clôturé, contre
l'export où il atterrit déjà (une table sur ses fichiers, ou DuckDB sur une livraison téléchargée quand il n'y a pas de moteur), avant que la construction n'existe. Montrez
chaque résultat. Enregistrez chacun comme D-NNN. La découverte lit ; elle ne change rien dans le
compte, pas même une activation de tag.
Pas encore d'export → le chemin d'amorçage : activer l'export (references/focus-export.md),
passer les étapes de collecte non basées sur les données (Organisation, Runtime, Catégories, Finance), construire la
stack contre la disposition attendue, et exécuter la découverte dans la session suivante une fois la première
livraison arrivée. Proposer la version d'essai (references/trial.md) en attendant.
2. Interroger pour les lacunes
Lisez references/intake.md. La règle d'arrêt ou d'hypothèse :
- la réponse concerne l'argent ou la propriété → laissez
null; le composant reste BLOQUÉ ; - c'est un nom, emplacement ou outil → utilisez la valeur par défaut, ajoutez une entrée
assumptions:et unD-NNN, passez à la suite.
Ne remplissez jamais un fait avec une supposition plausible, un exemple fixe ou une valeur provenant d'un exemple.
3. Vérifier et approuver le plan
Écrivez le vérificateur que references/plan.md décrit dans finops/ (une fois, avec un test par
règle) et exécutez-le sur le profil. Il affiche un verdict par composant et ce qui est prêt
à construire (PRÊT pour toutes ses dépendances). Montrez-le.
Pour chaque ligne BLOQUÉE, nommez qui peut répondre et la question exacte. Pour chaque PAS_ADAPTÉ,
dites la solution de rechange plus simple à voix haute — y compris « ne pas construire ceci » lorsque la ligne
org le dit. Obtenez une approbation explicite du plan ; enregistrez-la comme un D-NNN.
4. Construire, phase par phase
Pour chaque phase dans le tableau ci-dessous, dans l'ordre (les pistes optionnelles A et B après la Phase 4, uniquement lorsque PRÊT) :
- Vérification d'entrée. Le vérificateur avec
--entering <phase>sort avec code 0, et chaque phase dont elle dépend est passée dans STATUS.md. - Lire sa spécification (
references/<fichier>, colonne Spéc du tableau des phases) : le contrat, les invariants et les cas limites qui mordent quand on improvise. - L'écrire dans leur stack. Construire les composants de la phase dans le moteur et le langage
de l'utilisateur (Athena, BigQuery, Snowflake, Databricks, dbt, …) et dans le style de leur dépôt,
adapté par leur profil : un adaptateur par fournisseur depuis
providers.md, uniquement les répartitions dont le profil a besoin, uniquement les colonnes sur lesquelles il route. Les spécifications disent ce qui doit tenir et les cas ce qui doit sortir ; le code est à vous d'écrire. Portez le plus grand soin là où une erreur est silencieuse : les pondérations, l'étalement sur les jours, les vérifications de totaux, les gardes de devise. - Porter les cas en or (
cases/*.yaml; comment, dansreferences/testing.md) qui s'appliquent à ce que cette phase a construit, et ajoutez un cas pour chaque adaptation spécifique à l'organisation. Une phase n'est pas construite tant que ses cas ne passent pas. - Déployer et exécuter la porte contre leurs données réelles. Les portes sont des requêtes SQL qui renvoient zéro lignes lorsqu'elles sont saines. Montrez les nombres.
- Enregistrer le résultat, le SHA et les dépendances dans STATUS.md.
Rapport de porte, à chaque fois :
Porte 3 (tables de base), 2026-09, aws : PASSÉE
facture 120 000,00 · mensuel 120 000,00 · quotidien 120 000,00 · dérive 0,00
support : 1 ligne → 12 comptes, somme des pondérations 1,000000
Une porte échouée arrête la construction. Déboguer avec le tableau des échecs de la spécification (les noms des problèmes
sont dans reconciliation-ops.md, « Modes d'échec »), corriger, réexécuter.
Tableau des phases
| Phase | Construit | Champs du profil | Spécification | Cas | Porte |
|---|---|---|---|---|---|
| 0 | découverte, entretien, vérificateur, plan | tous | discovery.md, intake.md, plan.md | — | plan approuvé (D-NNN) |
| 1 | exports FOCUS | providers | focus-export.md | — | un mois complet interrogeable (D0) |
| 2 | disposition du stockage, moteur, tables et les livraisons qui remplissent raw (copie ou poller, staging, étape de publication de l'ETL) | runtime, providers | stack.md, data-model.md, pipeline.md (« Livraison ») | — | un mois publié correspond à son manifeste ; un mois raw interrogé scanne une fraction des fichiers de ce mois |
| 3 | adaptateurs, tables de base avec proratisation et répartitions de coûts partagés, la reconstruction de l'ETL | providers, dimensions, routing.key, finance, period_charges, shared_costs | providers.md, data-model.md, splitters.md, pipeline.md | adapters, load, share, checks | facture == mensuel == quotidien par payeur sur un mois clôturé |
| 4 | règles, table de correspondance, vues | categories, routing | rules-engine.md | rules, views | catégories apparaissent ; Non alloué signalé |
| A | répartitions SaaS | saas.vendors | saas.md | vendor | le fournisseur montre split ; les totaux tiennent |
| B | espaces de noms Kubernetes | kubernetes | kubernetes.md | k8s | pondérations propres ; couverture ≥ cible |
| 5 | réconciliation, opérations, clôture | finance, org.finance_partner | reconciliation-ops.md, testing.md | checks | là où l'organisation saisit les totaux de facture (Q-F5), un mois clôturé passe la vérification de facture pour chaque fournisseur, son résultat stocké avec le mois ; une répartition délibérément cassée échoue l'exécution sur une copie de développement ; une analyse des nombres avec le partenaire financier, ou le propriétaire s'il n'y en a pas |
references/pipeline.md contient l'ordre de traitement dans lequel ces éléments s'exécutent au moment de l'exécution.
Degrés de liberté
- Fixe (quel que soit le moteur) : les invariants, et ce que les cas doivent produire en sortie
(
adapters.yamlexcepté : il montre les choix d'un profil sur les formes brutes des exports) : les règles de proratisation, l'expansion avec des pondérations qui additionnent à 1,0, les portes des producteurs (par payeur, dépense positive, données fournisseur complètes pour le mois), les vérifications et sur quoi elles s'arrêtent, l'ordre d'exécution et l'effacement en cas d'échec, le refus des paramètres de requête non renseignés. - Leur (écrire librement pour correspondre à leur codebase) : le moteur et le langage, la disposition des fichiers, l'infrastructure en tant que code, le packaging, la planification, les tableaux de bord, l'IC, et les noms des fichiers, fonctions et infrastructure (les noms de tables, colonnes et problèmes que les cas utilisent sont fixes).
Lorsque vous n'êtes pas sûr de quel côté se trouve un changement, il est fixe.
Quand l'utilisateur résiste
references/pushback.md contient les résistances habituelles (« allouer dans l'ETL », « mapper 100% », « un
framework pour chaque fournisseur », « précision par pod ») avec la réponse et où l'article en
parle.
Terminé signifie
- Chaque mois clôturé passe la vérification des totaux pour chaque fournisseur, à travers chaque répartition, et, là où l'organisation saisit les totaux de facture, la vérification de facture.
- Celui qui rapporte les coûts (le partenaire financier, ou le propriétaire s'il n'y en a pas) répond « combien l'équipe X a-t-elle coûté le mois dernier ? » à partir d'une vue, sans nouveau SQL.
- Un changement de règle est une pull request : l'IC se développe, le relecteur voit ce qui bouge, la fusion re-catégorise tout l'historique sans reconstruction.
- Le non alloué est signalé comme un montant, dans sa devise, et en tendance à la baisse.
- Le coût d'exécution est faible et mesuré (sur la stack de référence, environ 10 $ par mois pour S3, Athena et Lambda ensemble pour 15 millions de lignes de facture par mois ; la part d'Athena est dans les métriques de balayage du groupe de travail).
- STATUS.md montre que chaque phase que le plan construit est passée avec un SHA ; DECISIONS.md explique chaque choix qu'un nouveau venu remettrait en question.