Serverless Framework Runner 架构解析:SF-Core 如何通过 Runner 扩展 CLI 命令执行体系
【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless
本篇技术指南聚焦 Serverless Framework(仓库中即 sf-core 与 framework 源码包)的Runners 执行框架:先厘清 Runner 作为 CLI 命令执行积木的定位,再逐一讲解 Runner 抽象类中“框架回调函数”与“实现内可用函数”两类 API 的职责与签名,最后结合TraditionalRunner、CoreRunner、CfnRunner、ComposeRunner四个具体实现,说明 CLI 路由、CLI Schema、状态存储与遥测事件是如何在这一套 Runner 协议上运转的。读完你将掌握如何理解该仓库中任意命令的完整执行链路,以及如何基于 Runner 协议扩展新的命令类型。
什么是 Runner:SF-Core 的命令执行积木
在 Serverless Framework 的新版核心(SF-Core,即 packages/sf-core)中,Runner是构建整个 CLI 的基本积木。文档定义清晰:它们负责执行命令,并管理命令的输入与输出("responsible for executing commands and managing their input and output")。
每个 Runner 都是一个继承自Runner抽象类的类,其职责边界包括:
- 声明自己关心哪些配置文件(如
serverless.yml、serverless-compose.yml、template.yaml); - 根据配置内容判断自己是否应该被执行;
- 声明当前命令的 CLI Schema(用于帮助信息生成与参数校验);
- 真正执行业务逻辑(部署、移除、登录、用量查询等);
- 上报服务唯一标识(
serviceUniqueId),供用量追踪与状态存储使用。
Runner 的实例由 SF-Core Framework 按路由规则实例化,并通过调用其run方法驱动执行。路由中枢位于 packages/sf-core/src/lib/router.js:
const runners = [ComposeRunner, CfnRunner, TraditionalRunner]可见声明式 runner 列表为三个;CoreRunner则通过另一条"核心命令优先"的规则被选中(见 router.js:当命令为空(onboarding)、命令命中CoreRunner.getCliSchema()或使用--version等场景时,Runner 会被替换为CoreRunner)。路由核心逻辑getRunner位于 router.js。
Runner 的公共 API 分两大类:由 SF-Core Framework 调用的函数(即 Runner 必须/可选实现的契约)与供 Runner 实现内部调用的函数(即基类提供的能力)。两者集中定义在 Runner 基类(class Runner自 index.js 起),并配有完整的 JSDoc 类型说明(如Command、Subcommand、Option、Positional、RunnerResult,见 index.js)。
必须由 Runner 实现的函数
configFileNames
返回会触发该 Runner 的配置文件名称列表。路由过程中,SF-Core 会在当前工作目录中扫描这些文件名(并允许通过configFileExtensions约束扩展名),找到后读取配置并交给对应 Runner。
static configFileNames = ['serverless'] // TraditionalRunner,见 framework.js static configFileNames = ['serverless-compose'] // ComposeRunner static configFileNames = ['samconfig', 'template'] // CfnRunner具体扫描逻辑见 router.js:SF-Core 遍历 runner 列表,把目录内每个文件的基础名与各 Runner 声明的configFileNames比对,若命中即返回该 Runner 类、配置对象与配置路径。CfnRunner还通过configFileExtensions限制了扩展名(samconfig仅接受 toml/yaml/yml,template仅接受 yaml/yml/json),避免把业务代码template.mjs误判为 SAM 模板(见 cfn.js)。
shouldRun
接收{ config, configFilePath },返回布尔值,判断基于配置文件内容该 Runner 是否应当执行。典型实现:
// TraditionalRunner static async shouldRun({ config }) { return !!config.service // 存在 service 顶层键即视为传统 serverless.yml 服务 } // ComposeRunner static async shouldRun({ config }) { return !!config.services }注意shouldRun是静态方法,在路由期(尚未实例化 Runner)就会被调用,因此只依赖 config 对象本身。
getCliSchema
返回当前 Runner 的 CLI Schema——一个描述命令、子命令、选项与位置参数的 JSON 结构。它有两个用途:
- 生成帮助信息(
serverless --help、serverless deploy --help); - 校验与规范化命令行参数。
在路由流程中,SF-Core 先取出 runner 的 schema 并调用validateCliSchema(见 packages/sf-core/src/utils/cli/cli.js)做解析校验,随后才执行runner.run()。若helpPrinted/versionPrinted为真,则直接返回、不执行命令。这一点在 router.js 中体现。
run
Runner 的核心方法,包含其全部主逻辑。基类 JSDoc 对run的实现方给出了严格的契约(见 index.js):实现方应当按顺序完成:
- 认证用户(authenticate);
- 解析变量(resolve variables);
- 如需使用状态存储,先 resolve 状态存储;
- 返回
RunnerResult:{ serviceUniqueId, state? }。
serviceUniqueId有三个去向:用量事件中标识服务、状态存储中作为服务的键(Compose 正确工作依赖它)、部署记录关联。state是可选字段,表示要整体写入状态存储的对象——注意不支持部分状态更新("No partial state management is supported"),这一点对理解 Compose 与状态恢复逻辑至关重要。
getServiceUniqueId
返回服务唯一标识。基类提醒:该函数被调用时不会预先完成认证、变量解析与状态存储初始化,因此实现方必须自行兜底(见 index.js)。TraditionalRunner的实现(framework.js)正体现了这一点:若尚未认证,会先自行调用resolveVariablesAndAuthenticate(),随后解析变量、取得 AWS 部署凭据,最后根据 stackName(provider.stackName或默认<service>-<stage>)查询 CloudFormation 栈信息,把CloudFormation StackId作为服务唯一标识返回;若栈不存在则抛出code = 'STACK_DOES_NOT_EXIST'的错误,该哨兵值会被 Compose 的 get-state 流程用来区分"尚未部署"与"真实失败"。
可选实现的函数
| 函数 | 用途 | 说明 |
|---|---|---|
customConfigFilePath | 返回自定义配置文件路径(如--config/-c选项指向的文件) | 静态方法,路由期调用。见findRunnerForCustomConfigName(router.js) |
getUsageEventDetails | 用量事件的补充详情 | 这些数据会发送到 Serverless, Inc. API,严禁包含任何不必要或敏感数据(基类 JSDoc 同样反复强调,见 index.js) |
getAnalysisEventDetails | 分析事件补充详情 | 用于形态分析(runtime、构建时长、产物大小、插件、sandboxes/MCP 配置块等,见TraditionalRunner实现 framework.js) |
getDeploymentEventDetails | 部署事件详情 | 仅在启用 Dashboard 且命令为 deploy/remove 时生成完整部署记录 |
getMetadataToSave | 追加写入 meta.json 的元数据 | 基类接口存在(index.js),TraditionalRunner用它保存 AWS 账号、栈状态、CloudFormation 模板等(framework.js) |
其中getUsageEventDetails、getAnalysisEventDetails与getDeploymentEventDetails的结果最终在 router 的finalize/sendAnalysisAndUsageEvent/sendDeploymentEvent阶段被并行收集并上报(见 router.js)。
基类提供给 Runner 实现使用的函数
认证
authenticate
使用以下输入完成用户认证:
| 输入 | 来源 |
|---|---|
org | orgCLI 选项、配置文件org键、SERVERLESS_ORG_NAME环境变量 |
app | appCLI 选项、配置文件app键 |
service | 配置文件service键 |
stage | stageCLI 选项、配置文件provider.stage,默认dev |
region | regionCLI 选项、配置文件provider.region,默认us-east-1 |
license key | SERVERLESS_LICENSE_KEY或SERVERLESS_ORG_ACCESS_KEY环境变量;配置文件licenseKey键;部署所用 AWS 账号中的/serverless-framework/license-keySSM 参数 |
实现见 index.js:通过progress.get('authenticate')上报进度,构造Authentication实例(来自 packages/sf-core/src/lib/auth/index.js),将 config/options/resolverManager/compose orgName 交给authentication.authenticate,结果缓存到this.authenticatedData并返回。认证完成后还会调用processNotifications处理来自 BFF 的通知(可阻断 deploy/package 等命令,见 index.js)。
resolveVariablesAndAuthenticate
在认证前先按序解析配置文件中的关键变量(见 index.js):
- 先解析
params(仅限一组受限 providers,见ResolverManager.limitedProvidersSet); - 解析
org、app、service、stage、region——Dashboard 认证需要它们来拉取服务级信息(如参数与来自 Dashboard Provider 的 AWS 凭据); - 解析
provider.profile——AWS SDK 认证需要;由于licenseKey支持ssm这类 AWS resolver,必须先确定 AWS profile; - 注入默认 AWS 凭据 resolver(
addDefaultAwsCredentialResolver),以便从 SSM/Secrets Manager 读取 license key; - 解析
licenseKey供认证使用; - 执行认证;
- 把 Dashboard 数据加载进 resolverManager(
loadDashboardData),供后续变量解析(Dashboard 参数、Dashboard Provider 的 AWS 凭据)使用; - 统一处理一次通知。
配置文件与变量解析
resolveVariables
解析配置文件中的全部变量。基于resolverManager.resolveConfigFile实现(index.js),支持printResolvedVariables选项(print命令 +--debug时打印解析结果,见 router.js 与reloadConfig的同名条件)。
reloadConfig
不做变量解析地重载配置文件,适用于"执行过程中配置文件被更新"的场景(如插件或前置命令改写了配置)。它返回{ config, configFilePath, configFileRaw, resolverManager, stage },并同步更新 Runner 实例的四个状态(见 index.js):
this.config:新配置对象;this.configFilePath:新配置路径;this.resolverManager:基于新配置重建的 ResolverManager(通过variables.createResolverManager);this.stage:若 stage 来源是配置文件则更新为新值。
其中createResolverManager还会把既有 Compose 的resolverProviders、params传入,保证嵌套上下文下 resolver 一致。
状态管理
resolveStateStore
创建或复用状态存储,使用状态存储前必须先调用本函数(见 index.js)。其底层执行步骤(对应@serverless/util导出的resolveStateStore):
- 解析状态存储 provider 的凭据:若配置了
state,使用其中指定的 provider;否则使用部署凭据(credentialProvider); - 查询
/serverless-framework/state/s3-bucketSSM 参数以获取 bucket 名;若未设置则生成 bucket 名并写回该参数; - 检查 bucket 是否存在,不存在则创建;
- 返回
{ putServiceState, getServiceState }供读写服务状态。
Runner 有两种使用方式:执行中通过返回的函数读写;或只在run返回的RunnerResult.state中携带完整状态对象,由 router 统一落盘。router 在 router.js 中检查runner.state && runnerResult.state,然后用putServiceState({ serviceUniqueId, runnerType, value })将JSON.stringify后的整个 state 写入状态存储。
凭据与数据读写(Resolver Provider 体系)
以下三个函数把 Runner 与 packages/sf-core/src/lib/resolvers 中的Resolver Provider体系打通。Provider 需在配置文件的stages.<stage>.resolvers中注册,具体形态为stages.<stage>.resolvers.<provider>.<resolver>。
getProviderCredentials({ providerName }):通过loadAndResolveProvider加载指定 provider 实例,再调用其resolveCredentials方法返回凭据(index.js);fetchData({ providerName, resolverName, key }):基于 provider 的resolveVariable能力获取数据——先getProvider再getResolver得到具体 resolver 函数并调用(index.js);storeData({ providerName, resolverName, key, value }):基于 provider 的storeData能力写入数据,走getWriter得到 writer 函数(index.js)。
CLI Schema:命令的声明式描述
getCliSchema返回的 JSON 结构描述的是整棵命令树。基类 JSDoc 定义了四种核心类型:
- Command:
{ command, description, builder? },command支持位置参数占位(如'server <mode>'); - Subcommand:与 Command 同构,可嵌套(
builder递归),并支持options/positional; - Option:
{ alias?, description, type, demandOption?, default? }——例如alias: 'r'表示--region可用-r缩写,demandOption: true表示必填,type为'string'|'number'|'boolean'; - Positional:
{ name, description, type, choices? }。
文档明确指出:schema 在底层会被转换为yargs配置对象,因此关于属性的更多细节可参考 Yargs API 参考。转换与解析的实际执行处即validateCliSchema(packages/sf-core/src/utils/cli/cli.js):它先把command与options拼成 argv 数组,构建yargs(argv)实例,再递归applyConfigurations把 schema 中的 command/builder/option 配置应用到 yargs 上,最终通过cli.argv触发解析,并把结果拆回{ command, options }。
CoreRunner的 schema 是一个很好的真实范本(core/core.js),覆盖了:
static getCliSchema() { return [ { command: '$0', description: 'Onboard to Serverless Framework', builder: [...] }, // 引导 { command: 'login', description: 'Log in a user', builder: [...] }, // login / login aws / login aws sso { command: 'logout', ... }, { command: 'usage', ... }, { command: 'reconcile', ... }, { command: 'plugin', ... }, // plugin install / uninstall { command: 'agent', ... }, // agent skills install / agent inspect { command: 'mcp', ... }, // mcp(transport/port 等选项) ] }值得注意的细节是:CoreRunner为了路由解析声明了agent inspect及其全部选项(含array: true的--name),但真正执行时通过delegateToFramework()把命令转交给新建的TraditionalRunner(core/core.js),因为该命令实际是 Framework 侧插件命令。转交时还通过stripCamelCaseDuplicateOptions清理 yargscamel-case-expansion产生的重复键(如--aws-services同时产生的awsServices),避免触发 Framework 侧 "Unrecognized option" 校验(见 core/core.js 的长注释说明)。
内置 Runner 家族:四种执行路径
TraditionalRunner(runnerType:traditional)
面向传统 Serverless Framework 服务(含service键的serverless.yml),见 framework.js。其run流程具有代表性:
- 调用
configureLocalstackAWSEndpoint处理 LocalStack 场景; - 依据 Framework 命令 schema 把选项短别名转换成完整名(
convertOptionShortcutsToFullNames); resolveVariablesAndAuthenticate()(认证);resolveVariables()(解析模板变量);- 解析 AWS 部署凭据;
- 捕获部署前
serviceUniqueId(用于区分"创建 vs 更新",记录stackExistedBeforeRun); - 调内部
runFramework构造new Serverless(...)(来自 packages/serverless),serverless.init()后执行命令(deploy/info/remove 等); - 用
buildCommandState(state-utils.js)基于 stack outputs 构建命令状态,并采集构建时长、artifact 大小等分析指标; - 返回
{ authenticatedData, state, serviceUniqueId }。
runFramework内部还承担了大量服务级校验:找不到配置抛FRAMEWORK_CONFIG_NOT_FOUND、provider.name仅支持aws(其他 provider 会提示使用 Extensions)、仅当前 stage/default 的params会被保留、Compose 上下文中 org 一致性校验等(framework.js)。
CoreRunner
处理与具体服务配置解耦的核心命令,见 core/core.js。run按this.command[0]分发:
- 无命令:执行 onboarding(
commandOnboarding); login/login aws/login aws sso:分别走loginAws/loginAwsSso,其余走this.authenticate();logout:调用Authentication.unAuthenticate();usage:认证后基于 AWS 凭据调用commandUsage(月度实例额度查询与导出);reconcile:认证后调用commandReconcile,把计费实例列表与真实 CloudFormation 栈对齐,回收手动删除产生的额度;plugin install/plugin uninstall:读写服务配置中的插件(需要服务目录上下文,无配置则抛CONFIG_FILE_NOT_FOUND);agent skills install:安装 Agent Skills 到服务目录;agent inspect交给 Framework Runner 处理(Compose 根目录下会给出不支持提示);mcp:以非交互模式启动 MCP Server(stdio或sse,端口默认 3001)。
CfnRunner(runnerType:cfn)
面向SAM/CloudFormation 栈(samconfig+template配置),见 cfn.js。它复用同一套 Runner 协议,通过deploy/remove/info/print子命令管理原生 CloudFormation/SAM 栈,CLI schema 中为每个子命令声明stage(-s)、region(-r)、stack(-st)、bucket(-b) 等选项。仓库文档树的 docs/sf/guides/sam.md 即为该能力的配套使用指南。
ComposeRunner(runnerType:compose)
面向多服务编排(serverless-compose.yml,含services键),见 compose.js。支持的子命令集为['deploy','info','remove','print','package']。其run会:
- 认证 + 解析变量 + 取得 AWS 凭据;
- 拒绝嵌套 Compose(
NESTED_COMPOSE_PROJECT); - 通过
runCompose把各子服务交由 router 再次路由(每个子服务内部会实例化其对应 Runner),并把 Compose 的resolverProviders/params共享下去。
配套的状态读写与 service 子集选择逻辑在 state.js 与 compose/index.js,相关配置文档见 docs/sf/guides/compose.md。
Runner 与遥测、状态、Compose 的协作关系
Runner 协议并非孤立设计,而是被三层协作所环绕:
- 用量与行为分析:router 的
createAnalysisEvent会注入 runnerType(projectType)、CLI options、resolvers、执行时长、错误信息与getAnalysisEventDetails()的返回;使用 license key 时不发送分析事件。非 CoreRunner 的成功执行会额外依据serviceUniqueId生成用量事件并交给实例用量追踪客户端。所有事件最终在finalize阶段并行发布(router.js)。 - 状态存储:只有声明了
runnerType(traditional/cfn/compose)的 runner 会随run返回的 state 一起写入状态存储,putServiceState以{ serviceUniqueId, runnerType, value }为结构落盘,Compose 依靠它判断各服务部署到哪一步、恢复执行。 - Compose 路由共享:Compose 在把子服务"扇出"到 router 时复用父级解析出的 resolver providers、params 与凭据 provider,避免对每个服务重复解析凭据。
runnerType由各 Runner 的静态属性声明,如TraditionalRunner.runnerType = 'traditional'、ComposeRunner.runnerType = 'compose'、CfnRunner.runnerType = 'cfn',并在路由末尾随结果一并返回(router.route的返回对象)。仓库测试如 runner-discovery.test.js、compose/state.test.js 与 core/agent-routing.test.js 可作为验证上述路由、状态与委托逻辑的参考。
小结:Runner 协议的设计要点
从源码与文档可以归纳出该 Runner 体系的核心设计约束:
- 对称契约:路由期需要"静态"能力(
configFileNames/shouldRun/customConfigFilePath/getCliSchema),执行期需要"实例"能力(run/getServiceUniqueId/各类事件详情),两类方法分离清晰; - 自包含执行顺序:
run被要求先认证、再解析变量、需要时再解析状态存储,且getServiceUniqueId被调用时不会预置任何上下文——实现方需容忍独立调用; - 安全红线:一切会外发到 Serverless API 的事件数据(usage/analysis/deployment)都要求实现方剔除不必要与敏感信息,源码中对 license key、access key 的日志也统一做了脱敏(
maskKeyForLog); - 声明式 CLI:CLI 帮助与参数校验完全由 Runner 返回的 schema 驱动,经 yargs 统一转换,命令行的展示与实现被解耦。
理解这套协议后,无论是排查serverless deploy的完整调用链(router → TraditionalRunner → Serverless 实例),还是阅读serverless agent inspect、serverless mcp等新命令如何复用认证与凭据管线,都有了清晰的入手点。
【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考