OneUptime Runbook 配置与安全指南:Agent 分发模型、超时限制、权限与数据库结构全解析
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
运行手册(Runbook)是 OneUptime 在事故响应与自动化修复场景中的核心编排模块:当告警触发时,Runbook 按照预先定义的步骤序列自动或手动执行诊断、止损与修复操作。本篇技术指南以官方文档 Runbook 配置与安全(英文原版见 configuration.md)为主线,深入剖析 OneUptime Runbook 的执行模型、超时与输出限制、权限体系、队列调度、安全加固机制以及底层数据库结构,并结合仓库源码给出可验证的实现细节。读完本文,你将能够正确规划 Runbook Agent(即 Runner)的部署、合理配置步骤超时、理解权限矩阵与数据流,并掌握在生产环境中安全运行自动化步骤的实战要点。
一、Bash 与 JavaScript 步骤的真实执行方式:Agent 分发模型
OneUptime 的 Runbook 步骤类型丰富(详见 RunbookStepType),其中 Bash 和 JavaScript 两类步骤有一个最关键的设计前提:它们永远不会在 OneUptime 的 Worker 进程上执行。它们会被作为作业(Job)派发给一个特定的 Runbook Agent——一个安装在你自己基础设施内某台主机上的小型进程(在 OneUptime 中现在正式命名为Runner)。
官方文档给出的分发模型分为四步:
- Runbook 步骤的作者在编写步骤时,从下拉框中选定一个 Runbook Agent(Runner)。
- 步骤执行时,Worker 在
RunnerJob表中插入一行记录,其中targetAgentId指向所选 Agent 的 ID,状态为Pending。 - 只有那个特定的 Agent(且仅有它能)以原子方式认领(claim)该作业,在本地执行脚本——Bash 通过
bash -c <script>,JavaScript 在isolated-vm沙箱内运行——然后将结果回传。 - Worker 拿到结果后继续推进 Runbook 的下一个步骤。
这一模型在源码中体现得非常清晰。在 StepExecutors.ts 中,Bash、JavaScript、SSH、Kubernetes 四类步骤共享同一条dispatchToAgent分发路径:先校验agentId非空且为合法 ObjectID,然后调用RunnerJobService.enqueue创建作业,再通过RunnerJobService.pollUntilTerminal轮询直到作业到达终态。runJavaScriptStep与runBashStep的唯一差异只是stepType不同——Agent 依据该字段在本地选择对应的执行器:
// runJavaScriptStep 与 runBashStep 的核心差异仅在于 stepType export async function runBashStep(step: RunbookStep, ctx: StepExecutionContext): Promise<StepRunResult> { const config: BashStepConfig = step.config as BashStepConfig; return dispatchToAgent({ stepType: RunbookStepType.Bash, step, ctx, script: config.script || "", timeoutInMs: resolveStepExecutionTimeoutInMs(config.timeoutInMs), claimTimeoutInMs: resolveAgentClaimTimeoutInMs(config.claimTimeoutInMs), agentId: config.agentId || "", missingAgentError: "Bash step is missing a Runbook Agent. Pick an agent under Runbooks → Agents.", }); }源码中的报错信息也印证了“脚本不再在 Worker 上运行”这一事实:runJavaScriptStep的missingAgentError明确写着"JavaScript no longer runs on the OneUptime Worker."。
一个重要的事实变更:RUNBOOK_BASH_ENABLED这个环境变量标志已经不存在了。某个部署中 Bash/JavaScript 步骤能否工作,完全取决于项目里是否至少有一个已连接的 Runbook Agent(Runner)。这意味着启用自动化步骤的前提是正确安装并注册 Runner。
二、输出上限与超时控制
为了保障 Worker 与 Agent 的资源安全,OneUptime 对步骤执行施加了严格的输出与超时限制:
| 限制项 | 默认值 | 说明 |
|---|---|---|
| 单步输出上限 | 50 KB | 超出部分被截断,并附加截断标记 |
| 单步执行超时(JavaScript / Bash / HTTP) | 30 秒 | 可在 Runbook 的「步骤(Steps)」页面按步骤设置,留空则使用默认值 |
| 单步认领超时(Claim timeout,Bash / JavaScript) | 2 分钟 | Worker 等待所选 Agent 认领作业的最长时间,超时则判定步骤失败,同样按步骤可配 |
| 超时取值范围 | 1 秒 ~ 1 小时 | 超出范围的值在步骤实际执行时被钳制(clamp)到边界值 |
超时取值范围的钳制逻辑具有重要的健壮性意义:一份写错的配置既不能关掉超时,也不能无限期占用一个 Worker 槽位。
源码层面的证据在 RunbookStepTimeout.ts 中:
DEFAULT_STEP_EXECUTION_TIMEOUT_IN_MS = 30 * 1000,MIN_STEP_EXECUTION_TIMEOUT_IN_MS = 1000,MAX_STEP_EXECUTION_TIMEOUT_IN_MS = 60 * 60 * 1000;DEFAULT_AGENT_CLAIM_TIMEOUT_IN_MS = 2 * 60 * 1000,最小/最大同样为 1 秒与 1 小时。
关键的解析函数resolveTimeoutInMs的语义是:任何不可用的输入(未设置、空字符串、非数字、零或负数)都回退到默认值,而不是让步骤失败——因为在事故进行中,用文档化的默认值跑完一个 Runbook,远比因配置损坏而拒绝执行更有用。可用值则取整到毫秒并钳制进[min, max]区间。Dashboard 步骤编辑器与 Worker 执行端都通过该模块解析超时,保证作者看到的边界与实际执行的边界永远一致。
输出上限 50 KB 则在 StepExecutors.ts 中实现:MAX_OUTPUT_BYTES = 50_000,truncate函数按 UTF-8 字节数截断输出,超出时追加\n... [output truncated]标记。
三、权限体系:Runbook 权限组
Runbook 的所有权限都归属在Runbook权限组之下。官方文档列出以下权限项:
| 权限 | 作用范围 |
|---|---|
CreateRunbook/EditRunbook/DeleteRunbook/ReadRunbook | 管理 Runbook 模板 |
CreateRunbookExecution/EditRunbookExecution/ReadRunbookExecution | 启动、勾选完成、读取执行记录 |
CreateRunbookRule/EditRunbookRule/DeleteRunbookRule/ReadRunbookRule | 管理自动触发规则 |
CreateRunner/EditRunner/DeleteRunner/ReadRunner | 管理在你自身基础设施中执行步骤的 Runner |
值得注意的兼容性说明:*Runner系列权限在 Runner 改名之前叫*RunbookAgent;现有授权已自动迁移,无需重新分配。
此外还有三个可分配给团队的角色:
RunbookAdmin——完整控制权,聚合了上面全部细粒度权限;RunbookMember——日常使用;RunbookViewer——只读访问。
这些权限在实际的数据库访问控制中有严格落地。例如 RunnerJob.ts 的@TableAccessControl明确规定create与update均为空数组(该表不可由用户直接写入,只能由 Worker 与 Agent 管理),读取则要求ProjectOwner/ProjectAdmin/ProjectMember/Viewer/RunbookAdmin/RunbookMember/RunbookViewer/ReadRunbookExecution之一。又如 Runner.ts 中 Agent 密钥(key字段)的读取权限被收紧到仅ProjectOwner/ProjectAdmin/RunbookAdmin——注释里写得很明白:拿到这把密钥就等于拿到了该 Runner 能接触的所有 Runbook 密钥与凭据(因为它会在认领作业时被用于解密注入),因此其读取门槛必须与管理 Runner 本身同级,避免让只读的 Viewer 成员间接拿到项目 SSH 私钥和 kubeconfig。
四、队列与 Worker:调度机制
Runbook 执行运行在名为Runbook的BullMQ 队列上。Worker 的并发度(concurrency)为25——如果你的部署存在大量并发执行,可以在部署中调整该值。
文档中还提到一个调度细节:当某个手动步骤通过 API 被勾选完成后,执行会被重新入队,以便从下一步继续推进。这样做的目的是保持 Worker 处于“热”状态(keep the worker hot),为 Runbook 的其余步骤持续服务。
与此相关的执行上下文见 StepExecutors.ts 的StepExecutionContext:它携带runbookExecutionId、runbookName、incidentId、alertId、scheduledMaintenanceId、triggeredByUserId以及previousStepExecutions(此前各步骤的状态快照),这些上下文正是后续步骤(尤其是 AI 步骤)判断触发来源与历史状态的依据。
五、安全加固要点(Hardening Notes)
5.1 JavaScript 与 Bash 的隔离执行
- JavaScript:运行在
isolated-vm沙箱中,并注入一段标准的 prelude(前奏代码),其职责包括:切断原型链(severs prototype chains)、移除Function与eval、冻结内置原型(freezes built-in prototypes),从而削弱逃逸沙箱的常见手段。 - Bash:通过
bash -c执行,并且超时强制在 Agent 侧实施——即 Agent 本地负责在超时后终止脚本进程。
由于这两类脚本运行在由你控制的 Agent 主机上而非 OneUptime Worker 上,即便脚本行为异常,其影响范围也被限定在 Agent 所在的主机环境内。
5.2 HTTP 步骤的宽松状态校验
HTTP 步骤使用一个宽松的状态校验器(permissive status validator):4xx/5xx 响应会被记录为步骤失败,而不是作为异常抛出。这样设计的好处是,捕获到的输出能真实反映上游实际返回的内容(状态码、响应头、响应体都会进入输出)。
源码佐证在 StepExecutors.ts 的runHttpStep:它使用原始 axios 发起请求,validateStatus: () => true令所有状态码都不触发 axios 的 reject;随后拼装Status/Headers/Body三段文本作为输出,2xx~3xx 判定成功,其余状态判定失败并附带HTTP <status>错误信息。
同时,HTTP 步骤的安全设计值得一提:因为步骤的 URL、方法、请求头、请求体来自 Runbook 的 steps JSON,可被项目成员读取,且响应会原样回传给调用方,这本质上是一个“攻击者可控形状”的出站请求。因此 OneUptime 通过DataSourceEgressGuard.assertUrlAllowedAndPin(见 DataSource/EgressGuard)校验目标地址并固定其解析后的 IP,同时禁止重定向(maxRedirects: 0)——因为 IP 固定只覆盖被校验的那个主机,如果放行 3xx 重定向就会绕过校验,把请求引向未经验证的目标(例如云元数据服务 IMDSv2)。
5.3 Agent 认证:ID + 密钥,权威身份来自数据库
Agent 的认证方式是ID + 密钥(secret key),二者以环境变量的形式配置在 Agent 容器中。服务端侧,Agent 的权威身份来自数据库中以所提交的 ID/密钥为键的那一行记录——这意味着:即使某客户端拿到了一个 Agent 的密钥,它也只能以该 Agent 的身份行事,无法伪装成另一个 Agent。
实现见 RunnerAuthorization.ts:中间件从请求体或x-agent-id/x-agent-key请求头提取凭据,缺少任一即返回BadDataException("agentId or agentKey is missing");随后调用RunnerService.findByIdAndKey按 ID+密钥查库,查不到则返回Invalid agentId or agentKey。身份校验通过后,req.runner被赋值,后续的认领与心跳接口都基于该身份工作。
六、数据库表结构
Runbook 功能背后涉及五张核心表:
Runbook——模板
存储 Runbook 模板本身:名称(name)、slug、描述、isEnabled、以及步骤 JSON(steps JSON,其中包含各步骤的配置,如 agentId、script、超时值等)。
RunbookExecution——一次运行一条记录
每次执行一行,带有可空的incidentId、alertId、scheduledMaintenanceId外键,以及一个 JSON 类型的stepExecutions数组,快照记录每个步骤及其实时状态。HTTP 步骤的完整状态/响应头/响应体也会被复制进stepExecutions,供项目成员查看。
RunbookRule——自动触发规则
带triggerEntityType判别字段(取值为 Incident / Alert / ScheduledMaintenance),并与要启动的 Runbooks 构成多对多关系。这类规则是 Runbook 自动化的入口,详见 rules.md。
Runner——每个已安装 Agent 一行
包含名称、密钥(key)、lastAlive(最近心跳时间)、connectionStatus(Connected/Disconnected)、hostInfo(主机名、OS、架构等自报信息),以及能力开关字段:canRunRunbooks(默认开启)、canRunCodeFixTasks(默认关闭)、canRunAiCommands(默认关闭,开启后 AI 自动修复命令才可能在其上执行)。表名保持Runner不变(改名会牵扯所有外键与索引),但产品名已统一为 OneUptime Runner。
RunnerJob——每个被派发的 Bash/JavaScript 步骤一行
关键字段包括:
targetAgentId——步骤作者选定的 Agent ID,只有它能认领该作业;stepType——步骤类型(Bash 或 JavaScript);script——要执行的脚本内容;status——生命周期状态,完整流转为Pending→Claimed→Running→Succeeded/Failed/TimedOut/Cancelled;claimDeadlineAt——认领截止时间,到期无人认领则 Worker 以TimedOut判定失败;leaseExpiresAt——租约到期时间,Agent 未按时心跳则 Worker 收回作业;output——Agent 回传的合并 stdout/stderr,服务端有 50 KB 上限;exitCode——进程退出码(超时时为空);errorMessage——失败时的简短错误说明。
从 RunnerJob.ts 的列定义看,该表还支持origin字段(Runbook 或 AiRemediation),用于区分作业来源,并据此决定 Runner 的认领能力门槛(canRunRunbooks或canRunAiCommands)。另外注意:script虽在数据库层面 NOT NULL,但 SSH/Kubernetes 这类“以 payload 携带结构化指令”的步骤其脚本为空是正常形态,因此列元数据层面required为 false——脚本/载荷的按类型约束由RunnerJobService.enqueue统一执行。
七、实战运维建议
官方文档在结尾给出了三条重要的生产实践建议:
确保每个步骤所选 Agent 处于健康状态。如果需要冗余,可以部署第二个 Agent 并把步骤分散到多个 Agent 上,或者维护一个指向另一 Agent 的备用 Runbook。判断 Agent 健康状况的字段是
Runner.lastAlive与connectionStatus,二者均由 Agent 每次心跳更新。捕获 URL,而不是数据块(Capture URLs, not blobs)。如果某个步骤会产生超过几 KB 的输出,不要试图把大段文本塞回步骤输出——把产物写到 S3 或你的日志栈,然后只返回对应的 URL。因为单步输出有 50 KB 的硬上限,超出部分会被截断丢失。
幂等性至关重要。自动化步骤(HTTP、JavaScript、Bash)在以下场景可能被执行多次:Worker 在步骤中途重启、或 Agent 的租约在脚本仍在运行时过期。因此设计这些步骤时,必须保证重复执行是安全的。
关于第 3 点的实现细节,源码里有一个很好的补充:dispatchToAgent在重新入队前会先调用RunnerJobService.findLatestJobForStep查找该步骤已有的作业记录——如果已存在终态作业就直接采用其结果,如果存在进行中的作业则“重新挂接(re-attach)”而非再次派发(见 StepExecutors.ts)。这是对“Worker 重启导致重复派发”的第一道防线,但作为 Runbook 作者,你仍应按“脚本可能跑两次”的前提来编写逻辑。
结语
OneUptime 的 Runbook 模块通过Agent 分发模型把最危险的脚本执行(Bash/JavaScript)从中心 Worker 隔离到自托管 Runner 上,配合 50 KB 输出上限、双超时钳制、基于数据库权威身份的 ID+密钥认证、以及isolated-vm沙箱等机制,构建了一套适合生产事故响应场景的安全执行框架。结合本文对 StepExecutors.ts、RunbookStepTimeout.ts、RunnerJob.ts、Runner.ts 等源码的剖析,你现在可以从执行模型、资源限制、权限矩阵、队列调度与数据模型五个层面完整理解 Runbook 的配置与安全语义。进一步的实操指引可继续阅读同一目录下的 agents.md(Agent 安装)、authoring.md(步骤编写)、running.md(执行与手动操作)与 credentials.md(凭据管理)。
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考