🌟 背景与目标

在企业级云环境中,往往使用多个云厂商服务(如阿里云、华为云、腾讯云),每个平台都有独立的计费系统。手动登录各平台查看余额效率低、易遗漏。

本文将带你从零开始,在 Ubuntu 22.04 系统上搭建一个 多云账户余额监控系统,具备以下能力:

✅ 支持阿里云、华为云、腾讯云余额查询
✅ Web 页面实时展示
✅ systemd 开机自启 & 日志管理
✅ 安全配置 AK/SK(环境变量隔离)
docker-compose 一键部署方案
✅ 生产级可维护架构


🧰 准备工作

✅ 系统环境

  • 操作系统:Ubuntu 22.04 LTS
  • Python 版本:Python 3.10(系统默认)
  • 用户权限:具备 sudo 权限
# 查看系统版本
lsb_release -a

# 查看 Python 版本
python3 --version  # 应输出 Python 3.10.x

第一步:安装基础依赖

1. 更新系统包索引

sudo apt update && sudo apt upgrade -y

2. 安装 Python 和 pip

Ubuntu 22.04 默认已安装 Python 3.10,但需手动安装 pip

sudo apt install -y python3-pip python3-venv

验证安装:

pip3 --version
# 输出示例:pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

第二步:安装并配置云厂商 CLI 工具

🔹 阿里云 CLI (aliyun)

安装
curl -sSL https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz | tar -xzf - -C /tmp
sudo mv /tmp/aliyun /usr/local/bin/
配置 Profile
aliyun configure --profile Domain
aliyun configure --profile BanLv
aliyun configure --profile BigData
aliyun configure --profile Sms

每次输入对应的 AccessKey ID 和 Secret

📌 参考文档:阿里云 CLI 配置指南


🔹 腾讯云 CLI (tccli)

安装
pip3 install --upgrade tccli
配置 Profile
tccli configure --profile YingYin_Service
tccli configure --profile Cunchu_Service

输入对应的 SecretId 和 SecretKey

📌 参考文档:腾讯云 CLI 快速入门


第三步:创建项目目录与虚拟环境(推荐)

# 创建项目目录
sudo mkdir -p /opt/bssopenapi
cd /opt/bssopenapi

# 创建虚拟环境(可选但推荐)
python3 -m venv venv
source venv/bin/activate

第四步:安装 Python 依赖

pip3 install flask huaweicloudsdkcore huaweicloudsdkbss requests urllib3

✅ 说明:

  • flask:Web 服务框架
  • huaweicloudsdk*:华为云 BSS SDK
  • requests:HTTP 请求库
  • urllib3:底层 HTTP 客户端(用于处理 HTTPS 警告)

第五步:编写主程序(略)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from flask import Flask, render_template_string
import subprocess
import json
from datetime import datetime
import os
import urllib3

# =============================
# 禁用 InsecureRequestWarning 警告
# =============================
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# =============================
# 华为云 SDK 导入
# =============================
from huaweicloudsdkcore.auth.credentials import GlobalCredentials
from huaweicloudsdkcore.http.http_config import HttpConfig
from huaweicloudsdkbss.v2.region.bss_region import BssRegion
from huaweicloudsdkbss.v2 import BssClient, ShowCustomerAccountBalancesRequest
from huaweicloudsdkcore.exceptions import exceptions

app = Flask(__name__)

# =============================
# 阿里云账户配置
# =============================
ALIYUN_ACCOUNTS = [
    {"profile": "Domain", "name": "Aliyun Domain"},
    {"profile": "BanLv", "name": "Aliyun BanLv"},
    {"profile": "BigData", "name": "Aliyun BigData"},
    {"profile": "Sms", "name": "Aliyun SMS"},
]

def get_aliyun_balance(profile):
    """通过 aliyun CLI 查询阿里云账户余额"""
    cmd = [
        "/usr/local/bin/aliyun",
        "--profile", profile,
        "bssopenapi",
        "QueryAccountBalance",
        "--region", "cn-hangzhou"
    ]
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=10
        )
        if result.returncode != 0:
            return {"error": result.stderr.strip()}

        data = json.loads(result.stdout)
        if not data.get("Success"):
            return {"error": data.get("Message", "Unknown error")}

        amount_str = data["Data"].get("AvailableAmount", "0")
        currency = data["Data"].get("Currency", "CNY")

        try:
            amount = float(amount_str.replace(',', ''))
        except (TypeError, ValueError):
            amount = 0.0

        return {"amount": amount, "currency": currency, "error": None}
    except Exception as e:
        return {"error": str(e)}

def get_huaweicloud_balance():
    """
    使用华为云 SDK 查询账户余额
    关键:使用正确的字段名 'amount' 而非 'available_amount'
    """
    try:
        ak = os.environ.get("CLOUD_SDK_AK")
        sk = os.environ.get("CLOUD_SDK_SK")
        if not ak or not sk:
            return {"error": "Huawei Cloud AK/SK not set"}

        credentials = GlobalCredentials(ak, sk)
        config = HttpConfig()
        config.ignore_ssl_verification = True
        config.timeout = 10

        client = BssClient.new_builder() \
            .with_http_config(config) \
            .with_credentials(credentials) \
            .with_region(BssRegion.value_of("cn-north-1")) \
            .build()

        request = ShowCustomerAccountBalancesRequest()
        response = client.show_customer_account_balances(request)

        balances = response.to_dict().get("account_balances", [])
        total_amount = 0.0

        # account_type 映射表
        ACCOUNT_TYPE_NAMES = {
            1: "现金账户", 5: "免费试用金", 4: "合作伙伴奖励金", 6: "代金券账户"
        }

        for account in balances:
            acc_type = account.get("account_type")
            amount_str = account.get("amount", "0")  # ✅ 正确字段
            try:
                amount = float(amount_str)
            except (TypeError, ValueError):
                amount = 0.0
            total_amount += amount

        return {
            "amount": round(total_amount, 2),
            "currency": "CNY",
            "error": None
        }

    except exceptions.ClientRequestException as e:
        return {"error": f"{e.error_code}: {e.error_msg}"}
    except Exception as e:
        return {"error": f"Huawei SDK Error: {str(e)}"}

def get_tencent_balance(profile, display_name):
    """调用 tccli 查询腾讯云账户余额"""
    cmd = [
        "tccli",
        "--profile", profile,
        "billing",
        "DescribeAccountBalance",
        "--cli-unfold-argument",
        "--region", "ap-guangzhou"
    ]
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=10
        )
        if result.returncode != 0:
            return {"error": result.stderr.strip()}

        data = json.loads(result.stdout)
        # ✅ 使用 RealBalance(单位:分),转为元
        amount_in_fen = data.get("RealBalance", 0)
        amount_in_yuan = amount_in_fen / 100.0  # 分 → 元

        return {
            "amount": round(amount_in_yuan, 2),
            "currency": "CNY",
            "error": None
        }
    except Exception as e:
        return {"error": f"TCCLI Error: {str(e)}"}

@app.route('/')
def index():
    results = []

    # === 阿里云余额 ===
    for acc in ALIYUN_ACCOUNTS:
        resp = get_aliyun_balance(acc["profile"])
        if resp["error"]:
            balance_text = f"<span style='color:#d9534f;'>Error: {resp['error']}</span>"
        else:
            balance_text = f"<strong style='color:#5cb85c;'>{resp['amount']:,.2f}</strong> CNY"
        results.append({
            "display_name": acc["name"],
            "balance": balance_text
        })

    # === 华为云余额 ===
    hc_resp = get_huaweicloud_balance()
    if hc_resp["error"]:
        balance_text = f"<span style='color:#d9534f;'>Error: {hc_resp['error']}</span>"
    else:
        balance_text = f"<strong style='color:#5cb85c;'>{hc_resp['amount']:,.2f}</strong> CNY"
    results.append({
        "display_name": "Huawei Cloud",
        "balance": balance_text
    })

    # === 腾讯云余额 ===
    tencent_accounts = [
        {"profile": "YingYin_Service", "name": "Tencent YingYin"},
        {"profile": "Cunchu_Service", "name": "Tencent Cunchu"}
    ]

    for acc in tencent_accounts:
        resp = get_tencent_balance(acc["profile"], acc["name"])
        if resp["error"]:
            balance_text = f"<span style='color:#d9534f;'>Error: {resp['error']}</span>"
        else:
            balance_text = f"<strong style='color:#5cb85c;'>{resp['amount']:,.2f}</strong> CNY"
        results.append({
            "display_name": acc["name"],
            "balance": balance_text
        })

    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    html_template = '''
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8" />
        <title>☁️ Multi-Cloud Balance Monitor</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f4f6f9; color: #333; line-height: 1.6; }
            .container { width: 90%; max-width: 600px; margin: 40px auto; padding: 20px; }
            .header { text-align: center; margin-bottom: 30px; }
            .header h1 { color: #0056b3; font-size: 28px; margin: 0; }
            .card { background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 24px; margin-bottom: 20px; }
            .account-item { font-size: 18px; margin: 16px 0; display: flex; justify-content: space-between; align-items: center; }
            .account-name { font-weight: 600; color: #333; min-width: 120px; }
            .footer { text-align: center; margin-top: 30px; color: #999; font-size: 13px; }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>☁️ Multi-Cloud Account Balance Monitor</h1>
                <p>Real-time monitoring for Aliyun, Huawei Cloud & Tencent Cloud</p>
            </div>
            <div class="card">
                {% for r in results %}
                    <div class="account-item">
                        <span class="account-name">{{ r.display_name }}</span>
                        <span>{{ r.balance|safe }}</span>
                    </div>
                {% endfor %}
            </div>
            <div class="footer">
                <p>Last updated: <strong>{{ now }}</strong></p>
                <p>Tip: Refresh page to get latest data</p>
            </div>
        </div>
    </body>
    </html>
    '''

    return render_template_string(html_template, results=results, now=now)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

第六步:安全配置华为云 AK/SK

为了避免密钥硬编码,我们使用 .env 文件管理。

创建 .env 文件

sudo vim /opt/bssopenapi/.env

内容:

CLOUD_SDK_AK=your_huawei_cloud_ak_here
CLOUD_SDK_SK=your_huawei_cloud_sk_here

设置安全权限

sudo chmod 600 /opt/bssopenapi/.env
sudo chown root:root /opt/bssopenapi/.env

第七步:配置 systemd 服务(生产级运行)

创建 service 文件

sudo vim /etc/systemd/system/cloud-balance-monitor.service

内容:

[Unit]
Description=Multi-Cloud Balance Monitor (Aliyun + Huawei Cloud + Tencent Cloud)
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/bssopenapi
ExecStart=/usr/bin/python3 /opt/bssopenapi/app.py
Restart=always
RestartSec=5

# 加载安全环境变量
EnvironmentFile=/opt/bssopenapi/.env

StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=cloud-balance-monitor

MemoryLimit=512M

[Install]
WantedBy=multi-user.target

启用服务

sudo systemctl daemon-reload
sudo systemctl enable cloud-balance-monitor.service
sudo systemctl start cloud-balance-monitor.service

查看状态与日志

# 查看服务状态
sudo systemctl status cloud-balance-monitor.service

# 查看实时日志
sudo journalctl -u cloud-balance-monitor.service -f

第八步:Docker Compose 一键部署(终极方案)

对于希望快速部署或跨环境迁移的用户,我们提供 docker-compose.yml 方案。

1. 安装 Docker 和 Docker Compose

# 安装 Docker
sudo apt install -y docker.io
sudo systemctl enable docker --now

# 安装 Docker Compose
sudo apt install -y docker-compose

2. 创建项目结构

mkdir -p ~/cloud-balance && cd ~/cloud-balance
mkdir config scripts

3. 编写 docker-compose.yml

version: '3.8'

services:
  balance-monitor:
    image: python:3.10-slim
    container_name: cloud-balance-monitor
    restart: unless-stopped
    working_dir: /app
    volumes:
      - ./scripts:/app
      - ./config/.env:/app/.env:ro
    environment:
      - PYTHONUNBUFFERED=1
    command: >
      bash -c "
      pip install flask huaweicloudsdkcore huaweicloudsdkbss tccli &&
      python /app/app.py
      "
    ports:
      - "5000:5000"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

4. 准备文件

  • app.py 放入 ~/cloud-balance/scripts/
  • .env(含华为云 AK/SK)放入 ~/cloud-balance/config/

5. 启动服务

docker-compose up -d

6. 查看日志

docker logs -f cloud-balance-monitor

🚀 访问服务

打开浏览器访问:

http://<your-server-ip>:5000

你将看到如下页面:

云厂商 账户名称 余额
Aliyun Domain ¥1,234.56
Huawei Cloud Huawei Cloud ¥30,334.14
Tencent YingYin_Service ¥124,483.12
Tencent Cunchu_Service ¥8,765.43

🔐 安全建议

项目 建议
AK/SK 存储 使用 .env 文件 + 600 权限,禁止明文写入代码
服务运行用户 使用非 root 用户(当前为 root,生产可降权)
HTTPS 建议配合 Nginx + Let’s Encrypt 实现 HTTPS
网络访问 使用防火墙限制 /5000 端口仅内网访问

🛠️ 故障排查

❌ 华为云返回 0 余额?

  • 检查是否误用了 available_amount → 正确字段是 amount
  • 确认 AK/SK 有 BSSReadOnlyAccess 权限

❌ 腾讯云显示 0?

  • 确保使用 RealBalance 字段
  • 单位是“分”,必须 /100 转为“元”

❌ 提示 InsecureRequestWarning

  • 已通过 urllib3.disable_warnings() 抑制
  • 生产环境建议配置 CA 证书验证

🌈 扩展建议

功能 实现方式
钉钉/企业微信告警 当余额 < 阈值时,调用 Webhook
数据持久化 将历史余额写入 SQLite 或 Prometheus
多实例高可用 配合 Nginx 做负载均衡
UI 升级 使用 Vue + Element Plus 做前端

📚 总结

本文完整实现了:

  1. ✅ Ubuntu 22.04 环境准备
  2. ✅ Python 3.10 + pip3 安装
  3. ✅ 阿里云、腾讯云 CLI 配置
  4. ✅ 华为云 SDK 安全集成
  5. ✅ systemd 服务管理
  6. docker-compose 一键部署

你现在拥有了一个 稳定、安全、可扩展的多云余额监控系统


🔗 参考链接

  • 阿里云 CLI:https://help.aliyun.com/document_detail/121422.html
  • 腾讯云 CLI:https://cloud.tencent.com/document/product/1097/34575
  • 华为云 SDK for Python:https://support.huaweicloud.com/sdkreference-python-sdk/bss.html
  • urllib3 TLS Warnings:https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings

更多推荐