前言:为什么要自己拉三云账单

每个月最后一周,财务扔过来一个问题:

「上个月咱们三朵云加起来花了多少钱?每个账号分别多少?哪些服务是大头?」

我们公司 AWS / 阿里云 / 腾讯云全在用,AWS 还有 3 个子账号、阿里云 2 个主账号、腾讯云 2 个国际站账号。光是把这些控制台挨个点开看本月合计,至少 15 分钟,而且:

  1. 看不到”过去 6 个月 EC2 在 AWS 的趋势”,控制台只有本月
  2. Excel 想统一格式发给财务,但三家 API 返回结构完全不一样
  3. 半夜 12 点想看下今天累计账单,控制台没有”昨日累计”,得第二天才看得到
  4. 退款、调账这种负数行经常藏在明细里,月度概览看不到

于是就有了 multi-cloud-billing —— 一个本地跑的「三云账单聚合」小工具:

  • AWS / 阿里云 / 腾讯云 各家账单拉下来,落到本地 SQLite
  • 带图表的 Web 仪表盘 + Excel 文件下载
  • 账号管理 UI(AK/SK Fernet 加密、添加账号自动拉 6 个月历史)

定位很明确:自托管的 FinOps 控制台——不替代云厂商控制台、不做预算告警,纯粹是把分散在三个控制台的月度账单汇总成一个视图,方便月底对账和看趋势。


它解决什么问题

把上面 4 件事压成一个本地网页

场景 之前 现在
月底看本月合计 3 家控制台挨个看 15min 打开 /,5 秒看本月合计(多币种并列)
看 EC2 历史趋势 控制台只有本月,自己存 切换月份滑块,自动从 SQLite 读
统一格式发财务 自己拼 Excel 累半天 点下载,3 sheet 的 xlsx 直接拿
半夜看昨日累计 没办法 SQLite 里有每日账单明细

核心能力

1. 仪表盘(首页 /

四张 ECharts 图:

  • 本月合计(多币种并列:$34,852.91 + ¥128,400.00不强行换算
  • 账号占比饼图(按 vendor 配色:AWS 橙 / 阿里红 / 腾讯蓝)
  • 服务 Top 10(横向条形图,看钱花在哪类服务)
  • 账号分布(堆叠柱,每个账号一根)

可按月份切换;切换的月份自动从 SQLite 读。

2. 月账单页(/monthly

  • 账号汇总表:每个账号的当月合计、币种、退款标记(负数行单独展示)
  • 服务 Top 15 横向条形图
  • 历史 Excel 下载列表:最近 6 个月的 xlsx 文件
  • 🔍 详情 按钮 → 弹窗显示该账号当月所有账单明细行(带中文服务名
  • ❓ 退款说明 按钮 → 弹窗显示该账号当月所有负数行 + 为什么会出现负数
  • 🔄 手动全量拉取 按钮 → SSE 实时推送拉取进度(每个账号、每页 API 调用都能看到)

3. 账号配置(/accounts,需要登录)

后台管理界面:

  • 新增 / 编辑 / 禁用 AWS、阿里云、腾讯云账号
  • AK/SK 在数据库里 Fernet 加密存储,页面只显示脱敏后 4 位(如 ****abcd
  • 从 .env 导入按钮(首次部署时一键把旧 .env 里的账号搬过来)
  • 后台拉取日志卡片(新增账号时会跑 6 个月历史 + 当月,实时显示每步状态)

4. Excel 下载(/download/xlsx/{filename}

每月一份 xlsx,三 sheet

  • 概览:账号 × 币种 × 月度合计 + 状态(正常 / Auth_Error / Error
  • 按服务汇总:每个账号的服务级明细
  • 统一明细(可选):三云的详细行级账单统一列结构

文件名 billing-YYYY-MM.xlsx,浏览器直接拉。

5. 定时刷新

每天凌晨 1 点自动跑:调 API → 写 SQLite → 从 SQLite 重建 xlsx。

调度用 launchd(macOS) 而非 cron —— 系统重启自动续上,比 cron 稳。Linux 也能改成 systemd timer。


架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ AWS Cost │ │ 阿里云 BSS │ │ 腾讯云 │
│ Explorer │ │ QueryBill │ │ intlpartners │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────────┼────────────────────┘

┌─────────────────────┐
│ app/main.py
│ fetch_aws/aliyun/ │
tencent() │
└──────────┬──────────┘


┌─────────────────────┐
│ data/bills.db │ ← SQLite (主键去重)
│ bill_items 表 │
└──────────┬──────────┘

┌────────────┼────────────
▼ ▼
┌──────────┐ ┌──────────┐
│ Web 仪表 │ │ xlsx │
│ ECharts │ │ openpyxl │
└──────────┘ └──────────┘

关键设计决策

  1. 数据源唯一:所有页面读 SQLite,不直接调 API(拉取时把 API 结果 upsert 进 SQLite)
  2. xlsx 从 DB 重建:凌晨定时任务拉完 API 后,从 DB 重建 xlsx,保证离线也能导出
  3. 新增账号触发历史拉取:accounts 表插一条 → 自动后台拉最近 6 个月 + 当月
  4. 凭证 Fernet 加密:AK/SK 不存明文,页面只显示 ****abcd

货币策略 · 有意不做汇率换算

每个账号保留厂商原生货币不做换算

  • AWS → USD
  • 阿里云 → CNY(账单上写人民币,但 Currency 字段是 “USD” 时按 USD 走)
  • 腾讯云 → CNY / USD(看 sub-customer 合同类型)

页面里 USD 和 CNY 分别合计,不强行加总。Excel 每行带 货币 列。

这样设计是有意为之——汇率每月变,如果按某日汇率折算再加总,对账时反而对不上。让财务按她自己查到的汇率算更稳。


安全模型

  • 凭证 Fernet 加密:AK/SK 落 SQLite 前用 BILLING_CREDENTIALS_KEY 加密;key 在 .env
  • 管理界面要密码/accounts 路由要求 session 登录,密码哈希在 .env(PBKDF2-SHA256 200k 轮)
  • 最小权限:每个厂商给 RAM/CAM 子账号只挂”账单只读”策略
  • 本地优先:默认监听 0.0.0.0:8088部署时建议加 nginx 反代 + basic auth,或者改成 127.0.0.1 只本机访问

技术栈

选型 为什么
Web 框架 FastAPI + Jinja2 一个进程同时提供 HTML 页面和 JSON API
图表 ECharts 5(CDN) 暗色主题 + 厂商配色灵活、Tooltip 美观
数据 SQLite(单文件) 个人工具零运维;200MB 单文件 100 万行足够撑几年
调度 launchd(macOS) 系统重启自动续上,比 cron 稳
Excel openpyxl 支持多 sheet + 表头合并 + 列宽自适应
云 SDK boto3 + alibabacloud_bssopenapi + tencentcloud-sdk 官方 SDK,省心

依赖列表(requirements.txt):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boto3>=1.34
fastapi>=0.110
uvicorn[standard]>=0.30
jinja2>=3.1
openpyxl>=3.1
requests>=2.32
python-dotenv>=1.0
cryptography>=42
itsdangerous>=2.0

# Aliyun
alibabacloud_bssopenapi20171214>=2.0.3
aliyun-python-sdk-core>=2.15

# Tencent
tencentcloud-sdk-python>=3.0
tencentcloud-sdk-python-intl-en>=3.0 # 国际站 sub-customer 必需

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
multi-cloud-billing/
├── app/
│ ├── main.py # 三云拉账单核心 (fetch_aws/aliyun/tencent)
│ ├── pusher.py # 生成 xlsx
│ └── service_zh.py # AWS 服务码 → 中文名映射
├── web/
│ ├── app.py # FastAPI 路由:仪表盘、月账单、账号管理、SSE 进度
│ ├── db.py # SQLite 表结构 + upsert/query
│ ├── account_store.py # 账号 CRUD(加密 AK/SK)
│ ├── secrets.py # Fernet + PBKDF2 密码哈希
│ ├── templates/ # Jinja2 (dashboard/monthly/accounts/login)
│ └── static/style.css # 暗色主题
├── data/bills.db # SQLite(账单明细 + 加密账号)
├── file/ # 生成的 xlsx
├── logs/ # launchd 任务日志
├── scripts/ # CLI 工具
├── launchd/ # macOS plist
└── docs/
├── intro.md # 你正在看的
└── deployment.md # 部署文档

快速开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd ~/ops/multi-cloud-billing

# 1. 虚拟环境
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# 2. .env:把 .env.example 复制成 .env,然后填账号
cp .env.example .env
$EDITOR .env

# 3. 启动
cd app
../.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8087

打开 http://localhost:8087/,登录账号管理页(/accounts)添加三云账号。

1
2
3
4
# 手动拉一次(调试用)
cd app
../.venv/bin/python pusher.py # 上个月
../.venv/bin/python pusher.py 2026-06 # 指定月份

已知限制 · 老老实实承认

现状 影响
单账号明细行 上限 500 行(防止超大账号炸前端) 详细看 /api/details/monthly?truncated=true
阿里云分页 PageSize=300(API 上限) 月度 < 300 服务一般够,超出会只取首页
腾讯云限流 每秒 ≤ 1 次,整进程加锁串行 多账号拉一次需 1-2 分钟(已显示进度)
多币种合计 不做汇率换算 USD 和 CNY 分开展示
预算告警 没做 财务自己盯 Excel 通知

后续可做

  • 预算 / 同比环比告警
  • 服务按业务标签分类(”开发 / 生产 / 测试”分组汇总)
  • AWS / 阿里云 Savings Plans 覆盖率统计
  • 数据导出到 S3 / OSS 做长期归档

不是什么

跟前面两篇博客一样,先把边界说清楚:

  • 不是云厂商控制台替代品:底层账单还是调各云官方 API,自托管只解决”汇总 + Excel + 历史趋势”
  • 不是 FinOps 平台:没做预算告警 / 同比环比 / 成本归因这些”企业级 FinOps”功能
  • 不是商业级高可用:单 SQLite 文件,崩了丢历史,但本地工具无所谓
  • 不做汇率换算:上面”货币策略”单独说过
  • 不替代财务系统:Excel 是给财务的中间产物,最终记账还是用专业财务软件

把分散在三个云控制台的”每月对账 15 分钟”压成”打开本地网页 5 秒看本月 + 切月份看趋势 + 一键下 Excel”——单 SQLite、单 FastAPI、launchd 调度,整套 Python 3.14 + 12 个依赖包就能跑。