Files
simple-cost-dashboard/README.md
T
2026-08-07 17:19:49 +02:00

290 lines
13 KiB
Markdown

# simple-cost-dashboard
Suivi centralisé des coûts AWS et Azure pour une multitude de tenants,
avec conversion automatique en EUR et historique conservé en base SQLite.
Dashboard web (page globale + détail par tenant) alimenté par une collecte
quotidienne (cron).
---
## 1. Installation locale (sans Docker)
```bash
cd simple-cost-dashboard
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
## 2. Configuration des tenants
Crée un fichier `config.json` à la racine du projet (à côté de ce README) :
```json
{
"aws": [
{ "name": "analytics-platform", "profile": "analytics-platform" },
{
"name": "client-a-prod",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "VOTRE_SECRET_ICI"
}
],
"azure": [
{
"name": "client-c-prod",
"tenant_id": "00000000-0000-0000-0000-000000000000",
"client_id": "11111111-1111-1111-1111-111111111111",
"client_secret": "VOTRE_SECRET_ICI",
"subscription_id": "22222222-2222-2222-2222-222222222222"
}
]
}
```
- **AWS** — deux modes au choix par tenant, dans l'ordre de priorité
`access_key_id`/`secret_access_key` puis `profile` :
- `access_key_id` + `secret_access_key` : clé IAM dédiée au tenant,
limitée au droit `ce:GetCostAndUsage`. **Seul mode qui fonctionne en
conteneur/Kubernetes** — il ne dépend d'aucun fichier ni état sur
l'hôte, contrairement à `profile`.
- `profile` : un profil déjà configuré dans `~/.aws/credentials` ou
`~/.aws/config`, pratique en local. Nécessite de monter `~/.aws` dans
le conteneur (`-v ~/.aws:/home/appuser/.aws:ro`) — et **ne fonctionne
pas avec un profil SSO** en conteneur : le token mis en cache finit
par expirer, et son renouvellement demande un navigateur, indisponible
côté conteneur/CronJob. Réservé au dev local avec des clés statiques
ou un profil SSO valide au moment du run.
- **Azure** : un service principal par tenant (`az ad sp create-for-rbac`),
avec le rôle **Cost Management Reader** sur la subscription visée.
- Le champ `"name"` est l'identifiant utilisé partout (URLs, dashboard).
Deux tenants peuvent avoir le même `name` sur des CSP différents (ex:
`analytics-platform` en AWS **et** en Azure) — ils restent distingués
par le couple `(provider, name)`, y compris dans les URLs
(`/tenant/aws/analytics-platform` vs `/tenant/azure/analytics-platform`).
⚠️ `config.json` contient des secrets (`client_secret` Azure, `secret_access_key`
AWS). Ne le commite pas — il est déjà exclu via `.gitignore`.
## 3. Base de données
La base SQLite (`cost_dashboard.db` par défaut, chemin surchargeable via
`COST_DASHBOARD_DB`) et son schéma sont créés automatiquement au premier
lancement de `collect.py`, `import_historical.py` ou de l'API. Rien à
faire manuellement. Le fichier `db/schema.sql` décrit la structure.
## 4. Importer l'historique (une seule fois)
**Depuis AWS/Azure directement**, pour peupler la base avec tout l'historique
disponible sans étape intermédiaire :
```bash
cd collector
python backfill_historical.py --config ../config.json --db ../cost_dashboard.db
```
Rapatrie 12 mois par défaut (`--months` pour ajuster), un par un, pour
chaque tenant de `config.json`. Limites à connaître :
- **AWS Cost Explorer ne conserve que 12 mois glissants** — au-delà, l'API
ne renvoie rien (pas une erreur), donc `--months` plus grand n'aide pas
côté AWS. Azure dépend du contrat (souvent plus long).
- Chaque mois interrogé est un appel Cost Explorer/Cost Management facturé
(~0,01 $ la requête AWS) — pour 28 tenants sur 12 mois, prévoir ~336
requêtes (quelques dollars, payés une fois).
- `--tenant <nom>` pour ne rapatrier qu'un seul tenant (ex: nouvellement
ajouté à `config.json`).
En plus de l'historique mensuel, ce même script rapatrie aussi les
**30 derniers jours en granularité journalière** (`--daily-days`, 0 pour
désactiver) — sinon les graphes 7j/30j de la page tenant restent vides
jusqu'à ce que le cron ait tourné 7 à 30 jours de suite pour les remplir.
Rejoue exactement `collect_tenant`/`collect_tenant_resources` de
`collect.py` (mêmes fonctions que le cron), jour par jour : même ordre de
grandeur en appels facturés que le backfill mensuel (~840 requêtes pour
28 tenants sur 30 jours), et ça prend plusieurs minutes (appels
séquentiels). Le cron quotidien écrase ensuite ces jours au fil de l'eau
sans créer de doublons (upsert sur la même clé).
⚠️ **Côté Azure**, l'API Cost Management est plus vite sujette au
rate-limiting (429) que Cost Explorer côté AWS — surtout en rafale sur les
dizaines d'appels d'un backfill. `providers.py` retente automatiquement les
429 (jusqu'à 8 fois), en respectant le délai suggéré par Azure — pas le
`Retry-After` HTTP standard, mais ses propres en-têtes
`x-ms-ratelimit-microsoft.costmanagement-{entity,tenant,clienttype}-retry-after`,
le plus restrictif faisant foi quand plusieurs sont présents. `backfill_historical.py`
ajoute en plus une pause de 2s entre deux appels Azure pour limiter le
risque d'en déclencher. Si un tenant Azure échoue quand même après ça,
relancer le backfill juste sur lui (`--tenant <nom>`) suffit généralement.
**Depuis des exports JSON existants** (si tu as déjà des données de coûts
passées, exportées via `cost_report.py --json` ou `tenant_detail.py --json`,
par exemple générées avant la mise en place de ce dashboard) :
```bash
python import_historical.py --db ../cost_dashboard.db ../historique/*.json
```
Les deux stockent avec `granularity='monthly'`, pour ne pas se mélanger avec
la collecte journalière (qui prime dessus dans les agrégations dès qu'elle a
des données pour un mois donné). Les deux sont ré-exécutables sans créer de
doublons.
## 5. Collecte quotidienne (cron)
`collect.py` récupère le coût de **J-1** (les données du jour même sont
rarement finalisées côté AWS/Azure) pour chaque tenant, le convertit en
EUR (taux figé au moment de la collecte), et l'écrit en base avec
`granularity='daily'`.
**Test manuel :**
```bash
cd collector
python collect.py --config ../config.json --db ../cost_dashboard.db
```
**Rattraper une journée précise :**
```bash
python collect.py --config ../config.json --db ../cost_dashboard.db --date 2026-07-28
```
Un échec sur un tenant (token expiré, permissions...) n'empêche pas la
collecte des autres — journalisé en base (`status='error'`) et affiché
dans le dashboard plutôt que de laisser un trou silencieux.
Juste après, pour chaque tenant collecté avec succès, `collect.py` récupère
aussi le **détail par ressource** (bucket S3, instance EC2, VM Azure...) sur
une fenêtre glissante de **14 jours** — c'est le maximum que fournit AWS Cost
Explorer pour ce niveau de détail (limite dure de l'API, pas de notre code),
gardé identique côté Azure par cohérence. Alimente le panneau qui s'ouvre au
clic sur un service dans le dashboard (§6). Côté AWS, ça suppose d'avoir
activé **"Resource IDs"** dans les préférences Cost Explorer du compte — si
ce n'est pas fait, la collecte du détail échoue proprement pour ce tenant
(`resource_collection_status.status = 'unavailable'`) sans bloquer le reste,
et le dashboard l'affiche avec un message explicite plutôt qu'un vide.
**En dehors de Docker/Kubernetes**, installation en crontab classique :
```
0 6 * * * cd /opt/simple-cost-dashboard/collector && /opt/simple-cost-dashboard/venv/bin/python collect.py --config ../config.json --db ../cost_dashboard.db >> ../cron.log 2>&1
```
(Voir §7 pour l'équivalent en CronJob Kubernetes.)
## 6. Lancer le dashboard web
```bash
cd api
uvicorn main:app --reload --port 8000
```
Puis ouvre `http://localhost:8000`.
- `/` — vue globale : tous les tenants, coûts mensuels sur 12 mois glissants
- `/tenant/<aws|azure>/<nom>` — détail d'un tenant : graph 7j/30j avec
comparaison à la période précédente, répartition par service (30
derniers jours), total depuis le 1er janvier. Cliquer sur un service (dans
le graphe ou le tableau) ouvre un panneau avec le détail par ressource
(bucket, instance, VM...) sur les 14 derniers jours — voir §5 pour les
limites côté AWS (fenêtre 14j, "Resource IDs" à activer).
- `/health` — endpoint de liveness/readiness (utilisé par Kubernetes)
## 7. Docker
**Build :**
```bash
docker build -t simple-cost-dashboard:latest .
```
**Lancer le dashboard web :**
```bash
docker run -d \
--name cost-dashboard \
-p 8000:8000 \
-v $(pwd)/data:/data \
-v $(pwd)/config.json:/app/config.json:ro \
simple-cost-dashboard:latest
```
La base SQLite vit dans `/data` (volume monté) pour survivre aux
redémarrages/recréations du container.
**Lancer une collecte ponctuelle (même image, commande différente) :**
```bash
docker run --rm \
-v $(pwd)/data:/data \
-v $(pwd)/config.json:/app/config.json:ro \
simple-cost-dashboard:latest \
python collector/collect.py --config /app/config.json --db /data/cost_dashboard.db
```
C'est la **même image** pour le serveur web et pour la collecte — seule la
commande change. C'est le pattern que je reprendrai pour Kubernetes :
un `Deployment` (serveur web, commande par défaut de l'image) et un
`CronJob` (collecte, commande surchargée), tous deux basés sur la même
image `simple-cost-dashboard:latest`.
## 8. Structure du projet
```
simple-cost-dashboard/
├── Dockerfile
├── .dockerignore
├── requirements.txt
├── config.json # à créer (voir §2) — non versionné
├── cost_dashboard.db # créé automatiquement (local) / dans /data (Docker)
├── db/
│ └── schema.sql # schéma de la base
├── collector/
│ ├── providers.py # accès AWS Cost Explorer / Azure Cost Mgmt + conversion EUR
│ ├── db.py # connexion SQLite + écriture des relevés
│ ├── collect.py # cron quotidien (granularité journalière + détail ressources 14j)
│ ├── backfill_historical.py # rapatrie l'historique dispo depuis AWS/Azure (voir §4)
│ ├── import_historical.py # import ponctuel d'exports JSON déjà générés
│ ├── cost_report.py # CLI ponctuel : rapport multi-tenants
│ └── tenant_detail.py # CLI ponctuel : détail d'un tenant
├── api/
│ ├── main.py # app FastAPI (routes API + pages statiques + /health)
│ └── queries.py # agrégations (vue mensuelle, YTD, comparaison, services, ressources)
├── frontend/
│ ├── index.html # page globale
│ ├── tenant.html # page détail tenant
│ └── static/
│ ├── style.css
│ ├── common.js # utilitaires partagés (formatage, config Chart.js)
│ ├── app.js # logique page globale
│ └── tenant.js # logique page détail + graphes + drawer ressources
└── deploy/ # à venir : manifestes Kubernetes (voir TODO)
```
## 9. À faire
**Déploiement Kubernetes** (`deploy/`, pas encore créé) :
- [ ] Manifeste `Deployment` pour le serveur web (readiness/liveness sur `/health`)
- [ ] Manifeste `Service` + `Ingress`
- [ ] Manifeste `CronJob` pour la collecte quotidienne, même image, commande surchargée
- [ ] `PersistentVolumeClaim` pour `/data` (la base SQLite doit survivre aux redéploiements)
- [ ] `Secret` Kubernetes pour `config.json` (credentials Azure notamment) plutôt qu'un ConfigMap en clair
**Authentification** :
- [ ] Basic Auth via l'Ingress (annotation nginx `auth-basic` + `Secret` `htpasswd`) — solution de départ validée
- [ ] Bascule vers SSO Entra ID (app d'entreprise Azure + groupe) une fois le besoin confirmé — probablement via `oauth2-proxy` en sidecar/Ingress plutôt que dans le code de l'appli, pour ne pas coupler l'auth à FastAPI
**Credentials AWS/Azure en environnement conteneurisé** :
- [x] AWS : tranché pour des clés IAM dédiées par tenant (`access_key_id`/
`secret_access_key` dans `config.json`, droit `ce:GetCostAndUsage`
uniquement) plutôt que des profils `~/.aws` — les profils SSO ne
fonctionnent pas en conteneur (le refresh du token demande un
navigateur). Voir §2. Pas encore fait : créer ces clés IAM dédiées côté
AWS pour chaque tenant (elles n'existent pas encore, seuls des profils
SSO existent aujourd'hui) — et envisager `sts:AssumeRole` depuis une
identité unique si la rotation de 28+ clés IAM statiques devient trop
lourde à gérer.
- [ ] `config.json` (les clés AWS et les `client_secret` Azure) doit passer
en `Secret` Kubernetes monté en volume une fois `deploy/` créé, pas en
ConfigMap.
**Fiabilité / observabilité** :
- [ ] Alerte (Slack/email) si un tenant est en échec de collecte plusieurs jours de suite — actuellement visible seulement en se rendant sur le dashboard
- [ ] Sauvegarde régulière de la base SQLite (le PVC seul ne protège pas d'une corruption)
- [ ] Limite de rétention ou archivage si le volume de la base grossit significativement à long terme (non urgent : le volume attendu reste faible)
**Fonctionnel** :
- [ ] Vérifier le rendu du dashboard avec de vraies données (actuellement testé uniquement sur la structure du code, pas sur un run réel bout-en-bout)
- [ ] Décider si la page globale doit aussi permettre de filtrer/rechercher un tenant si leur nombre grossit beaucoup