ludal c39364eda4 feat: add services page and API endpoints for service cost breakdown
- Implemented new API endpoints for fetching global services and service tenants details.
- Created a new services.html page with a sidebar for navigation and a chart for service costs.
- Updated frontend JavaScript to handle service data loading and rendering.
- Enhanced the dashboard with provider filtering options for AWS and Azure.
- Added a segmented toggle for selecting the date range for service costs.
- Improved styling for the sidebar and data tables.
- Refactored common utility functions for better reusability across pages.
2026-09-20 21:55:52 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00
2026-08-07 17:19:49 +02:00

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)

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) :

{
  "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 :

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) :

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 :

cd collector
python collect.py --config ../config.json --db ../cost_dashboard.db

Rattraper une journée précise :

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

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 :

docker build -t simple-cost-dashboard:latest .

Lancer le dashboard web :

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) :

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)
S
Description
No description provided
Readme MIT
149 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 54.1%
JavaScript 28.5%
CSS 9.1%
HTML 7.1%
Dockerfile 1.2%