开发者门户集成、权限与运行:让聚合入口不变成高权限故障源
一个服务页面同时显示仓库、构建、Kubernetes 工作负载、云成本和文档状态,看起来像是团队终于拥有了一张完整地图。事故发生时,值班人员却发现页面里的部署状态落后半小时,普通访客可以直接调用隐藏按钮背后的后端接口,Git 集成使用的个人 Token 还属于已经离职的同事。门户聚合得越多,一次错误授权、缓存污染或插件故障影响的系统就越多。
开发者门户不是把若干控制台塞进同一导航。它在浏览器、门户后端、插件、身份提供方和下游系统之间建立信任链:用户是谁,插件代表谁访问,哪些字段可缓存,写动作由谁批准,失败是否会阻断整个页面。运行治理的目标是让聚合带来可发现性,而不是制造一个持有全组织权限、又展示陈旧事实的单点故障源。
先认清框架、插件与商业产品的边界
Backstage 是开发者门户框架,不是安装后就自带企业级连接器、细粒度 RBAC、审计归档和厂商支持的成品。官方仓库当前稳定 release line 为 v1.53.0,核心代码采用 Apache-2.0 许可;插件目录同时包含核心、社区和供应商插件,目录收录不等于核心团队承诺兼容性、SLA 或商业授权。每个插件仍要单独核对包许可证、维护状态、数据去向、支持合同与停服后的导出能力。
Backstage 核心提供多种认证 provider 和可编程权限框架,但不承诺一个适配所有组织的 RBAC 管理控制面;官方产品 FAQ也明确它不是监控平台,只能通过插件接入监控工具。托管升级、成品化 RBAC、合规报表、多租户隔离、专有连接器与支持 SLA 是否存在,取决于采用的发行版和合同。采购评估不能拿演示页面代替 API、权限矩阵和退出条款。自建团队同样要为 PostgreSQL、缓存、对象存储、密钥、升级和事故响应付出持续成本。
v1.53.0 的升级说明包含会直接影响门户入口的破坏性变化:OAuth redirect URI 与客户端元数据 allowlist 匹配更严格,旧的代理引导已移除,Node 代理改用 NODE_USE_ENV_PROXY=1 配合 HTTP_PROXY、HTTPS_PROXY、NO_PROXY。升级时应使用官方 Upgrade Helper比较应用模板差异,并逐包阅读 changelog;不要把 next 文档或 alpha API 当作稳定承诺。Backstage Metrics Service当前仍标记为 alpha,生产指标可以先使用 OpenTelemetry 自动埋点或既有采集器,避免把长期监控契约绑死在 alpha 接口上。
先画出每条集成的信任链
每个插件接入前写清六个对象:数据 Owner、读取主体、写入主体、授权决策点、缓存位置和审计出口。Git 页面读取仓库描述可以使用 Git App 安装 Token;触发 CI 重跑可能需要用户授权或受控服务动作;Kubernetes 只读观察可以按用户身份透传,也可以使用受限 ServiceAccount;云成本数据通常由后台同步任务读取,不能把云凭证发到浏览器。
Backstage 的integrations 配置位于 app-config.yaml 根级,因为 Catalog、Scaffolder、TechDocs 等多个能力会复用它。复用不等于共享一个万能 Token。GitHub App 相比个人 Token 有清晰的安装边界与短期令牌,但 App 的权限仍需在 Git Provider 侧管理;官方的GitHub Apps 说明也提示权限取决于实际用途。按组织、环境或数据敏感级别拆分 App,可以缩小单次泄露和错误配置的影响面。
先做一张字段级数据流表。例如仓库默认分支由 Git 权威,构建结论由 CI 权威,部署副本由 Kubernetes 权威,Owner 由 Catalog 权威,文档内容由文档制品权威。插件只保存展示所需的最小投影、来源和抓取时间,不能在缓存里复制完整 API 响应。没有来源时间的绿色图标只是颜色,不是证据。
写链路和读链路要分开。读插件可以被限流、缓存和降级,写插件必须重新认证、重新授权、校验 CSRF/请求来源、记录理由并使用幂等键。一个插件同时拥有读取所有仓库和删除仓库权限,意味着任何 SSRF、依赖漏洞或路由越权都可能跨越整个组织。
从本地启用一个最小集成
在已有 Backstage 应用中,先选择一个只读 Git 集成跑通,不要一开始同时接 Git、云、集群与 CI。Git 集成由多个核心能力复用,通常先配置即可;需要 Kubernetes 页面时,再按应用锁定的 Backstage release line 安装前后端插件,并在新后端入口注册 backend plugin。Backstage 版本策略规定 umbrella release 与各 npm 包独立版本,不能手工把两个包都写成 1.53.0;应保持仓库 lockfile,并用官方版本工具计算兼容包版本:
yarn backstage-cli versions:check
yarn backstage-cli versions:bump
yarn --cwd packages/app add @backstage/plugin-kubernetes
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend前端把插件路由或实体页扩展挂到批准的位置,后端加入 backend.add(import('@backstage/plugin-kubernetes-backend'))。启动后先访问后端健康与插件 API,再给一个测试 Catalog 实体添加与测试集群匹配的定位注解。官方的Kubernetes 安装说明应与当前仓库 release line 一起核对;只安装 npm 包而没有前端扩展、后端注册、cluster locator 和实体定位,不算启用成功。
把 provider host 与凭证引用写入环境配置,凭证通过进程环境、工作负载身份或密钥注入提供;配置仓库中只保留 ${GITHUB_TOKEN} 这类引用。生产更适合 GitHub App 或同类应用身份,本地临时 Token 只授予读取测试仓库所需权限并设置短有效期。
app:
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
auth:
# 保持默认认证策略,不启用危险的全局绕过开关
cache:
store: memory
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
auth:
environment: development
providers:
github:
development:
clientId: ${AUTH_GITHUB_CLIENT_ID}
clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}这里有三类不同凭证。GITHUB_TOKEN 是门户后端访问 Git 内容的集成身份;OAuth client id/secret 用于用户登录或委托流程;Backstage 自己签发的身份令牌用于前后端与插件间识别。它们不能互换,也不应拥有同样的权限。Backstage 的认证说明区分登录识别和访问第三方资源,并说明常见的 user-to-server 短期 OAuth token 模式。新后端默认要求用户或服务凭证;backend.auth.dangerouslyDisableDefaultAuthPolicy 会让未认证请求进入插件,而且新认证服务迁移说明已经提示这个兼容开关未来会移除,生产配置不应依赖它。
本地验证先启动后端与前端,登录测试用户,打开一个测试组件的仓库页。正向证据是后端以受限身份读取允许仓库,页面显示来源与更新时间,Git 审计能关联 App 或测试主体。反向验证撤销测试仓库授权或改用无权限仓库,预期插件返回明确的 403/不可访问状态,其他插件和组件页面仍可使用,日志不出现 Token。结束后撤销临时 Token,删除 OAuth 回调与测试 App 安装,清理本地缓存。
托管门户没有本地安装插件进程的步骤,但信任链不应省略:在租户中创建测试集成,选择最小组织/项目范围,导入一个非敏感实体,检查供应商审计与下游审计能否对应,然后撤销集成并验证缓存、实体和凭证删除。离线配置审查只能证明字段、权限意图和回收步骤存在;企业 SSO、付费能力、生产下游授权与撤销效果必须由目标租户的请求结果和双方审计日志证明,控制台截图不能替代这些证据。
认证只回答是谁,授权决定能做什么
用户成功登录,不代表能读取所有目录实体或执行所有动作。认证产出 principal 与 claims;授权把 principal、permission、resource 和上下文送入策略;执行点必须在插件后端按照决策拒绝请求。前端根据权限隐藏按钮只是体验优化,攻击者仍可直接调用后端 URL。
Backstage 的权限框架明确由插件声明并执行资源与动作权限,策略返回允许、拒绝或条件决策。尤其要注意:不是所有端点都会因“启用了权限框架”自动受保护。新插件接入时,应枚举每个后端 route、HTTP 方法、资源类型、permission 名和失败语义,对未声明的写端点默认拒绝。
下面的本地权限模型可直接运行。它模拟前端是否显示按钮和后端是否执行授权。反例让前端隐藏但后端放行,直接请求仍会删除资源;正例由后端按角色、Owner 和动作共同决定。
const request = { user: 'user:default/alice', roles: ['viewer'] };
const resource = { id: 'component:default/payment', owner: 'group:default/payments' };
function insecureRoute(req, body) {
// UI 是否显示按钮与此路由无关,因此直接调用会成功。
return { status: 200, deleted: body.resourceId };
}
function authorize(req, action, target) {
if (action !== 'catalog.delete') return false;
return req.roles.includes('catalog-admin') && req.groups?.includes(target.owner);
}
function secureRoute(req, body) {
if (!authorize(req, 'catalog.delete', resource)) return { status: 403 };
return { status: 200, deleted: body.resourceId };
}
console.log('negative', insecureRoute(request, { resourceId: resource.id }));
console.log('denied', secureRoute(request, { resourceId: resource.id }));
console.log('positive', secureRoute(
{ ...request, roles: ['catalog-admin'], groups: ['group:default/payments'] },
{ resourceId: resource.id },
));预期特征是 negative 返回 200,证明隐藏 UI 不能防止越权;denied 返回 403;同时满足管理员角色与资源 Owner 关系时才返回 200。真实插件还要从受信任的请求凭证提取用户,不能接受 body 里的 user/groups,并为批量查询使用条件授权,防止先拉全量再在浏览器过滤。
服务到服务调用同样需要主体。请求链中的插件调用应使用 auth.getPluginRequestToken 并通过 onBehalfOf 传递原始调用者语义,接收端用 httpAuth.credentials 验证;脱离请求链的后台同步才使用插件自己的服务凭证。用户触发写动作时,若系统选择服务身份执行,审计必须同时记录发起用户和执行服务。旧 IdentityService 和 TokenManagerService 已被核心服务索引标记为 deprecated,新增插件不要继续围绕它们设计。不要因为“请求来自门户内网”就跳过授权,内网代理、其他插件和被攻陷的任务都可能调用同一 route。
Git、CI/CD、云、Kubernetes 与监控如何分权
Git 集成优先使用 App 安装范围而不是个人 PAT。Catalog 发现只需 Metadata: read、Contents: read,读取提交状态再增加 Commit statuses: read;组织同步才需要 Members: read。模板发布可能需要仓库管理、内容、Pull Request、Workflow、变量或 Secret 的写权限,不应和发现 App 共用。Webhook secret 只证明消息来自持密钥方,不赋予 Git API 权限;三者可以由不同 App 承担。Backstage 的 GitHub App 默认只读,而且权限变更仍需在 GitHub 侧批准。缓存键包含 host、installation、repository 与授权可见性,避免把 A 组织结果返回给 B 组织用户。
CI/CD 插件的读操作包括构建结论、日志摘要和 artifact 链接;写操作包括重跑、取消、批准环境、修改变量和部署。日志常含环境名、内部 URL 和失败数据,不宜整段复制进门户缓存。重跑必须绑定 pipeline/run ID、提交 SHA 和幂等键,先确认用户对目标仓库与环境有权;部署还要把制品 digest、目标环境和审批记录锁定在同一个任务里。门户不应代替 CI 自身的环境审批,更不能用一个管理员 Token 绕开保护规则。高风险写动作走独立连接器身份和并发池,读状态的凭证即使泄露也不能批准生产部署。
云插件使用工作负载身份、角色承担或短期联合凭证,避免静态 AK/SK。AWS IAM 最佳实践建议工作负载使用 IAM Role 临时凭证;Azure 优先 Managed Identity;Google Cloud 可用 Workload Identity Federation替代服务账号密钥。每个连接器把 issuer、audience、subject、目标 role 和 session duration 固定下来,不能只校验“来自公司 OIDC”。资源发现按账号、项目、region 和标签限制,成本读取与资源写入拆分角色,组织管理账号不承担日常采集。云 API 返回的网络、标签和策略可能泄露拓扑,Catalog 页面按数据分类裁剪。自助动作创建云资源时把审批、预算、策略和实际执行留在受控工作流,门户只持有触发与读取状态所需权限。
Kubernetes 插件由前端插件与后端插件共同组成,官方Kubernetes 插件说明给出这个分层。认证策略中的服务端 provider 会让用户共享后端身份,客户端 provider 则让用户受自己的集群权限约束;认证策略文档明确两类策略的差异。共享 ServiceAccount 只适合严格只读、按命名空间 RoleBinding 受限的观察,不能把 cluster-admin 暴露给所有门户用户。Kubernetes 1.22 起 Pod 默认通过 TokenRequest 获得短期、自动轮换的 projected token;把长期 ServiceAccount Secret 复制进 app-config 会失去这套生命周期,Backstage 集群配置文档也明确提示这种方式通常不适合生产。缓存键必须包含 cluster、namespace、resource、identity scope,错误信息也不能列出用户无权知道的命名空间。
下面三条命令由具备 impersonate 权限的集群管理员执行,能同时证明正向读取和反向拒绝;它们不产生资源,因此无需清理。将命名空间和 ServiceAccount 替换为测试值,预期依次为 yes、no、no。若第二或第三条返回 yes,先收紧 RoleBinding,再让门户连接集群。
kubectl auth can-i list pods \
--as=system:serviceaccount:backstage:backstage-reader -n payments
kubectl auth can-i delete secrets \
--as=system:serviceaccount:backstage:backstage-reader -n payments
kubectl auth can-i list pods \
--as=system:serviceaccount:backstage:backstage-reader -n kube-system监控连接器默认只读查询告警、SLO 和有限时间窗内的聚合指标,不授予规则修改、静默所有告警、数据源管理或删除时序数据的权限。Grafana、Prometheus、Datadog 等系统的 service account/API token 各有自己的组织和数据源边界,门户后端不能把 token 发给浏览器,也不能接受任意 PromQL、日志查询或 dashboard URL 后直接代理。查询模板应固定数据源、时间范围、最大序列数和超时;带用户名、仓库名或请求 ID 的高基数标签先聚合再展示,防止一张组件页拖垮监控后端。
文档插件分为构建与读取。TechDocs 或同类系统从仓库取源、在 CI 或后台生成制品,再从对象存储提供页面。构建身份只读源仓库并写对应前缀,读取身份只读允许的文档制品;私有仓库文档不能因发布到公共 bucket 而变公开。构建日志、搜索索引和页面 HTML 都可能包含内部 API、架构与值班信息,应继承源实体可见性并有删除链路。
Webhook 负责低延迟,轮询负责补洞
只做轮询,变更发现延迟和 API 配额会互相拉扯;只做 webhook,入口短暂不可用、事件过滤错误和供应商不重投都会留下永久缺口。Backstage 的 GitHub Catalog provider 可以订阅 github.push、github.repository 等主题,但还必须安装 Events 后端与 GitHub 路由模块,显式开放 github topic 并配置签名密钥。GitHub App 创建向导里的 webhook 默认关闭,Backstage 也不会因为配置了 GitHub App 就自动消费 webhook。
yarn --cwd packages/backend add \
@backstage/plugin-events-backend \
@backstage/plugin-events-backend-module-github \
@backstage/plugin-catalog-backend-module-githubbackend.add(import('@backstage/plugin-events-backend'));
backend.add(import('@backstage/plugin-events-backend-module-github'));
backend.add(import('@backstage/plugin-catalog-backend-module-github'));events:
http:
topics: [github]
modules:
github:
webhookSecret: ${GITHUB_WEBHOOK_SECRET}GitHub 侧把 Payload URL 指向 https://<portal-host>/api/events/http/github,只订阅 Catalog provider 实际消费的事件类型。入口必须位于受保护的公网或专线边界,不能为了接 webhook 把其他 /api/* route 一起匿名暴露。
入口收到请求后先限制 body 大小和来源速率,再用原始请求体和 X-Hub-Signature-256 校验 HMAC-SHA256,提取 X-GitHub-Delivery 作为幂等键,持久化接收时间、事件类型和 payload 摘要后立即应答。消费端以 delivery ID 去重,按资源版本、提交 SHA 或供应商时间判断新旧;“重复事件返回成功但不重复执行”和“旧事件不能覆盖新状态”是两个不同的验收点。GitHub 对失败 webhook 不会自动重投,供应商只保留有限的可重投窗口,因此平台必须有补偿轮询或主动查询失败 delivery 的任务。
下面的正反实验不需要真实仓库写权限。先在测试 App 的 Recent deliveries 选择一条无敏感内容的 push,记录 delivery ID;让测试入口正常返回 2xx,再对同一 delivery 执行 redeliver。GitHub App webhook 重投 API需要由 App ID 与私钥签发的 JWT,不能使用 installation token:
curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${APP_JWT}" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"https://api.github.com/app/hook/deliveries/${DELIVERY_ID}/attempts"预期 HTTP 状态是 202,接收计数增加两次,但业务更新计数只增加一次,审计第二次标记 duplicate。反向实验把 ingress 暂时改为返回 503 或把 secret 改错,预期供应商记录失败、门户业务状态不更新;恢复 secret 后手工 redeliver,同时触发一次 provider 全量轮询,最终 source SHA 收敛且只产生一次有效变更。GitHub 不会自动重投失败 delivery,当前只允许重投过去三天内的记录,因此补偿任务不能拖到事故复盘后才运行。结束后恢复原 secret,删除测试 webhook/App 安装、测试实体和接收记录,并确认旧 URL 返回 404 或受控拒绝。
轮询任务使用 Backstage Scheduler 时,scope: global 让同一插件的多个副本一次只执行一个任务,frequency 决定期望间隔,timeout 到期后任务才可能被其他 worker 接管。调度器 REST API 能查看 lastRunError、lastRunEndedAt、下次运行时间并手工触发任务;这不是业务消息队列,也不自动提供任意事件的持久化、逐条确认和死信。高吞吐 webhook 应落到 SQS、Pub/Sub、Kafka 或同类持久队列,再由 Backstage Events 模块消费。
队列采用至少一次语义时,消费者顺序必须是“读取 -> 校验 -> 幂等更新 -> 提交业务事务 -> ack”。失败按错误类型处理:429、超时和 5xx 带抖动退避;签名失败、schema 不兼容和永久 403 进入隔离队列,不无限重试。重放工具只能按 topic、时间窗、资源和失败原因筛选,先 dry-run 统计影响量,再限速重放;每次记录操作者、原因、原消息 ID、新 attempt ID 和结果。重放不能绕过当前授权,也不能直接把旧 payload 覆盖当前状态。
received_total{topic,result} 接收、签名失败、重复事件数
consumer_lag_seconds{topic} 最老未确认消息年龄
retry_total{topic,reason} 可恢复错误重试数
dead_letter_total{topic,reason} 隔离队列消息数
replay_total{topic,result} 人工或自动重放结果
reconcile_drift_total{provider} 轮询发现的事件遗漏数队列深度只说明“有多少”,最老消息年龄才说明用户看到的数据落后多久。告警同时看年龄、处理速率和下游错误;插件降级时保留 Catalog 核心信息,在卡片上明确显示 stale 与最后成功时间,暂停写动作,不能用旧的绿色结论继续允许部署。
Token 轮换从双凭证窗口开始
凭证清单至少记录类型、下游、权限范围、Owner、存储位置、签发方式、过期时间、最后使用、轮换方式和撤销证据。个人 Token、OAuth client secret、Git App private key、Webhook secret、云角色信任、Kubernetes ServiceAccount token、监控 service account token 和数据库密码有不同轮换语义,不能用“更新环境变量然后重启”覆盖所有情况。GitHub App installation token按安装范围签发且一小时后过期,应用私钥才是需要严密轮换的根凭证;工作负载联合身份通常轮换信任和短期 token,不应退回长期云密钥。
支持多活密钥的系统采用双凭证窗口:创建新凭证,部署引用,观察新凭证成功使用,再撤销旧凭证。只支持单 secret 的 webhook 可能需要下游同时接受新旧值、短暂双端配置或维护窗口。轮换的关键证据不是新 secret 已写入密钥库,而是各副本都使用新版本、旧凭证调用归零并最终撤销。
T0 盘点旧凭证 fingerprint、使用方与权限,不读取明文
T+1 签发新凭证,权限不高于旧凭证,写入新版本
T+2 灰度一个门户后端副本,观察认证成功与下游审计
T+3 滚动全部副本,确认新 fingerprint 使用覆盖稳定
T+4 禁用旧凭证,观察失败率与缓存刷新任务
T+5 撤销旧凭证并关闭回退窗口,保存撤销事件轮换失败常见于长生命周期 worker、连接池、缓存预热器和定时任务仍持有旧值。现象是大多数请求成功,周期任务间歇 401。按凭证 fingerprint、工作负载版本和实例维度聚合失败,找到仍未重载的进程;修复后旧 fingerprint 的使用计数应归零。日志只能记录 fingerprint 或 key id,不能记录 secret。
紧急泄露处置顺序是先限制影响面:撤销或禁用凭证、暂停高风险写 route、阻断异常出口,再轮换与恢复。随后查询下游审计、门户访问日志、任务记录、缓存和错误追踪,确认是否被使用及复制到哪里。只删除配置库中的旧值无法消除日志、镜像层、CI artifact 和截图中的副本。
缓存必须表达陈旧与权限
门户依赖多个慢而有限流的 API,不加缓存会把每次页面打开放大为 Git、CI、云和集群的请求风暴;缓存设计错误则会把陈旧或越权结果包装成实时事实。每条缓存记录应包含 source、fetchedAt、expiresAt、sourceVersion/etag、scope、status 与 payload 摘要。UI 同时显示采集时间和状态:fresh、stale、unknown、forbidden、error 不能合并成一个灰色横杠。
TTL 由业务变化速度、下游配额和错误容忍推导。仓库描述可较长,正在运行的流水线要短,部署告警可能需要事件推送。stale-while-revalidate 能改善延迟,但超过最大陈旧窗口后应展示未知,不继续显示绿色。下游 403 不能回退到过去的成功缓存,因为权限可能刚被撤销;404 也要区分资源删除、权限隐藏和发现延迟。
限流先尊重供应商返回的 Retry-After、剩余额度和 reset 时间,再做指数退避与随机抖动;不能把 403 一律当权限错误,因为 GitHub REST API 限流也可能用它表达配额或滥用保护。按 host、installation/tenant、插件和操作类型分桶,交互查询、后台同步、Webhook 补偿和写动作各有预算。接近配额时暂停预热和全量发现,合并相同请求并延长只读缓存;写动作没有可证明的新鲜前置状态时应失败关闭,不能排队到几个小时后悄悄执行。
缓存隔离至少包含租户/组织、用户或授权 scope、资源身份、插件版本和响应 schema 版本。若授权发生在读取后,不能把全量敏感响应放进共享缓存再由前端过滤。部署新插件版本改变缓存结构时使用版本化前缀,先双读或重建,回滚时仍能读取旧格式;不要让新旧副本互相覆盖不可兼容值。
正反实验可以用一个短 TTL 测试实体:先授权用户读取并缓存,随后在下游撤销权限。正向实现下一次请求重新授权并返回 403,缓存命中不绕过策略;反向实现若仍显示旧数据,即证明授权与缓存顺序错误。证据包括策略调用计数、缓存 key scope、下游审计和页面的 fetchedAt,而不是肉眼刷新一次。
插件隔离与供应链
插件是运行在高信任门户中的代码,不是普通主题包。安装前记录来源仓库、维护者、许可证、发布签名或摘要、依赖树、需要的 route、网络出口、数据库迁移、权限与数据字段。社区插件停止维护、依赖出现漏洞或 Backstage API 变化时,平台团队必须能禁用并替换,而不是因页面依赖而被锁死。
隔离分四层。进程层把高风险或高负载插件拆成独立 backend module/服务,设置 CPU、内存、连接池和超时;网络层只允许访问声明的下游 host,阻止任意内网探测;数据层使用独立数据库 schema、缓存前缀和对象存储前缀;权限层为插件服务身份分配最小角色。某个云插件内存泄漏或 API 卡住时,不应拖垮登录、Catalog 与其他插件。
后端 route 使用统一认证中间件、请求大小限制、超时、重试预算和结构化审计。插件不能接收任意 URL 由后端代请求,确需代理时使用 host allowlist、DNS/IP 校验与响应大小限制。错误响应不回传下游原始 header,避免泄露 Token、Cookie、内部主机名和供应商 request dump。
升级插件前在隔离环境恢复生产形态的脱敏数据,执行 route 授权正反测试、数据库迁移、缓存兼容、依赖扫描和页面回归。新版本先投少量副本,观察按插件分解的错误、延迟和资源占用。回滚要考虑数据库 migration 是否向后兼容;不可逆迁移必须先扩展 schema,让新旧版本共存,再清理旧列。
用 SLO 和容量预算管理聚合故障
门户 SLO 不应只有首页可用率。至少分为:登录与身份、Catalog 查询、组件页核心信息、插件卡片、搜索、模板写动作和后台同步。一个云成本 API 故障可以让该卡片降级为 stale,而不应把组件页整体判死;身份和 Catalog 故障则是核心路径。按依赖关键性定义错误预算和降级策略,避免所有插件共享一个总指标。
建议观测 RED 与队列状态:每个 route 的请求率、错误率、延迟;每个下游的调用、剩余额度、限流、超时和断路;缓存命中、陈旧年龄与刷新失败;数据库连接与慢查询;同步队列年龄、积压、重试、死信和重放;前端按插件的加载失败。指标标签不能使用仓库全名、用户邮箱或任意 URL,防止高基数与敏感信息进入监控。Backstage Metrics Service 在当前稳定文档中仍是 alpha,采用它时要把 exporter 和 dashboard 契约隔离在适配层,升级前验证指标名与属性变化。
容量模型从页面扇出开始。一次组件页若触发 8 个插件,每插件 2 个 API,请求峰值会被放大 16 倍;后台发现与缓存预热还会争抢同一配额。用实体数、活跃用户、页面访问率、每页扇出、缓存命中和下游 rate limit 估算预算,再设置并发池、批量 API、抖动刷新和优先级。交互请求优先于全量同步,高风险写动作使用独立池,避免读流量耗尽执行槽。
成本包括门户计算与数据库、缓存、搜索索引、对象存储、日志、出口流量、下游 API 额度、SaaS 座席和插件商业许可。按插件和实体类型分摊,观察“每活跃用户成本”“每组件页下游调用”“每成功自助动作成本”。某插件使用率很低却需要高权限、高维护和昂贵索引,应考虑改为深链接或按需查询。
排障从依赖分层而不是刷新页面开始
“整个门户打不开”先分 DNS/TLS/Ingress、前端静态资源、后端健康、身份提供方、数据库与 Catalog。浏览器拿不到静态资源与登录回调循环是不同故障;后端健康但数据库连接耗尽时,检查连接池与慢插件;身份提供方故障时,可保留无敏感内容的状态页,但不能全局关闭认证绕过。恢复后从登录、Catalog 查询、单插件卡片逐层验证。
“只有 Kubernetes 卡片报错”先看实体定位信息、cluster/namespace 映射、认证策略、后端权限和集群 API。401 多为凭证失效,403 是身份有效但 RBAC 拒绝,空结果可能是标签选择器或实体关系错误,超时可能是代理、CA 或集群不可达。不要把所有错误改成“暂无资源”。修复后用有权限与无权限用户各测一次,并确认其他 cluster 不受影响。
“页面仍显示旧构建成功”检查 fetchedAt、TTL、刷新队列、CI webhook 和 source version。刷新任务成功但值未变,可能缓存 key 不含分支或提交;刷新一直失败而 UI 仍绿色,是最大陈旧窗口缺失;Webhook 到达但未更新,检查签名、事件过滤和乱序处理。修复后推送一个测试提交,预期 source SHA 与状态按顺序变化,旧事件不能覆盖新事件。
“部分用户看到不属于自己的实体”立即暂停相关 route 或禁用插件,保存审计并检查缓存 scope、条件授权和批量 API。常见根因是以 URL 为 key 的共享缓存、后端先全量查询再前端过滤、服务身份绕过用户策略。修复后构造两个互斥权限用户,交替访问同一进程与缓存,任何响应都不能包含对方实体的 ID、数量或错误提示。
升级、备份与恢复必须一起演练
门户升级包含应用框架、Node 运行时、插件、配置 schema、数据库 migration、搜索索引和文档构建链。锁文件与镜像 digest 固定依赖,变更记录列出插件 API 和配置迁移。先升级测试环境并恢复脱敏备份,执行认证、授权正反、目录读写、模板、关键插件和缓存重建;生产采用滚动或蓝绿,确保新旧版本对数据库和 token 格式兼容。
Backstage 可以作为无状态应用配外部 PostgreSQL 运行在 Kubernetes;官方Kubernetes 部署指南展示了基本形态。但“Pod 无状态”不代表门户无状态:Catalog、Scaffolder task、插件表、权限配置、搜索/缓存、文档制品、密钥引用和身份提供方配置共同构成恢复对象。数据库与对象存储分别备份,配置和插件清单由 Git 保存,密钥只备份可恢复的引用与加密材料,不导出明文到普通备份。
恢复演练先在隔离网络创建空环境,恢复数据库与文档制品,注入测试凭证,重建缓存和索引,再验证实体数量、关系、最近 task、关键文档和授权。恢复时间证据是从空环境到核心路径可用,恢复点证据是可接受窗口内的目录与任务记录;两者由业务 SLO 决定,不使用脱离规模的万能数字。备份成功日志不能代替实际 restore。
回滚应用版本前确认 migration 向后兼容、旧插件能读取新 token/缓存格式。若不能,前滚修复通常比二进制回退安全。升级窗口内保留旧镜像、旧配置摘要、数据库快照与流量切换入口;回滚后执行一次登录、Catalog 查询、权限拒绝和关键插件读取,防止“页面能开”掩盖授权或数据损坏。
审计让一次点击能追到下游
审计事件应包含 actor、subject/service principal、action、resource、decision、policy version、request/task id、result、reason、source IP/客户端类别与下游 request id。读敏感字段、导出、写动作、权限策略变化、插件安装、凭证轮换、Webhook 重放、缓存管理和备份恢复都需要记录。拒绝事件同样重要,它能暴露扫描、错误集成和策略漂移。Backstage Catalog 和 Scaffolder 已能通过 Auditor Service 产生标准事件,但低严重度 Catalog 读取默认映射到 debug,在默认 info 日志级别下看不到;合规要求记录读取时要调整 backend.auditor.severityLogLevelMappings,并评估日志量与隐私成本。
审计数据本身敏感:实体关系、仓库名、集群和失败原因可能暴露组织拓扑。按职责限制查询,设置保留与防篡改策略,导出经过审批;Token、Cookie、文档正文和云 API 原始响应不进入审计。请求 ID 从浏览器穿过门户、插件到下游,使值班人员能把“用户点了重跑”与 CI 审计中的具体 run 对应。
定期做权限回归:匿名、普通成员、资源 Owner、平台管理员和插件服务身份分别访问同一组 route,验证允许与拒绝矩阵;再检查下游实际权限没有比门户策略更宽。策略代码变更进入评审与自动测试,紧急放行带过期时间和 Owner。只审查 UI 菜单可见性无法发现后端越权。
清理、迁移和退出时带走运行事实
停用单个插件先禁止新写动作,再暂停 webhook ingress 与后台拉取,等待运行请求和队列收敛;导出仍需保留的映射与审计,先在下游删除 webhook/订阅,再撤销 App、云角色、ServiceAccount 和监控 token,最后删除 OAuth callback、缓存、插件表与文档制品。Catalog 实体若仍由 parent location 或 entity provider 管理,直接删除实体只会被下次刷新重新创建,必须先移除权威来源再删除投影。最后用旧凭证请求应返回拒绝,旧 webhook URL 应拒绝请求,队列与定时任务不再增长,下游审计不再出现插件主体,门户页面明确移除卡片而不是永久显示陈旧成功。
退出整个平台时先建立资产清单:Catalog 实体与关系、字段权威、模板与任务、插件配置、权限策略、身份映射、审计、文档制品、缓存/索引可重建规则、备份和所有下游凭证。可重建数据不必迁移全部历史缓存,但权威元数据、运行中任务和授权语义不能丢。托管平台还要确认数据驻留、导出格式、保留期、删除证明和审计访问窗口。
迁移采用双读对账:新平台以只读方式导入实体,对比数量、稳定 ID、Owner、关系、生命周期和关键证据;再迁移插件与权限,使用互斥用户做正反测试;最后切换写动作和 webhook。旧平台进入只读,直到新平台 SLO 稳定、审计可关联、凭证已切换且恢复演练完成。若产品能力或合同发生变化,能否导出实体、策略、任务和审计,比页面组件多少更能决定真实退出成本。
门户长期可信依赖明确的共同责任:平台团队负责框架、身份、权限执行、插件供应链、SLO 与恢复;集成 Owner 负责下游 API、凭证与字段语义;服务 Owner 负责实体和文档事实;安全与合规团队定义敏感字段、审计和保留。每个插件都有 Owner、数据分类、权限清单、SLO、成本、升级节奏和停用条件,聚合入口才能在规模增长后仍是一张可验证的地图,而不是一层更漂亮的未知状态。
