把生产环境复制成 staging,听起来像一行命令的事。
真正出问题的地方,也往往就在这一行。staging 开始连生产数据库;preview 复用了生产自定义域名;开发环境继承了线上 secret reference,因为平台把「形状一样」理解成了「值也一样」。
Appaloft 本来就有一条很窄的 clone 路径:environments.clone 会复制环境自己的变量,并创建一个新的 environment 命名空间。它有用,而且故意不完整。真正的 staging 或开发环境,还需要资源拓扑、runtime profile、依赖绑定、路由意图和存储要求。难的不是把这些东西一起搬走,而是搬走形状时,不要把只属于生产的值一并拖过去。
这篇写的是这条边界:Environment Profile Duplication。它和一个部署 URL,应该说清楚什么会变、一个自定义域名接入 Appaloft 的过程、部署失败以后,平台应该留下什么是同一条控制面故事。前几篇讲路由、域名和失败恢复;这篇讲新环境在被允许部署之前,应该先完成什么。
形状不是状态
Environment Profile 不是资源的新主人。它是一个面向 plan、diff 和 sync 的、与 provider 无关的组合快照:
- 资源拓扑和类型;
- source 与 runtime profile;
- 网络意图;
- 变量要求;
- 依赖绑定意图;
- 路由意图;
- 存储要求;
- 若干策略意图,例如 auto-deploy 或 preview 的 base 选择。
它不是生产状态的全量导出,也不拥有数据库、自定义域名、secret 材料或部署历史。
这个区分很重要,因为 Appaloft 把所有权边界写得很死。Resource 仍然是可部署单元;Dependency Resource 拥有托管或导入的 Postgres、Redis、对象存储和外部服务;Domain Binding 拥有主机名治理;Deployment 仍然是一次不可变的执行尝试。Environment Profile Duplication 是覆盖这些 owner 的工作流,不是绕过它们的捷径。
如果产品只有一个叫「全部复制」的命令,这些边界会很快被便利性压垮。
先 plan,再做决策
安全默认是 plan-first。
appaloft env copy production staging --dry-run
这对应 environments.plan-duplicate。plan 应该说明:哪些可以作为形状复制,哪些需要重新生成,哪些必须显式决策,哪些仍然 defer。它绝不应该返回 secret 值。
审过 plan 之后,再带着决策 apply:
appaloft env copy production staging \
--dependencies create-new \
--secrets regenerate \
--data empty \
--domains generated \
--storage empty
apply 路径是 environments.duplicate-profile。兼容别名如 appaloft env duplicate plan 和 appaloft env duplicate apply 还在,但我们更希望大家记住的产品语言更简单:复制 profile,审 plan,再应用决策。
日常还有两个配套操作:
appaloft env diff-profile比较两个环境的形状和决策,不暴露 secret 值;appaloft env sync-profile把选定的缺失形状导入已有目标环境,而不是覆盖目标环境自己的选择。
copy 从 base profile 创建新环境;diff 和 sync 则在之后避免形状在错误的地方漂移。
真正要命的决策
大多数复制事故,藏在四类东西里:依赖、secret、域名、存储数据。
依赖
源环境用了托管 Postgres,不代表 staging 可以默默共用同一个库。plan 可以提供:
| 决策 | 含义 |
|---|---|
create-new-managed |
为目标环境准备独立依赖,并绑定新的 secret reference |
bind-existing |
把目标资源绑到目标上下文里已经存在的依赖 |
reuse-source |
在显式确认后,继续使用源依赖 |
defer |
先创建形状,部署前必须补完绑定 |
reuse-source 是允许的,因为有些团队会故意共享只读分析库,或临时拿生产依赖做排障。但它绝不能是静默默认。一旦选择,read model 和审计轨迹都应该继续显示 shared-source 警告。
Secret
secret 值不搬家。目标环境最多拿到新的 secret reference,或者一个待处理决策。如果复制出来的环境还能在日志里打印生产连接串,这个模型就已经失败了——哪怕部署看起来「成功」。
域名
生产自定义域名默认不复制。plan 会重新生成平台路由,或者要求显式填写目标主机名。这和自定义域名接入是同一套所有权观念:hostname 是关于「谁有权绑定它」的声明,不是 environment 上的普通字符串字段。
存储数据
卷的要求可以复制,卷里的数据不能被暗示复制。空卷、从备份恢复、从 artifact 导入,都必须是显式决策。「让 staging 看起来像生产」是数据迁移问题,不该藏在创建环境里。
Defer 比瞎猜更安全
如果必要的依赖或 secret 决策被 defer,部署准入应该 fail closed。
这听起来严厉。但比另一种结果好得多:半复制出来的 staging 直接打到生产,只因为平台替用户填了空。
defer 也会让 AI agent 和 CI 更安全。agent 可以创建目标环境,报告还有哪些决策没完成,然后停下来,而不是先宣布成功。GitHub workflow 可以在未解决的 profile decision 上失败,而不是把「environment 已创建」当成「可以接流量」。
这里和控制面在安装进度、部署 URL 上的规则一样:中间成功不等于最终成功。形状齐了、绑定没定,不叫可部署环境。
哪些东西仍然不会默默复制
即使 apply 成功,下面这些仍然不在静默路径里:
- 依赖 secret 材料;
- 生产数据库、缓存或存储数据;
- 需要目标侧所有权确认的自定义域名和生成路由;
- 仍需目标环境审阅的 resource 变量与 access profile;
- 需要显式数据决策的存储挂载;
- 部署历史。
其中一部分会变成目标环境上的后续工作。这是故意的。环境复制准备的是可审阅的形状,不是假装一个按钮解决所有运维决策。
Console 侧也有产品诚实边界。公共 Appaloft 暴露中性的 staged workflow 扩展点;托管 Cloud Console 可以做成多步复制体验。First-party Community Web 的完整 Duplicate / Diff / Sync 分步界面仍是 follow-up。今天稳定的契约是 CLI 和 API;Console 可以在具体发行版里做得更完整。
为什么还要保留变量级 clone
暂时保留 environments.clone,是有意的。
当有人只需要一个新的空命名空间,外加少量环境级变量时,变量级 clone 仍然合适。当应用形状本身需要迁移时,才该走 profile copy。
过早合并两者,要么会让 clone 危险地变宽,要么会让 profile copy 更难解释。所以文档继续分开说:
- clone:变量和环境身份;
- profile copy:拓扑和意图,环境相关值走决策。
如果以后这个切分让人困惑,可以再 deprecation 或 alias。现在不要用一个命令同时表示两件事,把问题糊过去。
更安全的复制,留下更少意外
profile copy 成功之后,有用的结果不是「staging 存在了」。
有用的结果是:
- 目标环境具备预期的资源形状;
- 依赖绑定要么隔离、要么被显式共享、要么明确 defer;
- plan 过程没有返回或写入生产 secret 值;
- 生产域名没有被静默 rebind;
- 必要决策未完成时,部署会被拦住;
- 之后还能用 diff 和 sync 把形状维持在诚实状态。
这比 naive clone 多几步。但它更接近谨慎的人本来就会做的事:先复制应用布局,再决定新环境接到哪里,然后才部署。
如果从 CLI 开始,先跑 --dry-run,把决策看清楚再 apply。如果某个决策显得过于自动,通常说明平台应该再问一次,而不是让操作者更用力地相信默认值。