预发布测试说明的是当时实际运行的产物与环境。生产部署前重新构建,会得到另一份输出,需要检查它的身份,或者重新验证它的行为。产物晋级指把已验证的那份文件继续交给下一个环境,并记录这次决定。它减少了一类不确定性,但不会让预发布与生产环境自动等价,也不会免除部署之后的运行检查。交付字节一致,是发布判断的一部分,而不是全部。
明确究竟把什么向前传递
使用不可变的软件包标识或内容摘要定位构建结果。会移动的标签、分支名乃至源码提交,本身都不足以唯一证明部署了哪些字节。同一源码的两次构建,可能因为工具链、依赖解析、嵌入设置或打包步骤而不同。可复现的重建确实可能得到完全相同的字节,但应先比较结果,再把它视为之前通过测试的同一产物,而不是仅凭源码相同推断。
应区分构建阶段与发布阶段。发布组合包含特定产物及该部署需要的配置,运行过程再把这个组合放到真实依赖和数据之上。记录产物身份与合适的配置版本,同时避免泄露秘密。如果前端环境设置在打包时直接编译进 JavaScript,修改设置就会改变字节,也就是生成了另一份需要验证的产物。不能一边修改包内内容,一边继续沿用原包通过测试的身份。
坏掉的样子
某些流水线名义上在晋级产物,部署脚本却悄悄重新编译;另一些流程下载了正确归档,却又覆盖归档内部的配置文件。这两种操作都改变了测试时的输入。应明确哪些设置位于不可变负载之外,并通过应用已有的配置接口读取。校验和如果发生在后续修改之前,就不能继续代表运行时文件;核验时点与文件变化顺序必须一起审查。
即便产物相同,数据库结构、功能开关、依赖版本、权限、资源限制或网络路径变化,也能让行为不同。晋级保证的是字节连续性,不是整个执行上下文都一致。部署后仍要验证关键生产行为;预发布结果无法在生产重现时,要比较真正相关的环境差异,而不是看到摘要相同就认定问题与发布无关。配置也是发布组合的一部分,需要可追溯的变化记录。
回滚同样有前提。旧程序可能无法理解新写入的数据,或者无法适应已经完成的破坏性结构迁移。把指针切回旧产物,并不会撤销这些外部影响。恢复说明应关联数据兼容性、配置和依赖状态;不能反向恢复时,就应准备必要的向前修复。不能把“重新选择旧镜像”描述成所有故障都适用的完整回滚方案。
快速验证
把代码保存为 promote.py,执行 python3 promote.py。示例只使用 Python 标准库、临时目录和绑定回环地址的 HTTP 服务。它写出一份很小的应用文件,记录摘要,再复制进两个本地环境。production-simulation 只是临时目录的标签,不会触发外部部署,也不会接触真实生产配置。这个命名用于让输出清楚地区分两个运行位置。
from hashlib import sha256
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Thread
from urllib.request import ProxyHandler, build_opener
import json
import runpy
import shutil
source = 'def render(config):\n return {"build": "local-build-41", "tier": config["tier"]}\n'
digest = sha256(source.encode()).hexdigest()
def verify(path):
if sha256(path.read_bytes()).hexdigest() != digest:
raise ValueError('artifact changed')
def handler(path, config):
verify(path)
render = runpy.run_path(str(path))['render']
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
data = path.read_bytes() if self.path == '/artifact' else json.dumps(render(config)).encode()
self.send_response(200)
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
return Handler
with TemporaryDirectory() as directory:
root = Path(directory)
artifact = root / 'build.py'
artifact.write_text(source)
opener = build_opener(ProxyHandler({}))
observed = []
for tier in ('staging-local', 'production-simulation'):
destination = root / tier
destination.mkdir()
deployed = destination / 'app.py'
shutil.copyfile(artifact, deployed)
config = {'tier': tier}
server = ThreadingHTTPServer(('127.0.0.1', 0), handler(deployed, config))
thread = Thread(target=server.serve_forever)
thread.start()
try:
origin = f'http://127.0.0.1:{server.server_port}'
with opener.open(origin + '/artifact', timeout=5) as response:
received_digest = sha256(response.read()).hexdigest()
with opener.open(origin + '/', timeout=5) as response:
rendered = json.load(response)
assert received_digest == digest
assert rendered == {'build': 'local-build-41', 'tier': tier}
observed.append({'artifact_sha256': received_digest, 'response': rendered,
'config_sha256': sha256(json.dumps(config, sort_keys=True).encode()).hexdigest()})
finally:
server.shutdown()
thread.join()
server.server_close()
deployed.write_text(source + '# rebuilt\n')
try:
verify(deployed)
except ValueError:
changed_rejected = True
else:
changed_rejected = False
assert changed_rejected
print(json.dumps({'environments': observed, 'changed_copy_rejected': changed_rejected}))
每个服务启动之前,脚本都核对复制文件是否符合原始摘要,再加载其中的应用函数。请求 /artifact 会返回部署文件的真实字节;请求 / 则用该环境单独提供的配置执行应用函数。两次 HTTP 下载得到相同的产物摘要,两份应用响应也都声明 local-build-41。这些结果结合文件复制与实际请求,建立了本地传递的证据。
响应中的环境标签不同,记录的配置摘要也不同;这里的配置仅包含无害标签。它说明同一份应用字节可以因为明确的配置输入而产生不同输出。服务停止后,脚本再修改一份副本,同样的摘要检查会拒绝它,因而这道身份门禁能够发现示例中的静默重建。不能把摘要检查称为功能测试,二者分别验证不同性质。
本实验没有模拟注册仓库、认证、数据库迁移、真实生产流量或回滚,只覆盖本地文件传递、校验、独立配置以及 HTTP 结果。它也不能证明某个真实部署系统已经遵循相同规则。把范围写清楚,才能让读者理解哪些步骤可直接观察,哪些发布条件仍需要结合自己的系统另行验证。
核对真正运行的发布组合
把晋级决定关联到产物与配置版本,然后检查已部署进程或实际服务的文件,不能只依赖上传成功。为需要的恢复窗口保留批准过的原产物。如果后续阶段生成了另一份软件包,应为新输出分配新的身份,并执行与变化相关的验证。可用的晋级记录,应能够回答传递了哪些字节、带了哪份配置,以及传递之后实际观察到了什么行为。
延伸阅读
来源
- The Twelve-Factor App: build, release, run,区分构建产物、发布组合和运行过程,已核验 2026-09-13。
- The Twelve-Factor App: config,随部署环境变化的配置,已核验 2026-09-13。