Preview Environment 生命周期:隔离种子、租约围栏与销毁证明
每个 PR 一套预览环境很方便,也容易串用数据、凭证和旧部署。本文定义短命环境的身份、种子快照、单调租约与销毁回执,让创建、重试、关闭和回滚都可验证。
本文要点
- 主题聚焦:TypeScript、DevOps、开源
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
PR 每次更新都自动部署一个 URL,表面上是开发体验升级,背后却有三组身份:代码提交、工作流 attempt 和实际资源。旧 attempt 在网络恢复后继续写 DNS,新环境可能被旧清理任务删掉;两个 Preview 共用一份“测试数据库”,演示操作互相可见;PR 关闭后域名消失,云盘和密钥仍在计费。CI/CD 流水线设计描述发布阶段,本篇把 Preview 自身当作必须收敛的短命对象。
Kubernetes Namespace 提供资源名称隔离,但不会自动隔离集群级对象、外部数据库或共享 bucket;Lease API 提供持有者与续约时间,也仍需要业务层决定谁可写。GitHub Deployment 把某个 ref、environment 和状态连接起来,却不等于资源已经完整创建或销毁。因此不能把 Namespace 名、部署状态或 URL 中任一个单独当作环境真相。
一份身份控制全部副作用
控制器首次受理 PR 时创建不可变 preview.environment.v1,分配不可复用的 environmentId,固定 repository、PR 身份、owner、创建时间和硬 TTL。revision、制品、seed、状态和租约代数会变化,必须进入独立的 preview.control.v1,以 recordVersion 做 CAS;提升 generation 是一条控制记录事件,不能重签或改写身份 manifest。
schema: preview.environment.v1
environmentId: pe_01JZ...
expiresAt: 2026-07-20T10:00:00Z
---
schema: preview.control.v1
recordVersion: 18
generation: 4
revision: 91a6c2e...
artifact: registry.example/web@sha256:82c...
seedSnapshot: sha256:d4b...
控制记录的状态限定为 REQUESTED -> PROVISIONING -> SEEDING -> READY -> DRAINING -> DELETING -> DELETED;无法确认副作用时进入 QUARANTINED。协调器只写 transactional outbox,唯一 effect executor 才能调用供应商;每项保存 effectId、generation、payload digest、状态及返回的 providerOperationId。执行端优先使用供应商原生 idempotency key、ETag/precondition;不支持时按代创建唯一资源名,禁止复用后覆盖。超时且无法确认是否生效的项进入 UNKNOWN_EFFECT,不能重试相反操作或推进 READY。
共享 Preview DNS alias 由单独的 alias CAS/Lease 串行化,只允许当前 control generation 指向已 READY 的代;各代真实 hostname 永不复用。PR 更新在同一数据库事务内 CAS 提升 generation 并插入 outbox;旧 executor 只能补记自己的 provider receipt,不能更新当前状态或 alias。关闭后先撤销入口与凭证、停止新会话,再按拥有者清单删除。
种子不是一条随时变化的脚本
种子数据发布为不可变 seedBundle.v1:包含 schema version、生成器 revision、随机种子、fixture blob 摘要、行数不变量和敏感数据证明。Preview 只可加载批准 digest,禁止运行时从生产抽样。邮箱、手机号和支付标识用明确的合成域生成;需要跨表关联时使用 bundle 内稳定 ID,而不是生产值的普通哈希。数据库、缓存、搜索索引与对象存储都以 environmentId 分区,应用凭证的 audience 也只允许该身份。
加载采用 staging 区域:先验摘要与 schema,完成全部行数和引用完整性检查,再原子切换为可读;半包不能进入 READY。数据库迁移管理负责结构的前后兼容,seed gate 另外验证当前应用版本是否能读取这份语义数据。刷新 seed 通过 control CAS 创建新 generation,不原地清空正在演示的环境。团队若需要保留场景,应导出经过审查的合成增量,而不是延长生产数据副本寿命。密钥管理中的短期凭证也必须绑定 Preview audience,通用 staging token 不得复用。
重放并发与销毁失败
故障套件同时发送重复 webhook,让旧 attempt 延迟 15 分钟,在供应商成功但 ack 前杀掉 executor,PR 关闭后立即重开,seed 上传到 60% 时杀进程,删除 Namespace 时卡住 finalizer,并让旧清理器碰新 generation。还要交换两个环境的数据库 URI、secret audience、对象前缀和 DNS alias。期望得到 outbox 幂等重放、UNKNOWN_EFFECT、STALE_GENERATION、PARTIAL_SEED、QUARANTINED 与 ISOLATION_VIOLATION。
每个资源强制携带不可变 owner tag:environmentId/generation/effectId。旧代出现 UNKNOWN 时,reconciler 必须用 tag 对供应商、集群级对象、DNS、数据库和对象存储做全量盘点,认领 providerOperationId 后再补偿;查不到不等于未创建。只有删除已知项后连续三轮、间隔至少 5 分钟的全域 owner-tag 扫描均为零,才可签发绑定 generation 与扫描摘要的 destruction receipt;未知或无 tag 资源只能进入 QUARANTINED。
验收门要求:100 轮并发创建只产生一个当前环境;跨环境读写成功数为 0;10 类迟到操作拦截率 100%;ack 丢失重放不产生第二资源;共享 alias 并发切换始终指向当前 READY 代;READY 前 seed 不变量覆盖率 100%;关闭后 2 分钟内入口和凭证失效,30 分钟内 99% 完成三轮零扫描;销毁回执资源数与盘点完全相等。
上线先让新控制器 shadow 生成 outbox 计划,不执行副作用;随后只管理内部仓库的 5% Preview,再逐步接管 DNS、种子和删除。迁移期间旧、新控制器不能同时拥有 executor Lease。若创建失败率超过 2%、隔离检查出现一次失败或 P95 就绪时间恶化 30%,冻结新环境,把 Lease 切回旧 executor,并按 owner tag 对账新控制器的代。Feature Toggle控制的是用户功能暴露,这里的回滚必须连同资源 owner 和销毁责任一起切换。
Related