
1. 为什么会有 CloddsBot从“看板疲劳”到自动化值守先交代一个背景我手上同时维护着几个不同云厂商的账号加起来有几十台云主机、数据库实例、负载均衡、对象存储桶。每天早上开工第一件事就是挨个登录控制台刷新实例列表看看有没有人半夜把磁盘打满或者证书快过期了。这种巡检式工作做一两个星期没什么做一两个月就非常折磨——你明知道这些事情大概率没问题但你又不敢不看。CloddsBot 就是在这种情况下写出来的一个轻量巡检机器人。名字拆开看是 Cloud Logs Bot思路很直接它替我们把“盯云资源”这件事自动化掉。项目做的事情归结起来就三件定时调用云厂商的 OpenAPI 拉取资源状态用一组可配置规则判断哪些状态算“异常”把异常信息推送到钉钉或飞书群顺便留好操作入口。整套东西跑在一台 1C2G 的轻量云主机上成本可以忽略。这个项目适合谁一种是账号多、实例多但又不愿意上一整套 Prometheus Grafana 的个人开发者或小团队另一种是想接触云厂商 OpenAPI 封装、定时任务调度、IM Webhook 集成想看一个完整可落地例子的后端学习者。这篇文章我会把架构选型、核心代码逻辑、部署方式和踩过的坑都展开讲代码量不大复制下来改改配置就能用。1.1 每天花半小时做重复劳动问题不只是累先说痛点。我最早也试过“云厂商自带监控 短信告警”但实际用下来有几个别扭的地方多账号之间切换要重新登录不同厂商控制台设计不一致默认告警规则又太粗——比如磁盘使用率超过 80% 就短信轰炸而很多时候只是测试机的临时数据增长并不值得半夜爬起来处理。真正需要关注的“实例意外宕机”“云盘即将到期”“SSL 证书链断裂”这类事件反而没有现成规则能覆盖。更麻烦的是账号一多控制台里的状态信息就变成了碎片。实例列表要看一遍RDS 要看一遍负载均衡要看一遍对象存储桶的权限到期又要看一遍。看完还容易忘第二天再重复一轮。时间一长就会意识到这种巡检本质上是在做“状态比对”和“异常判断”而这恰恰是程序最擅长的事情。1.2 现有开源监控体系为什么“用不动”我也认真评估过 Prometheus Grafana Alertmanager 这套标准组合。好处是生态成熟、指标能力强但问题也明显部署和升级有维护成本告警规则要用 PromQL 写学习曲线对于只想“盯几台机器”的场景来说有点重。如果目标是从云厂商 API 层面管资源生命周期而不是从操作系统层面采指标那这套体系的收益其实是打折的。CloddsBot 选择的是另一条路不采集 CPU 内存这些性能指标这类需求用云监控就够了只聚焦“状态类事件”——实例是否 running、到期时间是否临近、账单是否异常、日志里有没有新增 ERROR。状态类事件数量少、判定逻辑直白用一个轻量规则引擎就能搞定完全不需要庞大的采集器和时序数据库。常见需求传统做法CloddsBot 的做法多账号实例状态巡检登录控制台逐个看定时调用 OpenAPI 汇总到一张状态表磁盘 / 费用告警云监控默认阈值短信轰炸自定义规则 时间窗口去重 批量聚合证书到期提醒第三方在线检测工具本地真实握手验证 提前 30 天推送操作记录追溯控制台操作日志分散在各厂商统一落到 SQLite 审计表一条命令查看1.3 项目的三条设计原则CloddsBot 从第一天起给自己定了三条规矩第一配置驱动所有账号、区域、规则都写在 YAML 里加账号不用改代码第二只读优先默认只调用云厂商的查询类接口涉及重启、扩容的操作全部走独立命令并记录审计第三推送必须有人看告警进 IM 群比进邮箱有效得多群里的历史记录还天然可检索。2. 三个核心模块与选型逻辑采集器、规则引擎、通知器整个 CloddsBot 可以拆成三个模块这也是大多数类似工具的标准拆法采集器Collector负责对接各云厂商 OpenAPI规则引擎Evaluator负责把采集到的原始状态翻译成“正常、异常、需要关注”通知器Notifier负责把判定结果推送到 IM并对接人工操作。三个模块之间通过队列或共享数据库解耦现阶段数据量不大直接用轮询 SQLite 就够了。2.1 数据流定时触发、状态归并、结果落库流程大概是这样的APScheduler 按 cron 表达式触发一次巡检任务采集器依次遍历配置中的每个账号和区域拉取资源清单规则引擎将原始字段归一到统一状态模型然后逐条匹配规则命中异常的事件先写入 SQLite 的 events 表再经过告警抑制判断决定是否真正推送到群。所有历史事件落库还有一个好处隔周想复盘“上周到底出了几次问题”一条 SQL 就能查出来。SELECT date(created_at), level, count(*) FROM events WHERE created_at datetime(now, -7 days) GROUP BY date(created_at), level;2.2 为什么是 Python APScheduler 而不是 Crontab Shell选择 Python 几乎是必然的主流云厂商都提供官方 SDKpip install 就能用异常处理和 JSON 解析写起来比 Shell 舒服太多。调度部分我没有用系统 Crontab而是用 APScheduler 的 CronTrigger原因是它支持错过任务补偿——如果进程在巡检时间点恰好重启APScheduler 会在恢复后补跑而 Crontab 错过了就是错过了。常驻进程模式也比每分钟唤起一个脚本来得更稳依赖连接和日志句柄都能提前初始化。调度配置示例from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger scheduler BlockingScheduler(timezoneAsia/Shanghai) scheduler.add_job( run_instance_check, CronTrigger.from_crontab(*/5 * * * *, timezoneAsia/Shanghai), idinstance_check, misfire_grace_time60, ) scheduler.add_job( run_cert_check, CronTrigger.from_crontab(0 9 * * *, timezoneAsia/Shanghai), idcert_check, misfire_grace_time3600, )这里 misfire_grace_time 也是容易被忽略的参数它表示任务原定执行时间点过后多少秒内仍可以补跑。设得太短进程重启稍微慢一点就会跳过巡检设得太长低频任务可能在错误的时间点突然补跑一堆。我习惯状态检查给 60 秒每天一次的检查给 1 小时。2.3 通知通道为什么选 IM 自定义机器人 Webhook钉钉、飞书、企业微信都支持自定义机器人 Webhook使用门槛最低只需要出站网络请求不需要暴露任何入站端口。这对安全很友好CloddsBot 所在的主机可以不开任何公网入站规则。需要强调一点这类 Webhook 是单向的机器人只能往群里发消息收不到群成员回复。所以我把“确认后执行操作”这个闭环放在了一个独立命令行工具里而不是试图在群里做交互。这样设计是权衡过的自定义机器人做交互往往要升级为企业自建应用涉及权限申请和回调服务对个人项目来说太重了。2.4 配置即代码YAML 与环境变量分家的做法账号信息属于敏感内容绝对不能写死在 YAML 里提交到 Git。我采用的模式是YAML 只保存账号名、区域、规则这些非敏感内容密钥通过环境变量或 docker-compose 的 env_file 注入程序读取环境变量后组装 SDK 客户端。这样即使代码仓库泄露攻击者拿到的也只是一串标识拿不到真正的密钥。# config/accounts.yaml accounts: - name: demo provider: aliyun regions: - cn-hangzhou - cn-beijing access_key_env: DEMO_ALIYUN_AK secret_key_env: DEMO_ALIYUN_SK - name: staging provider: tencent regions: - ap-guangzhou access_key_env: STAGING_TENCENT_AK secret_key_env: STAGING_TENCENT_SK密钥在 .env 文件里管理DEMO_ALIYUN_AKLTAI5t... DEMO_ALIYUN_SKxxxx STAGING_TENCENT_AKAKID... STAGING_TENCENT_SKxxxx3. 核心功能实现从取数到告警再到操作的完整链路下面我把代码层面的实现拆开讲。这部分不追求把每个文件都贴出来重点讲清楚几个容易出问题的环节是怎么处理的。3.1 云资源采集分页、状态归一化和多账号遍历云厂商 OpenAPI 基本都需要分页拉取。拿阿里云 ECS 的 DescribeInstances 举例每页最大返回 100 条如果实例数超过 100必须根据 TotalCount 循环翻页。# collector/aliyun_ecs.py import json from aliyunsdkcore.client import AcsClient from aliyunsdkecs.request.v20140526.DescribeInstancesRequest import DescribeInstancesRequest def list_instances(access_key, secret_key, region_id): client AcsClient(access_key, secret_key, region_id) request DescribeInstancesRequest() request.set_PageSize(100) page 1 instances [] while True: request.set_PageNumber(page) response json.loads(client.do_action_with_exception(request)) instances.extend(response.get(Instances, {}).get(Instance, [])) if page * 100 response.get(TotalCount, 0): break page 1 return instances这段代码本身不复杂真正需要上心的是“状态归一化”。阿里云返回的是 Running、Stopped腾讯云返回的是 RUNNING、STOPPEDAWS 返回的是 running、stopped——如果规则引擎直接拿原始字符串去匹配同一套规则没法应用到多账号。CloddsBot 在采集层维护了一张映射表把各厂商的状态统一为 running、stopped、starting、stopping、expired、error 几种枚举规则引擎只认枚举值。云厂商原始状态示例归一化状态阿里云Running / Stopped / Expiredrunning / stopped / expired腾讯云RUNNING / STOPPED / ISOLATEDrunning / stopped / expired华为云ACTIVE / SHUTOFF / ERRORrunning / stopped / error多账号遍历时我会用并发限制来控制对云厂商 API 的请求速率。不是所有请求越快越好很多账号默认 QPS 很低几个区域同时拉很容易触发限流。实现上我用 ThreadPoolExecutor 加 max_workers4每个账号之间用独立令牌桶错峰实测这个配置最稳。3.2 规则引擎阈值判断 时间窗口 告警抑制规则引擎是 CloddsBot 的核心我用 rules.yaml 描述规则加载后变成内存中的规则对象。# config/rules.yaml rules: - name: instance_down target: instance.status condition: ! running level: warning window: 15m - name: disk_usage_high target: instance.metrics.disk_usage condition: 85 level: critical window: 30m aggregate: true - name: cert_expire_soon target: cert.days_left condition: 30 level: warning window: 1d这里有两个细节值得展开。第一是 window 字段它是“事件去重的观察窗口”同一台实例如果持续宕机超过 30 分钟规则引擎不会每 5 分钟推一次告警而是窗口内只保留第一条避免告警风暴。第二是 aggregate 字段它解决的是“多台实例同时异常”的合并问题。真实场景里很多所谓宕机是机房网络抖动导致的一批实例同时不可达逐台推送毫无意义反而让人忽略真正重要的单点故障。CloddsBot 会在一次巡检中对同一区域内多次命中同一条规则的事件做聚合合并成一条“该区域有 12 台实例状态异常列表如下”的汇总消息。聚合消息的效果大概是这样的【区域异常汇总】cn-hangzhou 共 12 台实例不可达 - i-bp1234abcd-web-01 - i-bp5678efgh-web-02 - i-bp9012ijkl-db-01 ... 首次检出时间2024-06-01 10:23:00 当前状态已持续 6 分钟3.3 Webhook 推送加签与消息模板钉钉机器人支持三种安全设置我建议直接用“加签”。签名逻辑是时间戳换行拼接密钥用 HmacSHA256 计算后做 Base64 和 URL 编码拼到 Webhook 地址上。# notifier/dingtalk.py import time import hmac import hashlib import base64 import urllib.parse import requests def build_signed_url(webhook, secret): timestamp str(round(time.time() * 1000)) string_to_sign f{timestamp}\n{secret} hmac_code hmac.new( secret.encode(), string_to_sign.encode(), digestmodhashlib.sha256 ).digest() sign urllib.parse.quote_plus(base64.b64encode(hmac_code)) return f{webhook}timestamp{timestamp}sign{sign} def push_markdown(webhook, secret, title, text): url build_signed_url(webhook, secret) payload { msgtype: markdown, markdown: {title: title, text: text}, } requests.post(url, jsonpayload, timeout10)消息模板我强烈建议用 Markdown 格式而不是纯文本。告警消息里至少包含异常资源名称、账号 / 区域、当前状态、持续时间、可能的处理操作。因为群里消息是给人快速判断的丢掉关键信息等于没推。推送失败时也要有兜底逻辑requests 返回非 200 时把告警事件标记为 pending等下一轮巡检重试而不是直接丢弃。3.4 操作闭环cloddsbotctl 与审计表IM 推送只解决“发现问题”真正“处理问题”我放在了 cloddsbotctl 这个命令行工具里。cloddsbotctl status --account demo cloddsbotctl restart --account demo --instance-id i-bp1234abcd cloddsbotctl audit --last 50为什么不用 Web 界面因为一个 Web 控制面意味着要处理登录鉴权、会话管理、CSRF 这些问题对内部工具来说维护成本不划算。SSH 到主机跑一条命令反而是最透明、最容易审计的方式。每次执行操作都会往 SQLite 的 operations 表里写入操作人、目标资源、操作类型、结果状态。操作前还会先调用查询接口确认资源当前状态避免对一台已经处于 stopping 状态的实例重复下发 restart这种保护在云 API 的幂等性不够完善的场景下非常必要。operations 表结构设计很简单CREATE TABLE operations ( id INTEGER PRIMARY KEY AUTOINCREMENT, operator TEXT NOT NULL, account TEXT NOT NULL, resource_id TEXT NOT NULL, action TEXT NOT NULL, status TEXT NOT NULL, message TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now)) );3.5 数据存储为什么 SQLite 就够用CloddsBot 的事件、告警、操作记录数据量一天最多几百条一年也不到十万条SQLite 单文件完全能扛住。用 SQLite 还有一个好处是备份简单——定时把 db 文件复制到对象存储即可不需要搭数据库服务。唯一需要留意的点多线程写入时 SQLite 可能遇到 database is locked解决方案是开启 WAL 模式并让写入走单一线程。import sqlite3 conn sqlite3.connect(data/cloddsbot.db) conn.execute(PRAGMA journal_modeWAL;) conn.execute(PRAGMA synchronousNORMAL;)开启 WAL 之后读和写可以并行日常使用基本不会碰到锁问题。4. 部署落地容器化、Systemd 守护与参数调优4.1 Docker 镜像与 compose 配置先看 Dockerfile我基于 python:3.11-slim安装依赖后直接运行主程序FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, main.py]docker-compose.yml 中需要特别注意两点挂载配置目录和数据库目录到宿主机设置 restart: always。services: cloddsbot: build: . restart: always volumes: - ./config:/app/config:ro - ./data:/app/data env_file: - .env配置目录只读挂载防止进程运行时意外修改配置文件数据目录持久化这样容器重建后历史事件和审计记录不会丢。还有个细节容器时区默认是 UTC如果不想在代码里处处处理时区可以在 compose 里加 environment 的 TZAsia/Shanghai但记得代码里的调度表达式还是显式指定 timezone 最保险。4.2 Systemd 管理而不是裸跑 Docker常驻服务最好交给 Systemd 托管。下面是 unit 文件要点[Unit] DescriptionCloddsBot runtime Afterdocker.service Requiresdocker.service [Service] WorkingDirectory/opt/cloddsbot ExecStartdocker compose up ExecStopdocker compose down Restartalways RestartSec10这样开机自启、崩溃重启、日志统一 journalctl 管理。日志轮转方面可以加一条 logrotate 配置对 /var/log/cloddsbot 目录按天切割并保留 30 天。主程序内部我用 logging 模块同时输出到 stdout 和文件stdout 走 journald文件走 logrotate两边都不耽误。4.3 实测运行时参数与调度频率运行三个月后的经验是主程序常驻内存约 120MB每次巡检耗时在几秒到十几秒之间主要取决于账号数量和实例规模。调度频率建议是一个折中方案——实例状态检查每 5 分钟一次证书到期检查每天一次账单异常检查每天一次账单数据本身 T1 才更新查太频繁没有意义。个人经验是把“实时性要求高”的状态检查和“低频但重要”的到期检查分开配置而不是一刀切全按 5 分钟跑。检查项推荐频率理由实例状态巡检每 5 分钟宕机发现越早越好但没必要秒级SSL 证书到期每天 09:00证书状态变化频率低T1 足够费用异常每天 10:00账单 T1 更新查太早拿不到数据日志关键词扫描每 15 分钟依赖日志落盘延迟过密无意义5. 运行三个月后回头看那些坑和对应的解法5.1 最开始的告警风暴一次误报带来几十条推送第一个版本没有告警抑制某天凌晨区域网络抖动我同时收到几十条“实例状态异常”的钉钉消息。那次之后我做了两个改动一是引入上文提到的 window 去重二是对同一批次异常做聚合。还有一个心态层面的收获告警推送的“权威性”很宝贵如果群里经常刷无意义告警团队成员会逐渐无视所有通知那才是最大的失败。排查链路值得记录一下当时我先去看采集日志发现所有不可达实例都集中在同一区域而且时间点完全一致然后单独调用一次该区域查询接口发现返回正常再翻外层网络监控发现是机房出口抖动。整个过程其实不到十分钟但因为消息刷屏群里所有人都被惊动了。从那以后凡是不涉及具体资源 ID 的异常我都默认先聚合再推送。5.2 云厂商 SDK 的分页参数并不一致这一点在跨厂商接入的时候非常坑。阿里云用 PageNumber/PageSize腾讯云部分接口也用类似风格但 AWS 风格接口用的是 NextToken 游标。写统一 Provider 接口时建议把“分页遍历”封装成生成器上层调用方只负责一个个拿资源不感知底层差异。def iter_instances(provider, account, region): for raw in provider.list_instances(account, region): yield normalize_instance(raw)当时我踩的坑是某个厂商的 SDK 默认每页只返回 20 条而它的 SDK 文档里关于分页的说明藏得很深。第一周巡检出 25 台实例只推了 20 台剩下 5 台的状态完全没有被检查到。这个问题后来是通过一个“资源总数自检”发现的——拉取列表后把归一化后的实例数和厂商返回的 TotalCount 做比对不一致就直接告警“采集可能不完整”。这个自检逻辑强烈建议保留。5.3 时区问题永远是定时任务的隐形杀手我最初把所有调度表达式都写成本地时间后来添加了海外区域资源后发现告警里的时间戳对不上。最终统一约定程序内部所有时间存储使用 UTC仅在与用户交互展示时转换成本地时间APScheduler 的 cron 表达式全部显式指定 timezoneAsia/Shanghai。不要在代码里隐式依赖系统时区否则部署到不同环境很容易出现“凌晨三点没跑巡检”这种诡异问题。还有个小坑SQLite 的 datetime(now) 返回的是 UTC 时间如果插入时不注意查出来的时间会和你本地时间差 8 小时。所以我在表里存时间统一用 ISO 8601 带时区后缀或者干脆存 UTC 并在查询时显式转换。5.4 SSL 证书检测的翻车经历实现证书到期提醒时我最初只检查了证书的 notAfter 字段结果一条生产环境的证书在到期前一周就被我判成“正常”。原因是该证书链里的中间证书先过期了实际握手已经失败。所以证书检查不能只看叶子证书的到期日期有条件的话应该直接用 openssl 做一次真实握手来验证。echo | openssl s_client -servername api.example.com -connect api.example.com:443 2/dev/null | openssl x509 -noout -dates这也是“状态巡检类工具”最容易犯的错误以为字段齐了就是对的其实数据真实含义还得往下一层看。证书到期时间字段存在不代表证书链完整磁盘使用率字段存在不代表云盘没有进入只读状态。规则引擎只能帮你发现你定义过的问题定义问题本身需要你对底层机制有足够的了解。5.5 机器人 Webhook 地址泄露后的补救自定义机器人 Webhook 如果泄露任何人都能往你的群里推消息。钉钉后台可以重置密钥但更稳妥的做法是把 Webhook 和密钥放到环境变量里统一管理在配置中不要写明文同时给推送接口设置一个“每日消息数上限”异常流量直接被熔断防止群被刷屏。MAX_DAILY_MESSAGES 200 def can_send_today(): count db.execute( SELECT count(*) FROM notifications WHERE date(created_at) date(now) ).fetchone()[0] return count MAX_DAILY_MESSAGES其实这个限制还有一个额外收益它会逼着你优化告警质量。消息配额有限你自然会去合并同类项、去除低级别噪音最后群里剩下的都是真正值得看的。6. 这个项目接下来我打算怎么演进CloddsBot 目前的状态已经能覆盖我绝大部分日常巡检需求但后续还有几个明确想做的方向。现阶段每加一个新能力都遵循同样的套路先想清楚数据从哪来再想清楚异常怎么定义最后才写代码。6.1 费用异常检测云厂商账单接口支持拉取按产品线分组的日账单。思路是在账单异常规则里加“对比近 30 天均值”的判定比如某产品线当天费用超过前 30 天日均费用的 3 倍时告警。这类场景用规则引擎的框架同样能覆盖只是数据源从实例列表换成了账单接口。费用异常检测有一个特殊点账单数据是 T1 的所以不需要高频巡检但需要保留足够长的历史数据做基线。Sqlite 直接存 30 天账单原始值用 window 函数取均值即可不需要额外引入时序数据库。6.2 多账号操作审批流现阶段 cloddsbotctl 只有本机审计没有审批。如果后面加入“推送消息 → 指定负责人确认 → 确认后执行”的流程就需要一个能暴露给 IM 回调的轻量服务。我的初步想法是做一个只监听内网 HTTP 的确认服务配合企业自建应用的回调能力可以大幅降低误操作风险。审批流的设计原则是“最小暴露面”确认服务不挂在公网只在内网监听每个确认请求带一次性 token30 秒过期操作执行前后各写一条审计。这种内部工具不追求功能花哨稳定和可追溯永远排在第一位。6.3 巡检报告周报把每周的事件和告警做汇总生成一份 Markdown 周报定时推到群里。内容包括异常类型分布、处理耗时、当周新增资源数。这种周报对个人开发者可能无所谓对小团队来说是很好的透明化手段。周报模板大致长这样## 本周巡检报告06.01 - 06.07 本周共巡检 8 个账号、137 台实例发现 23 个异常事件。 | 异常级别 | 数量 | 已处理 | | --- | --- | --- | | critical | 2 | 2 | | warning | 21 | 19 | 异常类型分布 - instance_down1 - disk_usage_high3 - cert_expire_soon17 - cost_anomaly2如果你也正被“每天登录控制台巡检”这种低效习惯困扰不妨照着上面的思路拆一个自己的版本。核心逻辑其实就三步拉数据、定规则、推消息。真正花时间的从来不是代码而是想清楚“什么才算异常”以及“推给谁看最有价值”。