概述
周五下午四点半,业务群开始刷屏:
“@运维 帮我重启一下订单服务” “@运维 线上这个接口报错了,帮我捞下日志” “@运维 机器是不是又满了?帮我清下磁盘”
三个请求,三个 @,运维同学放下手里正在写的巡检脚本,SSH 登录、敲命令、回群里贴截图、再被追问一句“好了吗”。一个下午就这么碎了。这不是个例,是大多数团队运维工作的常态——高频、低难度、高度依赖人肉。
我在自研运维平台项目里做过一次统计:把 20 多个运维操作(服务重启、缓存清理、配置下发、证书续期、日志捞取、磁盘扩容……)从“群里 @ 人”搬进自助平台之后,单次操作的平均耗时从 15 分钟降到 90 秒左右,运维团队花在“响应请求”上的人力降了约 40%。这两个数字后面没有魔法,就是一件事:把操作变成平台上的标准动作。
这篇文章把我做这件事的完整思路摊开讲:为什么要平台化(不是“提效”这种空话,而是四个具体的结构性问题)、6 层架构怎么设计、操作目录怎么标准化、审批流怎么做才不会被绕过,以及四个真实踩过的坑。代码和配置都是可以直接抄走的。
先说清楚一点:运维操作平台化不是“写个 Web 界面跑脚本”。如果你只是在 Web 页面上开了个终端让开发自己敲命令,那不叫平台化,叫换个地方人肉。平台化的核心是:操作本身被定义成结构化的资产——有参数、有权限、有审批、有回滚、有审计。Web 界面只是这个体系的门面。
手工运维的四个结构性问题
在讲架构之前,先把问题说透。很多团队知道手工运维“慢”,但慢只是表象。真正让运维团队疲于奔命的是四个结构性问题:
问题一:请求链路太长,人的时间被切碎
一次“帮我重启服务”的完整链路是这样的:
业务在群里 @ 运维(等待响应:5 分钟)
→ 运维确认影响范围(翻 CMDB / 问业务:5 分钟)
→ 运维 SSH 登录执行(1 分钟)
→ 运维回群确认 + 贴执行结果(2 分钟)
→ 业务追问验证(3 分钟)
单次 15 分钟,其中真正“执行操作”只有 1 分钟,剩下 14 分钟都是沟通成本。Red Hat 在一篇 AIOps 实践文章里给过一个真实数据:一家英国金融公司用 Ansible Automation Platform 每周跑 600 多个任务(装机、补丁、合规扫描、配置漂移修复),每周产生约 40 张失败工单——如果没有平台,这 40 张工单每一张都是一次“群里 @ 人”。任务量到这个级别,人肉响应模式必然崩掉。
问题二:审计黑洞
手工操作最大的风险不是慢,是说不清。出了故障复盘时,这些问题的答案往往是“不知道”:
- 昨晚 23 点是谁登录了那台机器?
- 他执行了什么命令?成功了几条?
- 改的那个配置文件,改之前是什么样?
SSH 直连的操作,审计只能依赖机器上的 ~/.bash_history(还可能被清掉)和堡垒机的录像(如果有的话)。等保 2.0 审计要求操作留痕、可回溯,人肉模式基本过不了关。我在做等保合规项目时深有体会:审计人员问的第一个问题永远是“操作日志在哪”,人肉运维在这个问题面前没有答案。
问题三:知识断层
“重启订单服务找老张,清缓存找老李”——操作知识长在具体的人身上,而不是长在系统里。老张休假,订单服务就没人敢动。每个操作依赖特定的人、特定的路径、特定的顺序,没有文档,或者有文档但和实际操作已经对不上。
问题四:风险不可控
手工执行没有“防呆”。复制粘贴错一个环境、rm -rf 少打一个点、在测试环境验证过的命令直接拿到生产跑——这些都真实发生过。Google SRE Book 里对这类重复性手工操作有个专门的概念叫 toil(琐事):手工、重复、可自动化、无长期价值、与服务规模成线性增长。琐事拖慢效率只是表层,真正的代价是风险——人在重复操作中的疲劳和麻痹,本身就是故障源(这个概念的完整展开可以看 相关文章:消除琐事:SRE 的事务性工作治理)。
四个问题汇总对比:
| 问题 | 人肉模式 | 平台模式 |
|---|---|---|
| 响应链路 | 群里 @ 人,单次 15 分钟,沟通占 90% | 自助发起,单次 90 秒,无人值守 |
| 审计留痕 | 依赖 history 和记忆,复盘靠猜 | 全量操作日志 + 变更前后快照 |
| 知识留存 | 长在人身上,人员离职即断层 | 操作目录即文档,新人照单执行 |
| 风险控制 | 靠自觉和细心,无防呆机制 | 权限拦截 + 参数校验 + 高危复核 |
运维操作平台化到底是什么
用一句大白话说清楚:运维操作平台化,就是把“谁、在什么机器上、执行什么操作、依据什么参数”这件事,从人与人的口头沟通,变成系统与系统之间的结构化契约。
拆开看四个要素:
- 谁——操作发起者要有身份,权限体系判断他能不能做
- 在什么机器上——操作对象是受管的资源,不是随便一个 IP
- 执行什么操作——操作本身是预定义的标准动作,不是临时敲的命令
- 依据什么参数——参数有类型、有校验、有默认值,危险值直接拦截
这里有个容易混淆的边界要说清楚:自动化脚本解决的是“怎么执行”,平台解决的是“谁、何时、能否、可否回滚、如何审计”。你用 Ansible 把重启服务写成 playbook,这是自动化;把 playbook 挂到平台上,让业务方自己填参数、点按钮、等审批、看结果、留审计,这才是平台化。两者是递进关系,不是替代关系。
演进路径通常是这样的:
阶段 0:纯手工(SSH + 记忆)
↓ 脚本化:把常用操作写成脚本(解决"怎么执行")
阶段 1:脚本仓库
↓ 定时化:cron / systemd timer 跑固定任务(解决"何时执行")
阶段 2:定时任务
↓ Web 化:脚本暴露 Web 界面(解决"谁可以执行")
阶段 3:运维 Web 化
↓ 平台化:加权限、审批、审计、回滚(解决"能否、可否回滚、如何审计")
阶段 4:自助化:业务方自助发起,运维只做审批和兜底
很多团队卡在阶段 2 到 3 之间:脚本写了一堆,但执行入口还是“运维帮忙跑一下”。原因通常不是技术,是没人把权限、审批、审计这套治理体系建起来。下面的架构就是解决这个问题的。
六层架构设计
先看全景图,再逐层拆解:
┌─────────────────────────────────────────────────────┐
│ L1 入口层 Web 控制台 │ ChatOps │ OpenAPI │ 定时触发 │
├─────────────────────────────────────────────────────┤
│ L2 治理层 权限模型(RBAC) │ 审批工作流 │ 高危双人复核 │
├─────────────────────────────────────────────────────┤
│ L3 操作目录 Operation Catalog(结构化操作定义) │
├─────────────────────────────────────────────────────┤
│ L4 编排引擎 DAG 依赖编排 │ 并发控制 │ 失败重试 │
├─────────────────────────────────────────────────────┤
│ L5 执行通道 Ansible │ 原生 SSH │ K8s API │ 云厂商 API │
├─────────────────────────────────────────────────────┤
│ L6 审计层 操作日志 │ 终端录像 │ 配置快照 │ 回滚预案 │
└─────────────────────────────────────────────────────┘
L1 入口层:操作从哪里来
入口层回答“用户从哪里发起操作”。三个入口,重要性不一样:
- Web 控制台(必须):完整的操作目录浏览、参数表单、审批流转、执行历史。这是主入口。
- ChatOps(推荐):在 IM 里输入
/restart order-service --env prod直接发起。降低使用门槛的关键——业务方不想为了重启个服务再开一个网页。IM 的回调机器人把命令转发给平台 API,复用同一套权限和审批逻辑。 - OpenAPI(进阶):供 CI/CD 流水线和其他系统调用。比如发布系统在部署完成后自动调用“预热缓存”操作。
我的建议是入口可以多样,但必须收敛到同一套权限和审计逻辑。见过反面案例:ChatOps 机器人为了“快速响应”绕过了审批直接执行,出了事审计里只有一条 IM 消息记录,什么都查不到。
L2 治理层:谁能做什么
这是平台化的灵魂,后面单独用一章讲权限模型和审批流。
L3 操作目录:平台的核心资产
操作目录(Operation Catalog)是所有被平台化的操作的注册中心。每个操作是一份结构化定义:名称、描述、参数 schema、执行体、风险等级、回滚方式。这一层是整个平台的核心资产——操作目录的质量直接决定平台的生命力,后面详细展开。
L4 编排引擎:多步骤怎么办
单步操作(重启一个服务)直接执行就行,但真实世界的操作经常是多步骤的:扩容 = 创建新实例 + 挂载负载均衡 + 健康检查 + 通知。这需要一个编排引擎,支持:
- 顺序依赖:步骤 B 依赖步骤 A 的输出
- 并行分支:无依赖的步骤并发执行,缩短总耗时
- 条件跳转:检查通过走 A 分支,不通过走 B 分支
- 失败策略:单步失败是终止、重试还是跳过
编排引擎可以直接用 DAG(有向无环图)模型实现。关于 DAG 调度引擎的完整设计(拓扑排序、状态机、并发控制),我在这篇里展开过:相关文章:用 Go 自研 DAG 调度引擎的架构决策,这里不重复。
一个多步骤操作的编排定义长这样(以“扩容并接入负载”为例):
# workflow-scale-out.yaml
# 编排定义:横向扩容一个服务实例并接入流量
apiVersion: ops/v1
kind: Workflow
metadata:
name: scale-out-service
spec:
steps:
- name: provision # 步骤一:创建新实例
operation: host-create
params: {image: prod-base, spec: 4c8g}
outputs: [instance_ip] # 声明输出,后续步骤引用
- name: deploy # 步骤二:部署服务(依赖新实例)
operation: service-deploy
params:
host: "${steps.provision.outputs.instance_ip}" # 引用上一步输出
retry: {maxAttempts: 2, backoff: 30s} # 部署偶发失败可重试
- name: health-check # 步骤三:健康检查(并行不了,必须等部署)
operation: http-check
params: {path: /healthz, expectCode: 200}
onFailure: abort # 健康检查失败:终止,不接入流量
- name: attach-lb # 步骤四:挂负载均衡(先隔离后接入,先改权重)
operation: lb-backend-add
params: {weight: 10} # 灰度:先给 10% 流量
needs: [health-check]
两个设计取舍值得说:
- 失败策略要显式声明:
deploy步骤可以自动重试(部署脚本幂等),但health-check失败必须abort——把一个不健康的实例挂进 LB,等于主动制造故障。哪些步骤可重试、哪些必须立刻停,是编排定义的一部分,不能靠默认值 - 灰度接入写进编排而不是靠人记得:新实例挂 LB 时权重从 10 开始,观察无异常再提到全量。这个动作手工操作时靠的是老工程师的肌肉记忆,平台化之后写死在编排定义里,新人发起的扩容和老手发起的效果一致
L5 执行通道:真正干活的地方
执行通道是“手”和“脚”。按操作对象类型分四类:
| 通道类型 | 适用对象 | 典型工具 |
|---|---|---|
| Ansible | 传统主机、批量操作 | ansible-playbook |
| 原生 SSH | 单机快速操作 | golang.org/x/crypto/ssh |
| K8s API | 容器化工作负载 | client-go |
| 云厂商 API | 云资源 | aliyun-sdk / tencentcloud-sdk |
我的推荐:主机类操作统一走 Ansible,不要自己维护 SSH 连接池。理由很实际:Ansible 的 Inventory 管理、批量并发、模块生态(copy、service、systemd)都是现成的,自己造这个轮子要处理连接复用、超时控制、输出解析一堆脏活。Go 自研平台可以直接用 github.com/apenella/go-ansible 库在代码里调 playbook,两全其美。
Go 平台里调用 Ansible 的核心代码(简化版,生产可用的骨架):
// ExecuteOperation 平台执行入口:把操作定义翻译成 Ansible 调用
func ExecuteOperation(op *Operation, params map[string]string) (*Result, error) {
// 1. 参数二次校验(不信任任何前置层,执行前最后一道防线)
if err := op.ValidateParams(params); err != nil {
return nil, fmt.Errorf("参数校验失败: %w", err)
}
// 2. 组装 Ansible 执行器,动态传参(extra-vars)
ansible := goansible.NewAnsiblePlaybookExecutor(
op.Execution.Playbook, // playbook 路径来自操作目录定义
goansible.WithExtraVars(params),
goansible.WithTimeout(op.Execution.Timeout),
)
// 3. 执行并捕获结构化输出
stdout, stderr, err := ansible.Execute(context.Background())
result := &Result{
OperationID: op.Metadata.Name,
Params: params, // 参数快照进审计
Stdout: stdout, // 完整输出落盘,供审计回放
FinishedAt: time.Now(),
}
if err != nil {
result.Status = StatusFailed
result.Error = stderr
} else {
result.Status = StatusSuccess
}
return result, nil
}
注意第 1 步的“二次校验”:Web 表单校验过一遍,执行前还要校验一遍。两层校验的意义在于防御中间环节的疏漏——比如审批期间操作定义更新了参数约束,或者 API 调用方绕过了表单。执行层是最后一道防线,它对“参数一定合法”的假设永远保持怀疑。
原生 SSH 通道只保留一个用途:在线终端(给用户兜底的手工登录入口),而且必须挂堡垒机录像。
L6 审计层:出事时能说得清
审计层记录四类数据:
- 操作日志:谁、何时、什么操作、什么参数、结果如何
- 终端录像:Web 终端和 SSH 会话的完整回放(用 ascinema 录制 tty 输出)
- 配置快照:变更类操作执行前后的配置对比(diff)
- 回滚预案:每个高危操作预置的回滚动作
审计的查询入口要给到故障复盘场景:按主机、按时间窗、按操作人三个维度过滤。只记不查的审计等于没记——这是我在踩坑章节会展开的血泪教训。
操作目录:把操作变成结构化资产
这一章是全文的重心。操作目录的设计,我用一份实际的 YAML 定义来说明(这是我们平台里真实使用的格式,简化后呈现):
# operation-restart-service.yaml
# 操作目录中的一条记录:定义"重启服务"这个操作
apiVersion: ops/v1
kind: Operation
metadata:
name: restart-service # 操作唯一标识,代码引用用这个名字
displayName: "重启指定服务"
category: service # 分类:service / host / config / cert...
riskLevel: medium # 风险等级:low / medium / high
owner: sre-team # 操作负责人(审批默认路由到这)
spec:
description: |
重启指定环境中的指定服务。
仅支持滚动重启,会自动等待健康检查通过。
params: # 参数定义:表单自动渲染 + 后端校验
- name: service
type: enum
required: true
options: [order-service, pay-service, user-service]
description: "要重启的服务名"
- name: env
type: enum
required: true
options: [test, staging, prod]
description: "目标环境"
- name: reason
type: string
required: true
maxLength: 200
description: "操作原因,会写入审计日志"
execution:
type: ansible # 执行通道:ansible / ssh / k8s / cloud
playbook: playbooks/service/restart.yaml
timeout: 300 # 超时秒数,防止任务挂死
approval:
prod: [owner-confirm] # 生产环境需要操作负责人确认
staging: [] # 预发免审批
test: [] # 测试免审批
rollback: # 回滚预案:重启类操作幂等,回滚即重试
type: re-execute
这份定义里有几个关键设计,每一个都是踩过坑之后才加上的:
设计一:参数必须是枚举优先,拒绝自由文本。service 参数用 enum 而不是 string,用户只能从下拉框选,不可能拼错服务名。自由文本参数是事故之源——真实案例:环境参数本该填 prod,业务方手滑填了 pord,脚本在错误判断下走了异常分支。能用枚举的绝不用字符串,能用下拉的绝不手输。
设计二:reason 强制填写且写入审计。这个字段看起来多余,但在复盘时价值巨大——“为什么昨晚 23 点有人重启了支付服务”,答案就写在操作记录里,不用去翻聊天记录。
设计三:审批策略按环境分级。同一个操作,测试环境免审批(鼓励自助),生产环境要确认(守住底线)。分级思路是平台化的通行原则,一刀切必翻车。
设计四:riskLevel 决定审批强度。见下一章权限模型的展开。
再看一个高危操作的完整定义,重点看双人复核和回滚预案的部分:
# operation-disk-expand.yaml
# 高危操作示例:生产环境磁盘扩容
# 风险点在于涉及数据盘,操作失败可能导致文件系统损坏
apiVersion: ops/v1
kind: Operation
metadata:
name: disk-expand
displayName: "扩容数据盘"
category: host
riskLevel: high # 高危:触发双人复核 + 变更窗口检查
owner: sre-team
spec:
description: |
扩容指定主机的数据盘并扩展文件系统。
仅支持在线扩容(LVM 场景),缩容不支持。
params:
- name: host
type: enum
required: true
options: [db-01, db-02, cache-01, cache-02]
description: "目标主机(仅限白名单)"
- name: device
type: enum
required: true
options: [/dev/vdb, /dev/vdc]
description: "目标磁盘设备"
- name: sizeGB
type: integer
required: true
min: 100 # 参数范围校验:防止误填 10GB 或 100000GB
max: 2000
description: "扩容后的目标容量(GB)"
execution:
type: ansible
playbook: playbooks/host/disk-expand.yaml
timeout: 600
idempotent: true # 扩容到目标值天然幂等:已是目标容量则 no-op
approval:
prod:
- owner-confirm # 第一道:操作 owner 确认
- double-review # 第二道:双人复核码确认
- change-window # 第三道:必须在变更窗口内
test: [owner-confirm]
rollback: # 回滚预案:高危操作必填
type: manual # 磁盘缩容不安全,只能人工评估
runbook: runbooks/disk-expand-rollback.md
和前面中危的重启操作对比,高危定义多出来的三样东西:
double-review审批节点:第二个人的确认码校验(实现细节见下一章)change-window约束:只允许在变更窗口(比如工作日 22:00 后)发起,窗口外提交直接拒绝并提示原因rollback.runbook:回滚预案不是“重跑一遍”,而是一份人工处置手册的链接——磁盘这种数据安全攸关的操作,自动化回滚的风险比正向操作更大,承认这一点比硬造一个自动回滚更负责任
幂等性:操作可重复执行的前提
操作目录里的每个操作,必须回答一个问题:执行两次和执行一次,结果一样吗? 这就是幂等性。它是平台化操作和手工脚本最大的分水岭。
为什么这么重要?因为平台环境下,操作会被重试:网络抖动后前端重发、编排引擎的失败重试、用户看没反应又点了一次按钮。如果操作不幂等,每一次重试都是一次事故。
反例(非幂等):
# 错误示范:清理缓存目录的脚本
# 执行两次时,第二次会把新产生的缓存数据也删掉
rm -rf /data/cache/*
echo "cache cleared"
正例(幂等):
# 正确示范:只删除 7 天前的缓存文件
# 执行多次,结果一致(老文件已被删,重跑删不到东西)
find /data/cache -type f -mtime +7 -delete
echo "cache cleared, removed files older than 7d"
Ansible 的多数模块天然幂等(service 模块对已停止的服务执行 start,第二次执行 no-op),这也是我推荐 Ansile 作为主通道的原因之一。自己写脚本时,幂等性是 code review 的必查项。
权限模型与审批流:治理层怎么做才不被绕过
这一章讲 L2 治理层。先给结论,再展开论证:
审批流的设计目标不是“卡住操作”,而是“让合规的操作比绕过平台更快”。
这句话是我踩完坑总结的。第一版平台我把审批做得非常严格:所有生产操作需要“发起人 → 运维负责人 → 技术总监”三级审批。结果三个月后我从堡垒机日志里发现,业务方绕过平台直接 SSH 的操作量反而上升了。审批太重的平台会被人肉通道“用脚投票”。
权限模型:三个维度的矩阵
RBAC 在运维平台里要落成三维矩阵:用户 × 操作 × 环境。
# 权限策略示例:订单团队的开发同学的权限
# 读作"谁可以对什么环境的什么操作做什么"
apiVersion: ops/v1
kind: AccessPolicy
metadata:
name: order-team-dev
subjects: # 谁被授权
- group: order-team-dev # 订单团队开发组
operations: # 对哪些操作
- restart-service # 重启服务
- tail-service-log # 捞日志
- clean-cache # 清缓存
environments: # 在哪些环境
- test
- staging
effect: allow # allow / deny
这套模型里,环境维度是关键。多数团队的权限问题不是“能不能执行这个操作”,而是“能在生产执行吗”。把环境作为一等公民放进权限矩阵,测试环境放开自助、生产环境收紧审批,既保效率又控风险。
操作分级:不同风险,不同强度
不是所有操作生而平等。按风险分三级,对应三种管控强度:
| 风险等级 | 操作特征 | 典型操作 | 管控方式 |
|---|---|---|---|
| 低危 | 只读、可逆、影响面小 | 捞日志、查状态、看配置 | 免审批,全自助 |
| 中危 | 可逆、影响单服务 | 重启服务、清缓存、扩容 | 一级审批(操作 owner) |
| 高危 | 不可逆、影响全局、涉数据 | 删数据、改内核参数、证书替换、网络变更 | 双人复核 + 变更窗口 |
高危操作的双人复核要动真格:发起人提交后,系统生成一个确认码,第二个人必须在页面上输入确认码才能放行。确认码不是验证码(机器能识别的那种),而是一串需要人工念给对方听的码——强制两个人之间发生一次真实的沟通。听起来繁琐,但删数据这种事,多一道 30 秒的沟通,救过我们至少一次。
复核码的实现有几个讲究,直接上核心逻辑:
// IssueReviewToken 为高危操作生成双人复核码
// 设计要点:短时效 + 操作绑定 + 单次有效
func IssueReviewToken(opExec *OperationExecution) (string, error) {
// 确认码里绑定了操作 ID 和参数摘要——
// 复核人念码时,发起人屏幕上显示的是同一个操作和同一份参数
// 防止"发起人提交 A 操作、复核人批准的却是 B 操作"的时序错位
payload := fmt.Sprintf("%s|%s|%d",
opExec.OperationName, opExec.ParamsDigest(), opExec.ID)
token := hotp.Generate(payload, 6) // 6 位数字,方便口头念
// 10 分钟有效,超时作废;用过即焚,同操作不能复用
store.SetWithTTL("review:"+payload, token, 10*time.Minute)
return token, nil
}
// VerifyReviewToken 第二个人输入复核码放行
func VerifyReviewToken(opExec *OperationExecution, input string) error {
payload := fmt.Sprintf("%s|%s|%d",
opExec.OperationName, opExec.ParamsDigest(), opExec.ID)
stored, ok := store.GetAndDelete("review:" + payload) // 取出即删
if !ok {
return errors.New("复核码已过期,请发起人重新提交")
}
if stored != input {
return errors.New("复核码不匹配")
}
return nil
}
两个细节值得多看一眼:
- 复核码绑定参数摘要。如果发起人在复核过程中修改了参数(比如磁盘扩容从 500GB 改成 800GB),旧复核码自动失效,必须重新走复核。参数和确认动作绑死,杜绝“确认的是 A、执行的是 B”
- 取出即删。同一个复核码不能用于两次执行——否则第二次高危操作就绕过了第二个人的确认
这套机制的成本是每次高危操作多 30 秒的沟通,收益是把“我以为是他在操作”这类最扯皮的故障场景从机制上消灭了。
审批流的工程实现要点
审批工作流本身不复杂(状态机:pending → approved/rejected → executing → done/failed),但三个工程细节决定成败:
细节一:审批要能异步触达。审批请求推送到审批人的 IM,附带操作摘要(谁、什么操作、什么环境、什么原因),一键批准或拒绝。审批人不用登录平台找红点。我们接 IM 机器人后,审批平均响应时间从 2 小时缩到 8 分钟。
细节二:审批要有超时升级。审批人 30 分钟不响应,自动升级到上级或备用审批人。否则“审批人休假”就成了大家绕开平台的现成借口。
细节三:每个审批动作本身要进审计。谁批准的、什么时候批准的、批准时的操作参数快照。出了问题,责任链清晰。
ChatOps 入口:让操作“随手发起”
审批流搭好之后,下一个问题是入口触达。Web 控制台是主入口,但真实场景里业务方不想为了一次重启去开浏览器、找目录、填表单。ChatOps 不是花架子,是平台能不能真正被用起来的关键。
IM 机器人把命令转发给平台,核心是“命令解析 → 权限复用 → 审计统一”三件事走同一套逻辑。命令格式约定要克制,不要设计成一门脚本语言:
/restart order-service --env prod --reason "内存泄漏疑似复现"
解析伪代码:
// parseChatOpsCommand 解析 IM 机器人命令,复用 Web 入口的权限和审计
// 格式:/操作名 参数 --flag value
func parseChatOpsCommand(text string) (*OperationRequest, error) {
fields := strings.Fields(text)
opName := strings.TrimPrefix(fields[0], "/") // 去掉 / 前缀,得到操作名
op, err := catalog.Get(opName) // 从操作目录查找定义
if err != nil {
return nil, fmt.Errorf("未找到操作 %q,可用列表用 /list 查看", opName)
}
req := &OperationRequest{Operation: opName}
// 剩余字段按操作定义的 param schema 解析(枚举校验、范围校验)
if err := op.BindParams(fields[1:], req); err != nil {
return nil, err // 参数校验失败直接返回,不进审批流
}
return req, nil
}
三个工程要点:
- 操作名和参数 schema 直接复用操作目录,不在 ChatOps 里重新定义一套。
/restart解析出来的请求和 Web 表单提交的请求是同一个OperationRequest结构,后续权限校验、审批流、审计走完全一样的代码路径 - 权限复用:IM 账号映射到平台用户(绑定关系在用户首次使用时建立),RBAC 判断和 Web 端一致。不存在“IM 里能用、Web 上不能用”的权限裂口
- 审计统一:ChatOps 发起的操作,审计记录里发起入口标记为
chatops,和 Web 发起的web记录在同一张表里查询
我见过反面案例:某团队的 IM 机器人为了响应快,直接执行命令、跳过审批,审计只有一条 IM 消息。后来某个环境配置被改坏,复盘时谁也说不清改之前是什么样。这是“入口多样但治理不统一”的典型翻车——入口可以多,治理逻辑只能有一套。
落地路径:四个阶段,每一步有验收标准
平台化不能一口气吃成胖子。我的落地经验是四个阶段,每个阶段有明确的验收标准,不达标不进下一阶段。
阶段一:脚本标准化(打地基)
把团队现有的高频脚本收拢,统一规范。验收标准:
- Top 10 高频操作全部脚本化,每份脚本有:参数说明、幂等性保证、明确的退出码、结构化日志输出
- 脚本统一进 Git 仓库,禁止散落在个人目录
- 每份脚本在测试环境跑通过至少一次
这个阶段没有平台,只有规范。看起来不起眼,但没有标准化的脚本,后面的一切都是空中楼阁——垃圾脚本平台化之后就是自动化地产生垃圾。
常见卡点:“没时间整理脚本”。处理办法是从工单数据里拉 Top 10 高频请求,只标准化这十个——被 @ 得最多的操作,就是最值得先平台化的操作。不要试图一次收拢全部脚本,收拢不完的存量让它在使用中逐步淘汰。
阶段二:操作目录化(选 Top 10 上平台)
把 Top 10 操作按操作目录的格式定义出来,挂上 Web 界面。验收标准:
- 10 个操作可以从 Web 发起并正确执行
- 每个操作的参数有表单校验
- 执行历史可查(此时审计可以简陋,但不能没有)
这个阶段的目标是让团队尝到甜头。捞日志从“@ 运维等 15 分钟”变成“自己点 30 秒”,用户的正反馈会推动平台化往前走。
常见卡点:操作定义“过度设计”。一开始就想把参数做得万能(支持正则、支持表达式),结果表单复杂到没人会用。原则是先把最常用的 80% 场景做成傻瓜式下拉,剩下 20% 的长尾需求让用户提工单——过度灵活的入口等于没入口。
阶段三:权限与审批接入(立规矩)
接入 RBAC 和审批流,高危操作双人复核上线。验收标准:
- 所有操作有权限控制,越权操作被拦截并有日志
- 生产环境操作 100% 走审批流
- 审计日志覆盖全部操作,可按人、时间、主机检索
这个阶段阻力最大——业务方会抱怨“以前 @ 一下就干了,现在还要审批”。应对办法是数据说话:把“审批耗时”和“以前群里 @ 人等响应的耗时”放在一起比。我们实测一级审批平均 8 分钟,而以前群里 @ 运维平均响应 15 分钟起。审批没有变慢,反而把响应时间变成 SLA 化的承诺。
阶段四:自助化开放(收果实)
把测试环境的多数操作开放给业务方自助,运维只保留生产和高危的审批职责。验收标准:
- 测试环境操作 80% 以上由业务自助完成
- 运维团队的“响应请求”类工单量下降 50% 以上
- 平台月活用户覆盖 60% 以上的研发人员
到这一步,“群里 @ 运维”的日常才真正变成“群里讨论架构”的日常。
常见卡点:自助化范围失控。开放自助的热情上来后,容易把不该开放的操作也放出去(比如直接在生产执行任意 SQL 的入口)。红线清单要提前定好:涉数据的写操作、跨全局的网络变更、不可逆的删除动作,永远不进自助区,无论业务方怎么抱怨。
平台自己不能成为单点:高可用与降级设计
平台化的一个隐藏风险容易被忽略:运维平台本身挂了,所有操作停摆,运维团队会瞬间退化到比“人肉运维”更糟的境地——因为流程已经习惯依赖平台,人肉通道反而生疏了。平台自身的高可用不是可选项,是平台化的组成部分。
三层防线:
第一层:执行层无状态化。执行节点(跑 Ansible 的机器)水平扩展至少两台。Ansible 控制机本身无状态(Inventory 和 playbook 放共享存储或随节点分发),平台侧做健康检查、故障摘除就行。执行层是最容易扩展的一层,没有理由单点。
第二层:控制面主从。Web 界面和 API 服务双实例,数据库主从(操作记录丢一条都是审计损失,数据库的高可用不能省)。控制面挂一半不影响正在执行的操作——执行中的任务在执行节点上继续跑完,结果落库延迟写入。
第三层:降级预案。平台全挂时,降级为“带审批的手工操作”:审批走 IM 群(值班经理口头批),操作走堡垒机(保底录像),事后 24 小时内补录审计。这份预案要写进值班手册并且每季度演练一次——没演练过的降级预案等于不存在,真出事时没人记得流程、没人有权限、没人敢操作。
我们踩过这个坑才补的这层设计(细节在踩坑实录的坑三),成本不高:两台执行节点加一套主从数据库,相对整个平台化的投入,不到 10%。
生产环境踩坑实录
四个真实的坑,每一个都付过学费。
坑一:审批流太重,用户用脚投票绕过平台
前面提过,展开讲讲。第一版平台上线时,我设计了三级审批(发起人 → 运维负责人 → 技术总监),初衷是安全第一。三个月后做平台使用数据复盘,发现两个刺眼的数据:平台月活 40 人(研发团队 200 人),而堡垒机的 SSH 登录量不降反升。
去和业务方聊,反馈很直接:“重启个测试环境的服务,等三个领导批,黄花菜都凉了。我还不如直接 SSH。”
解法是分级 + 提速:
- 测试环境操作全部免审批(自助)
- 生产中低危操作一级审批(操作 owner,IM 一键批)
- 只有高危操作保留双人复核
- 审批超时 30 分钟自动升级,不再卡在人身上
调整后一个月,平台月活从 40 涨到 130。治理的强度要匹配风险,而不是匹配想象中的恐惧。
坑二:非幂等脚本,重试变事故
清缓存操作最早版本的脚本是 rm -rf /data/cache/*。某天执行时网络抖动,平台前端没收到响应,业务方刷新页面又点了一次。两次执行间隔 40 秒,第二次执行时,第一次清完后新产生的缓存数据(包括一批正在写入中的会话文件)被一并删掉,引发了 10 分钟的登录异常。
修复分三层:
- 脚本改成
find /data/cache -type f -mmin +10 -delete(只删 10 分钟前的文件),幂等 - 平台前端加防重复提交(按钮点击后禁用到返回)
- 操作目录定义里加
execution.idempotent: true标记,编排引擎对非幂等操作禁用自动重试
这个坑让我把“幂等性是操作目录的必填字段”写进了平台规范。
坑三:执行通道单点,平台自己成了故障源
Ansible 执行机一开始只部署了一台。某天这台机器的磁盘被日志打满,所有批量操作全部挂起——运维平台自己成了最大的单点故障。那天的感受是黑色幽默:我们给所有业务做高可用,平台自己先挂了。
解法:
- 执行节点做成无状态,水平扩展两台(成本不高,Ansible 控制机本身没有状态,Inventory 在共享存储)
- 平台侧加执行节点健康检查,摘除故障节点
- 兜底预案:堡垒机手工通道永远保留,平台全挂时降级为“带审批的手工操作”(审批走 IM,事后补录审计)
坑四:审计只记不查,出事等于没记
早期版本的审计日志就是往数据库插记录,连查询界面都没有。一次安全审计(等保 2.0 的检查项),审计人员要看“过去一个月谁在生产执行过配置变更”,我们从数据库手写 SQL 查了半小时,格式还不符合要求。
之后的改造:
- 审计日志加了三维度检索界面(人 / 时间窗 / 主机)
- 增加“操作回放”功能:点开一条执行记录,能看到该操作的完整输出(Ansible 的执行 stdout 落盘)
- 变更类操作自动生成配置 diff 快照
审计的功能边界是“出了事能快速还原事实”,做不到这一点的审计都是自我安慰。
效果度量:平台化之后到底赚了什么
空说“效率提升”没有说服力,上我们在 20 多个操作平台化运行半年后的实测数据(对比基线是平台化前三个月的手工操作均值):
| 度量项 | 平台化前 | 平台化后 | 变化 |
|---|---|---|---|
| 单次操作平均耗时 | ~15 分钟 | ~90 秒 | ↓ 90% |
| 其中运维人力占用 | 15 分钟 | 2 分钟(审批+异常处理) | ↓ 87% |
| 运维响应请求类工单 | ~120 单/月 | ~45 单/月 | ↓ 62% |
| 操作审计覆盖率 | 无法统计 | 100%(平台内操作) | 从 0 到 1 |
| 生产变更类故障 | 平台化前 6 个月 5 起 | 之后 6 个月 2 起 | ↓ 60% |
几个说明:
- “单次操作耗时 90 秒”包括:发起人填表单 30 秒、审批等待 30 秒(IM 一键批)、执行 30 秒
- 工单量降了 62% 而不是 80%,因为剩下 45 单里大部分是平台覆盖不了的复杂操作(需要人工判断的疑难杂症),这恰恰是运维应该花时间的地方
- 变更类故障降 60%,主要来自参数校验拦截(枚举参数杜绝拼错环境)和幂等性保障(重试不再放大事故)
有一个数字要诚实说明:人力节省 40% 的口径是“响应操作请求的人力投入”,不是运维团队总人力。省下来的时间去了哪里?一部分流向了巡检平台的开发(把巡检也自动化了,见 相关文章:自动化巡检平台的插件化架构),一部分流向了容量规划和容灾演练。琐事减少,工程时间增加——这就是 toil 治理的正循环。
总结
把这篇文章的要点收拢成一段话:
运维操作平台化的本质,是把“谁、在什么机器上、执行什么操作、依据什么参数”从口头契约变成系统契约。它解决的不只是效率问题(虽然单次操作 15 分钟到 90 秒很可观)——审计留痕、知识留存、风险控制这三个手工模式无解的结构性问题,才是平台化真正的价值所在。
四条最想留给你的经验:
- 操作目录是核心资产,比编排引擎和界面都重要。YAML 定义的操作标准是平台无关的,先把格式想清楚。
- 审批流的目标是让合规比绕过更快,不是卡住操作。分级管控(低危自助、中危一级审批、高危双人复核),审批要 IM 触达、超时升级。
- 幂等性是操作进目录的准入条件。非幂等操作在平台环境下就是定时炸弹,重试机制会把它引爆。
- 审计做不到“出事快速还原事实”就等于没做。三维度检索 + 执行回放 + 配置 diff,这三件套起步。
选型上记住一条:50 台机器以下用 Spug 起步,重度 Ansible 用户先看 AWX,有专职运维开发编制再考虑自研。平台化是长跑,第一年能做到“高频操作全部自助、生产操作 100% 留痕”,就已经跑赢了大多数团队。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Spug - 开源自动化运维平台 — openspug 社区,轻量级无 Agent 运维平台的架构与功能设计参考
- 中小企业如何做运维自动化? — Spug 官方博客,平台功能清单与适用场景分析
- 2026年四大自动化运维系统深度测评 — trainsignalcn,流程层-平台层-场景层三层架构与高危操作分级复核机制的行业实践
- Demystifying agentic AI: How to build production-ready AIOps with open source models — Red Hat,英国金融公司每周 600+ Ansible 任务、40 张失败工单的生产数据参考
- Google SRE Book - Eliminating Toil — Google SRE 团队,琐事(toil)的定义与治理框架