Backstage:把服务目录、软件模板与权限执行连成可恢复平台
告警响起时,值班同学在门户里找到 payment-api,页面写着 Owner 是支付组;支付组却说这个仓库半年前已经移交,运行手册仍指向旧域名,创建新服务的模板还在发放旧版流水线。更糟的是,平台团队刷新页面后看到的仍是“绿色”。这不是页面缓存的小毛病,而是目录把“能展示一条记录”误当成了“记录有稳定身份、可靠来源和可验证处理状态”。
Backstage 的价值也不只是把链接收进一个首页。它是一套可组装的开发者门户框架:Software Catalog 把仓库、团队、API、系统和资源变成有关系的实体;Scaffolder 把创建仓库、生成骨架、登记目录等动作编排成任务;认证系统建立调用者身份;权限框架作出决策,再由各插件后端执行;插件把 CI、Kubernetes、文档和观测证据挂到实体上。只要其中任意一环没有责任人,门户就可能比没有门户更危险,因为它会把过期信息包装成权威答案。
当前稳定主线是 Backstage 1.53.0,该 release 是一组经组合验证的 npm 包版本,并不等于每个包都使用相同版本,也不遵循平台级 semver;next 是预发布线,不能直接当生产稳定线。Backstage 源码采用 Apache License 2.0,CNCF 将它列为 Incubating 项目。许可证允许企业自行修改和商用,但开源项目交付的是框架与社区插件,不附带托管租户、厂商 SLA、实施支持、商业 RBAC 套餐或合规背书;这些能力如果来自第三方发行版或托管服务,应以该供应商合同、数据处理条款、插件清单和退出能力为准,不能写成 Backstage 核心承诺。
从创建器跑起第一个实例
Backstage 更像由大量 npm 包组装出的内部产品,而不是下载后直接配置的成品。官方独立安装入口是 @backstage/create-app 创建器。1.53.0 稳定线支持 Node.js 22 与 24;动手前还要确认 Corepack、Git、Docker、原生编译工具和磁盘空间。官方入门文档要求 Unix 类环境,Windows 开发机优先放在 WSL 中。Node 支持线会随 Backstage release 变化,不要从旧教程抄版本,先看 独立安装文档 与 发布和版本策略,再用生成项目的 package.json#engines、backstage.json、.yarnrc.yml 和锁文件确定真正基线。
node --version
corepack --version
docker version
npx @backstage/create-app@latest
cd my-backstage-app
cat backstage.json
cat .yarnrc.yml
corepack yarn install
corepack yarn start创建器会询问应用名,并生成 packages/app、packages/backend、app-config.yaml、catalog-info.yaml 和示例实体。yarn start 同时启动前端与后端,默认前端在 http://localhost:3000,后端在 http://localhost:7007。可接受的启动证据不是“浏览器出现页面”这一项,而是前端编译成功、后端监听端口、catalog、scaffolder、auth、permission 等插件完成初始化,且没有持续重启。
创建器带有 --skip-install,适合只查看模板或由 CI 统一安装依赖,但它改变了验证顺序:刚生成的种子锁文件可能需要解析更新,此时直接执行 yarn install --immutable 会以“lockfile would have been modified”失败。先用普通 yarn install 形成并评审锁文件,提交后再让 CI 使用不可变安装,才是可重复构建。一个新模板解析两千多个传递包、占用 GiB 级缓存并不罕见,平台团队应提前准备 npm 代理、缓存容量、原生模块编译环境和依赖扫描,而不是等容器构建超时后再补。
本地默认使用内存 SQLite 和 guest 登录,进程退出后状态消失。它适合理解对象与链路,不能成为共享环境。app-config.yaml 中这些字段会直接改变行为:
app.baseUrl 是浏览器访问入口,backend.baseUrl 是后端公开地址;反向代理后仍写 localhost 会造成 OAuth 回调、资源链接和 CORS 错误。backend.cors.origin 决定浏览器源,不能为了省事写任意来源并同时允许凭证。backend.database 决定各插件持久化位置;:memory: 意味着重启即丢失目录处理状态、任务与签名材料。
integrations.github 等集成配置决定 Catalog 能否读取私有仓库、Scaffolder 能否发布;Token 应由环境变量或密钥系统注入。catalog.rules 是实体类型准入规则;显式配置后会替换默认规则,rules: [] 会拒绝所有实体。catalog.locations 是静态入口,catalog.readonly: true 会禁止通过 Catalog API 注册和删除 Location,也会让依赖这些写接口的导入流程失效。
catalog.processingInterval 是建议的最小再处理间隔,不是精确刷新 SLA;负载高时实际间隔会更长,并带有抖动以分散压力。permission.enabled 只是启用权限链路,不代表策略已经正确,也不代表每个第三方插件都执行了资源级检查。
Catalog 实体不是一行导航数据
一个实体的稳定身份由 kind、metadata.namespace、metadata.name 组成,常写成 component:default/payment-api。metadata.uid 是 Catalog 接收后生成的实例标识,不应写进仓库,也不适合作为跨环境业务主键。名字、命名空间或 kind 一变,Catalog 看到的是新身份;如果旧来源仍在,它就会同时保留两个实体。因此,团队重命名服务时必须把仓库、关系、告警路由和退役动作放在同一个变更计划里。
catalog-info.yaml 是最常见的仓库侧权威入口。下面的组件同时声明了可读身份、Owner、生命周期、所属系统、依赖 API 和文档位置:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: payment-api
namespace: default
title: Payment API
description: Handles payment authorization requests
annotations:
backstage.io/techdocs-ref: dir:.
github.com/project-slug: your-org/payment-api
tags:
- java
- payments
spec:
type: service
lifecycle: production
owner: group:default/payment-platform
system: checkout
providesApis:
- payment-http-api
dependsOn:
- resource:default/payment-db
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: payment-http-api
namespace: default
spec:
type: openapi
lifecycle: production
owner: group:default/payment-platform
system: checkout
definition:
$text: ./openapi.yamlspec.type 是组织自定义分类,不会因为写成 service 就自动获得运行能力;spec.lifecycle 也不是状态机,production 只是团队约定的枚举。spec.owner 必须能解析到真实的 Group 或 User 实体,否则页面可能显示一个字符串,却无法建立成员关系和问责链。providesApis、dependsOn、system 会在处理阶段转成关系;关系目标暂时不存在时,源实体仍可能进入目录,所以“页面可见”不等于关系完整。
注解是插件接入的常用键,也最容易失控。仓库 slug、集群定位、文档引用、值班标识都可能暴露内部拓扑。平台团队应维护注解注册表:每个键的生产者、消费者、敏感级别、格式、弃用替代项和 Owner 都明确;禁止插件各自发明同义键,更不要把 Token、连接串、漏洞明细或客户标识写入实体。Catalog API、搜索索引、日志、插件缓存和备份都可能复制这些字段。
实体从发现到可查询经历什么
浏览器能查询到的不是仓库里的原始 YAML,而是处理与 stitching 后的最终实体。Provider 负责发现并提交原始实体,Policy 设定基础形状约束,Processor 读取、解析、验证、变换并发出关系、错误或子实体,stitching 再把这些辅助数据组装成最终视图。Catalog API 默认暴露最终实体;某个 YAML 已经被 Provider 发现,却可能因为排队、读取失败或处理错误而尚未出现。
Provider 有稳定名称,并在数据库中拥有自己输出的实体集合。两个 Provider 不能随意接管同一个实体引用;locationKey 用来约束来源冲突。相同 kind/namespace/name 从另一位置出现时,若来源键不同,新定义不会静默覆盖旧定义。这一机制阻止“随便注册一个同名 YAML 就劫持服务”,也解释了为什么迁移发现方式前要先设计来源交接,而不是同时打开两个 Provider 后期待后者自然获胜。
处理队列中的实体带有下一次处理时间。catalog.processingInterval 到期只是进入竞争资格,多个 Catalog 实例协作领取任务,负载与抖动会拉长实际刷新时间。手动 Refresh 的正确含义是把目标实体尽快重新排入处理,而不是绕开 Git、直接修改最终实体。刷新后要观察处理状态、错误变化和最终实体版本;只看到 HTTP 成功码不能证明 YAML 已被重新读取并 stitch 完成。
Provider 不再发出根实体时,可以触发实体及其派生数据的快速删除;Processor 不再发出某个子实体时,子实体可能先被标记 backstage.io/orphan: 'true',等待清理。对仍由活动 Location 持续供给的实体直接调用按 UID 删除,实体会在后续处理后重新出现。真正退役应从权威源移除定义或注销对应 Location,再确认 orphan、最终删除和下游索引都收敛。Catalog API 的行为与删除注意事项可对照 Software Catalog API。
用正反实验看见处理链
先运行创建器生成的示例,不改任何业务仓库。启动日志中应看到 Catalog 数据库迁移和插件初始化;浏览器访问 /catalog 应出现 Component、API、Group、User、Template 等示例。后端默认认证策略会拒绝无凭证调用,因此直接请求可以先构造反例:
curl -i 'http://localhost:7007/api/catalog/entities/by-query?limit=20'预期证据是 HTTP/1.1 401 Unauthorized,响应体指出缺少凭证。这个 401 证明后端路由要求身份,不证明登录用户拥有读取所有实体的权限。浏览器通过 guest provider 获得短期 Backstage 身份后再查询,响应应包含 items,并在结果超过当前页时提供继续查询所需的分页信息;生成模板的演示数据通常能看到 Component、API、Group、User 和 Template。共享环境必须把 guest provider 换成企业身份源,并将用户解析到 Catalog 中的 User 与 Group,否则“谁登录了”与“属于哪个 Owner 团队”仍然断开。
然后把一个最小组件放到可访问的 Git 仓库,以完整文件 URL 从 /create 的 Register Existing Component 登记。先用正确实体:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: catalog-lab-service
namespace: default
spec:
type: service
lifecycle: experimental
owner: group:default/platform成功判据是 Location 注册成功,处理状态无错误,component:default/catalog-lab-service 可由按名称 API 查询,并且 Owner 目标存在。接着做两个稳定反例:第一,把 kind 改为规则不允许的类型,预期 Location 或实体处理出现 rule 拒绝;第二,把 YAML 缩进破坏,预期实体留在失败处理状态,最终实体不应继续以新内容出现。不要只刷新 Catalog 列表,要在 Unprocessed Entities 页面或 DevTools 中查看 failed/pending 与原始实体。该页面需要前端 @backstage/plugin-catalog-unprocessed-entities 和后端 @backstage/plugin-catalog-backend-module-unprocessed;它还定义了删除未处理记录的权限,不能只装前端包。
修复 YAML 后提交 Git,触发 Refresh,再确认失败记录消失、实体重新可查、关系目标解析正确。最后从权威 Location 移除实验实体,观察 orphan 或删除结果,并确认搜索中也不再命中。这样一次实验同时证明了发现、处理、错误保留、刷新、stitching 与清理,而不是只证明 YAML 能被解析。
发现规模决定刷新方式和成本
十几个仓库可以手工注册 Location;几百上千个仓库继续依靠人工,会迅速出现漏登记、重复来源和无人维护。组织通常使用代码托管 Provider 按组织、主题或仓库规则发现 catalog-info.yaml。Provider 的过滤条件是容量控制面:规则过宽会产生大量 API 请求、处理任务和数据库写入;规则过窄会让新服务永远不进目录。先在影子环境记录发现仓库数、未处理实体积压、单轮耗时、远端 API 限流和错误分类,再决定并发与调度。
processingInterval 不能当成“元数据最多延迟多少分钟”的承诺。实际新鲜度由 Provider 调度、代码托管 API、处理队列、Processor 外部调用和 stitching 共同决定。更可靠的观测指标是:待处理实体最老年龄是否持续上升,同一轮之后积压能否回落,处理成功与失败按 Processor 原因如何分布,手动 Refresh 到最终实体变化的端到端延迟是否稳定。生产阈值应由实体规模、外部限额与更新 SLO 测得,不使用脱离负载的万能秒数。
多实例部署能协作处理任务,但自定义 Processor 必须能承受并发、重试和重复执行。Processor 适合做确定性解析与关系生成,不适合每轮同步请求慢速系统并写副作用。如果确需外部证据,使用有超时、缓存、限流和来源时间戳的客户端;失败时保留“未知/过期”,不要沿用上次绿色结果冒充实时状态。
Scaffolder 任务要按可补偿工作流设计
Software Template 是 Catalog 中的 Template 实体。参数 schema 生成表单,Scaffolder 后端按顺序执行 action,每次执行形成独立 task ID,并保存步骤状态和日志。创建仓库、写入文件、登记 Catalog 的快乐路径很短,真正的风险发生在第二个外部副作用已经完成、第三步失败时:重试可能再建一个仓库,取消任务也不保证当前 action 立刻停止。
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: create-service
spec:
owner: group:default/platform
type: service
parameters:
- title: Service identity
required: [name, repoUrl, requestId]
properties:
name:
type: string
pattern: '^[a-z][a-z0-9-]{2,48}$'
repoUrl:
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts: [github.com]
allowedOwners: [your-org]
requestId:
type: string
description: Stable idempotency key supplied by the caller
pattern: '^[a-zA-Z0-9-]{8,64}$'
steps:
- id: fetchBase
name: Render source
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
- id: publishRepo
name: Publish repository
action: publish:github
input:
repoUrl: ${{ parameters.repoUrl }}
repoVisibility: internal
- id: registerEntity
name: Register catalog entity
action: catalog:register
input:
repoContentsUrl: ${{ steps.publishRepo.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
- id: compensateRepo
name: Compensate partially created repository
action: yourOrg:github:deleteRepo
if: ${{ failure() }}
input:
repoUrl: ${{ parameters.repoUrl }}
requestId: ${{ parameters.requestId }}
- id: auditResult
name: Emit completion audit
action: yourOrg:audit:write
if: ${{ always() }}
input:
requestId: ${{ parameters.requestId }}步骤 ID 使用 camelCase,避免表达式把连字符当减号。failure() 只在前序失败后运行,always() 无论成功失败都运行;普通 truthy 条件在失败后不会继续执行。补偿 action 还要幂等:目标已不存在返回成功,目标已被接管则拒绝删除并转人工,不允许用仓库名模糊匹配。requestId 或业务幂等键应在创建前检查,重复执行要返回原资源或明确冲突,不能默默创建 service-2。
取消语义同样要验证。Scaffolder 会发送 abort signal,并跳过后续普通步骤;当前 action 只有实现了取消处理才会停止。自定义 action 访问云 API 或等待 CI 时,应把 abort signal 传给客户端,设置超时,在 finally 释放临时目录与句柄,并把外部资源 ID 持久化到可供补偿读取的位置。任务日志不得打印 Token、模板中的密码字段、生成后的 Secret 或完整响应头。
正向验证应使用无生产权限的测试组织:输入合法名称,确认每步状态、仓库内容、catalog-info.yaml、最终实体和输出链接一致。反向验证在 publishRepo 后注入确定性失败,确认 registerEntity 被跳过、compensateRepo 执行、仓库已删除或进入人工补偿队列、再次以同一幂等键执行不会泄漏第二份资源。官方模板的任务、失败、取消与状态函数语义可在 Software Templates 和 Writing Templates 中逐项核对。
登录成功不等于动作已授权
Backstage 认证同时服务于用户登录和第三方资源访问。登录提供方把外部身份解析成 user:default/alice 一类 Backstage 主体;Catalog 中的 User/Group 关系再提供团队归属。错误的 sign-in resolver 可能把两个外部账号合并成同一用户,或者让已离职人员仍匹配旧实体。上线前必须用同名、改名、禁用和跨租户账号做反例,确认解析是拒绝而不是猜测。
新后端系统的默认认证策略要求请求带用户或服务凭证;backend.auth.dangerouslyDisableDefaultAuthPolicy: true 会关闭这层保护,而且官方迁移说明已声明该逃生开关未来会移除,不应成为生产依赖。这个默认策略保护后端请求,不保护静态前端 bundle;官方威胁模型仍假设外部用户不能直接访问 Backstage,入口代理、网络边界和实验性的 public entry point 需要单独评估。服务到服务调用使用 Backstage 服务凭证,而不是共享个人 PAT。GitHub、GitLab、云和 Kubernetes 凭证则属于第三方集成,应优先使用 App、工作负载身份或短期令牌,按读 Catalog、创建仓库、删除补偿等动作拆分权限。轮换后验证旧凭证失效,离职回收同时覆盖身份源、代码托管应用、密钥系统与审计导出。
身份通过也不等于授权通过。权限框架不会自动给所有插件端点补上细粒度保护:插件要声明 permission,中央 policy 返回允许、拒绝或条件决策,拥有资源数据的插件后端再负责执行条件。没有接入 permission 的端点,在通过默认身份检查后仍可能对所有已认证用户开放。前端隐藏按钮只是体验优化,攻击者仍可直接请求 API。每个高风险动作都要沿后端路由检查:谁声明 permission,路由是否取得 httpAuth.credentials,是否调用授权,条件查询是否在数据库侧过滤,拒绝是否返回一致错误,审计是否记录主体、资源引用和 task ID。
最小权限反例很直接:普通开发者读取自己团队实体应成功,读取允许公开的目录字段按策略成功;创建受限模板、删除 Location、查看未处理原始数据或执行补偿删除应返回拒绝。平台管理员成功执行同一动作后,还要证明审计可关联到身份和资源。只测试 UI 角色菜单,无法证明后端执行点安全。权限概念与“插件负责执行限制”的契约见 Permission Framework。
插件是运行代码也是供应链
新前端系统可通过 app.packages: all 自动发现 packages/app 依赖中的插件,也可以使用 include/exclude 控制发现;后端模块仍需要在后端包中安装并注册。尚未迁移的第三方前端插件可以通过 @backstage/core-compat-api 包装,但这只是兼容桥,不会把旧插件变成新系统原生插件。自动发现降低接线成本,却把“加一个依赖”变成“向门户注入前端代码、路由、配置和可能的后端能力”。安装前至少确认包来源、维护者、许可证、发布频率、漏洞记录、支持的 Backstage release、前后端配对版本、所需外部权限、数据发送位置与退出方案。/alpha 与 /beta 导出允许更快破坏性变化,核心威胁模型也明确要求像审计任意 npm 代码一样审计插件;插件目录收录不是安全认证或商业支持承诺。
第三方插件不应默认获得跨组织高权 Token。按插件建立凭证、出站域名、可读实体字段、缓存位置、日志字段和保留期清单;在代理层限制目标域,在密钥系统分配独立身份,在测试环境抓取出站请求,验证插件不会把 Catalog 全量、用户邮箱、漏洞详情或模板参数发送到未知服务。插件卸载不仅是删 npm 包,还要撤销凭证、删除路由和配置、迁移或清理数据库表、移除注解生产者、重建前端并验证残留链接。
依赖升级以 Backstage release 为组合单位,不要只对单个核心包执行任意 semver 更新。前后端成对插件应来自兼容 release,后端应先于或同时于前端部署。锁文件、backstage.json、容器镜像摘要和插件清单共同构成可追溯制品;安全扫描要覆盖 npm 依赖、基础镜像和自定义 action 依赖。安装插件的推荐方式与 feature discovery 行为见 Installing Plugins,版本偏斜约束见 发布和版本策略,运行信任边界见 Backstage Threat Model。
PostgreSQL 才是共享环境的状态根
生产形态通常把 Backstage 应用做成无状态实例,把插件状态放在外部 PostgreSQL。先在后端包加入 pg,再通过环境变量注入连接信息:
backend:
database:
client: pg
pluginDivisionMode: schema
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
ssl:
ca:
$file: ${POSTGRES_CA_FILE}
knexConfig:
pool:
min: 2
max: 12
acquireTimeoutMillis: 60000
idleTimeoutMillis: 60000默认模式会给插件划分逻辑数据库;受限托管 PostgreSQL 无法创建多个数据库时,pluginDivisionMode: schema 可改用同一数据库中的独立 schema。这个选择会影响授权、备份粒度、迁移和故障半径。数据库账号至少需要目标数据库或 schema 的连接、建表和迁移权限;是否允许创建数据库要和平台侧预创建策略一致。连接池 max 乘以 Backstage 副本数、滚动升级时的新旧副本和后台任务峰值,必须小于数据库连接预算,不能只看单实例配置。
TLS 不能停在“已加密”。ssl.ca 或环境的 SSL mode 应验证服务端证书;仅要求加密但不校验身份,仍可能连接错误端点。密码不得写进配置仓库、镜像层、命令行或任务日志。数据库审计、慢查询、容量、连接等待、迁移时长和磁盘增长要纳入观测,Catalog 队列老化与 Scaffolder task 表增长通常比 CPU 更早暴露治理问题。
备份需要覆盖 PostgreSQL、Backstage 配置与密钥引用、插件版本清单、模板和自定义插件源码。只备份数据库无法恢复 Git 中的模板与实体,只备份 Git 又会丢失任务、用户设置和插件运行状态。恢复演练在隔离环境完成:恢复数据库快照,使用与备份一致的镜像和配置启动,验证插件迁移不会意外前滚,再抽查实体数量、关系、Location、失败处理记录、模板任务与权限策略。恢复点和恢复时间目标由业务 SLO 决定,演示环境的数值不能直接复制。
Docker 与 Kubernetes 各自承担什么
Docker 镜像负责冻结后端 bundle、生产依赖和运行用户,不负责提供持久数据库、身份源或密钥系统。官方 部署入口 同时允许 Docker、Kubernetes 和组织已有的其他基础设施;Docker/Kubernetes 是示例路径,不是许可或支持边界。官方 Docker 路径推荐在宿主机完成构建后组装运行镜像,以获得更直接的缓存;多阶段构建适合构建环境统一但会增加镜像构建时间。无论哪种方式,都应以非 root 用户运行、固定基础镜像摘要、只复制必要制品、设置只读文件系统可行性,并给 Scaffolder 工作目录独立的临时空间和容量限制。
生成项目已经带有 packages/backend/Dockerfile。先把 PostgreSQL、正式身份提供方和生产配置接好,再从仓库根目录构建;不要把本地 guest 登录和内存 SQLite 一起封进“生产镜像”。下面的命令把依赖、类型检查、后端 bundle 和镜像构建串起来,backstage-lab.env 只保存本地实验占位值且不得提交:
corepack yarn install --immutable
corepack yarn tsc
corepack yarn build:backend
docker image build . -f packages/backend/Dockerfile --tag backstage-lab:local
docker run --name backstage-lab --rm \
--env-file backstage-lab.env \
-p 7007:7007 \
backstage-lab:local镜像构建成功应出现最终 image ID;容器日志应显示后端插件初始化完成并监听 7007,从另一终端执行前面的无凭证 Catalog 请求应得到 401 而不是连接拒绝或 502。前者证明进程和默认认证边界都在,后两者分别指向容器未监听或代理/上游未就绪。实验结束执行 docker stop backstage-lab;若容器带 --rm 会自动删除,再用 docker image rm backstage-lab:local 清理本地镜像。PostgreSQL 数据库、OAuth/Git 应用和测试仓库仍要按后文顺序单独撤销,删除容器不会清理这些外部状态。
Kubernetes 适合运行无状态 Backstage 副本并滚动升级,但不会自动赋予高可用。Deployment 需要 readiness/liveness、资源 requests/limits、PodDisruptionBudget、滚动策略和受控 ServiceAccount;Secret 通过密钥控制器或工作负载身份注入。PostgreSQL 放在集群内还是托管服务,是数据平台的可靠性决策,不应由一份示例清单默认决定。Kubernetes 插件用于服务 Owner 查看工作负载,也不意味着 Backstage Pod 必须获得集群管理员权限;按集群和命名空间分配只读凭证,限制可见资源与日志。
扩副本前先确认数据库连接预算、任务调度、Catalog 处理吞吐、Scaffolder 临时文件和外部 API 限流。Backstage 应用可水平扩展,不代表所有自定义插件都无状态;把进程内缓存当唯一状态、把任务文件写本地永久盘、在启动时执行非幂等初始化,都会让副本之间出现差异。官方 Kubernetes 部署说明 给出基础对象,但生产责任仍包括集群、网络、证书、数据库、备份、告警、容量和灾难恢复。
按第一证据排查目录和模板故障
“Catalog 里没有服务”至少分成四类。发现失败时,Provider 没有看到仓库,先查过滤规则、安装权限、API 限流和调度日志;读取失败时,Location 已存在但仓库 URL、凭证、代理、DNS 或 CA 出错;处理失败时,查看 Unprocessed Entities 的错误、raw entity、rules、Policy 和 Processor;stitch 后关系异常时,实体可见但 Owner 或依赖目标缺失,应按完整 entity ref 查目标和 namespace。先分层再刷新,反复点击 Refresh 只会放大限流。
“改了 YAML 但页面没变”先确认变更已合并到 Provider 实际读取的分支和路径,再检查 Location 来源、下次处理时间、失败状态和最终实体。若两个来源竞争同一实体,比较 managed-by location 与来源键;若直接删除后又出现,说明活动来源仍在供给。若 orphan 长期不清理,检查 Processor 是否仍发出子实体、清理任务是否运行和下游是否持有旧索引。
“模板一直 Running”先定位当前 action,而不是重启整个门户。查看 action 是否有超时和 abort 支持,外部 API 是否已创建资源,task worker 是否存活,临时目录与磁盘是否耗尽。重启可能让 UI 状态变化,却不会自动删除已创建的仓库或云资源。先以 task ID 和幂等键盘点副作用,再决定恢复、补偿或人工接管。
“用户能看见按钮但调用失败”通常是前端展示条件与后端 permission 不一致;“用户看不见按钮但 API 成功”则是后端没有执行授权。抓取实际 API 状态码,确认主体引用、permission 名称、资源条件与插件执行点。401 指向缺少或无效身份,403 指向已识别但被拒绝;不要把两者都归结为“SSO 有问题”。
网络类故障要分浏览器到前端、前端到后端、后端到 Git/身份源/数据库三段。OAuth 回调错多查 baseUrl、代理转发头和注册回调;私有仓库读取错多查 integration host、Token scope、企业 CA;数据库超时查 DNS、TLS、连接池等待和总连接数。日志先脱敏再进入集中平台,尤其避免 Authorization、Cookie、仓库写 Token、模板 Secret 和完整实体原文。
升级、回滚与恢复要作为同一个动作
Backstage release 聚合了一组验证过的包版本,单个 npm 包的 semver 与整体 release 不是同一概念。升级前读取 backstage.json,运行 yarn backstage-cli versions:bump 统一提升 @backstage 包,检查 create-app changelog 和 Upgrade Helper 中的模板差异,再审查每个第三方插件。先在隔离环境恢复一份脱敏数据库备份并启动新版本,才能同时验证代码兼容和数据库迁移。
上线顺序应让后端先于或同时于对应前端插件,避免新前端调用旧后端能力。金丝雀副本要验证登录、Catalog 查询、Refresh、模板 dry run 或无副作用 action、权限拒绝、搜索与关键插件;同时观察数据库迁移、错误率、处理队列年龄、task 失败率、连接数和外部限流。只有首页可打开,不足以推进全量。
回滚前先判断迁移是否向后兼容。镜像回退很快,数据库 schema 和新任务数据未必能被旧代码读取;盲目回滚会把一次应用故障扩大成数据故障。可逆升级使用旧镜像加兼容 schema 回退;不可逆迁移则停止写入,从升级前快照恢复到隔离实例,核对数据后切换流量,并重新处理升级窗口内的 Git 变化。Scaffolder 已产生的外部副作用不能靠数据库恢复撤销,必须按 task 审计做补偿。
清理实验实例时,先停止入口和任务消费,撤销 OAuth 应用、Git App、PAT、数据库账号和 Kubernetes ServiceAccount,再删除临时仓库、测试 Location、任务工件、数据库/schema、对象存储与镜像。最后从 Catalog 权威源移除自身实体并确认不再被 Provider 发现。这个顺序避免“先删数据库导致无法枚举外部资源”,也避免凭证在无人维护的环境继续有效。
把门户当作长期产品治理
服务目录成功的信号不是实体数量持续增长,而是工程师能用它作出正确决定。每个实体类型要有 schema Owner,每个字段要有权威来源,每个模板要有产品 Owner 和版本策略,每个插件要有维护、升级与退出责任。服务团队负责随代码维护 catalog-info.yaml,平台团队负责规则、Provider、Processor、模板运行时和门户可靠性;组织目录团队负责 User/Group 同步,安全团队负责高风险 action、凭证与审计策略。责任重叠时用字段权威表解决,不让最后写入者获胜。
采用率不能只看月活或页面访问量。更有意义的组合是:有可解析 Owner 的活跃组件比例、孤儿实体年龄、元数据变更到可见的延迟、模板成功后无需人工修复的比例、失败任务补偿闭环时间、插件错误预算和目录搜索后到下游工具的成功跳转。指标必须能回到具体 Owner 和修复动作;为了追求覆盖率而自动填充假 Owner,会让数字更好看、事故更难处理。
成本由四部分组成:平台开发与值班人力,Backstage 和插件依赖升级,PostgreSQL、计算、日志、搜索与对象存储,以及代码托管、云和身份系统的 API 配额。新增插件前比较它减少的上下文切换与带来的运行责任;新增模板前比较自助节省与失败补偿成本;新增实体字段前比较决策价值与维护频率。不能回答“谁在什么事件下更新它”的字段,不应进入权威目录。
最终的健康状态是一组可重复的不变量:权威源变化后实体在约定窗口内收敛;坏 YAML 留下可诊断错误而不会污染最终视图;无凭证与无权限请求在后端被拒绝;模板后段失败时外部副作用可枚举、可补偿、可审计;任一插件都能撤销凭证并退出;升级前备份能在隔离环境恢复;删除来源后实体、索引和敏感数据按保留策略消失。做到这些,Backstage 才从“漂亮的内部首页”变成可信的软件生产入口。
