1、"我这儿明明是好的"

开发说"我在本机跑通了",运维部署到服务器却报错。排查半天——本地装的依赖是 3.2 版,服务器上是 3.0 版。

问题的根源是:代码 ≠ 制品。你的源码不是要部署的东西,构建出来的"制品(Artifact)"才是。本课讲清楚:怎么把一个项目变成一个可复现、可追溯、可部署的制品。

2、构建:从源码到制品

构建(Build) 是把源码 + 依赖 + 配置,编译打包成可部署的东西:

源码 ──构建──▶ 制品
        │        ├── 后端:jar / war / 二进制 / 容器镜像
        │        ├── 前端:静态资源包(dist/)
        │        └── 移动端:apk / ipa

"一次构建,多地部署" 是铁律:

        ┌── 测试环境部署(用这个制品)
构建 ───┼── 预发环境部署(用同一个制品)
        └── 生产环境部署(还是这个制品)

为什么? 如果你在测试环境用 A 制品、生产环境重新构建一遍,那构建过程本身就可能引入差异(时间戳、随机数、依赖解析),"测过的"和"上线的"就不是同一个东西了。构建一次,处处部署,才能保证"测试通过的东西 = 上线的东西"。

3、构建的可复现性

可复现构建(Reproducible Build):相同的源码 + 相同的环境,每次构建产出完全相同的制品。

破坏可复现的常见因素:

  • 依赖版本浮动(^1.2.0 可能解析成不同小版本)→ 锁定版本(package-lock.json / poetry.lock / Cargo.lock)。
  • 构建时间戳打进制品 → 用固定时间戳(SOURCE_DATE_EPOCH)。
  • 构建依赖全局环境 → 容器化构建(Docker),环境也是"代码"。
  • 构建产物顺序不定 → 排序后打包、确定性工具。
价值:可复现 + 锁依赖 = 你真的知道线上跑的是什么。出了事故能准确回溯、重建。

4、语义化版本(SemVer):让版本号有意义

版本号不是随便递增的数字,它是一种契约:

    1  .  4  .  2
    │     │     └── PATCH:向后兼容的 bug 修复
    │     └──────── MINOR:向后兼容的新功能
    └────────────── MAJOR:不兼容的破坏性变更

"向后兼容"是什么意思? 用你库的下游代码,不用改就能升级。

1.3.0 → 1.3.1   ✅ 安全升级(只有 bug 修复)
1.3.0 → 1.4.0   ✅ 安全升级(加了功能,旧接口还在)
1.3.0 → 2.0.0   ⚠️ 谨慎!旧代码可能编译不过/行为变了

预发布版本:1.4.0-beta.1、2.0.0-rc.2(rc = release candidate,发布候选)。

常见配套:

  • 构建号(Build Number):CI 每次构建递增(如 #1234),用于精确追溯。
  • 提交哈希:制品打上 git commit SHA,出问题一秒定位源码。
  • 版本嵌入:把版本号写进制品(app --version 能打出来)。

5、依赖管理:别让"依赖地狱"找上门

App ──依赖──▶ 库 A ──依赖──▶ 库 C@1.0
    └──依赖──▶ 库 B ──依赖──▶ 库 C@2.0    ← 冲突!

应对原则:

做法说明
锁定文件提交 lock 文件,保证团队/CI/H 环境装同一套版本
依赖尽量少每多一个依赖,多一份安全与维护负担
定期升级别攒到"三年不升、一升全崩"
扫描漏洞依赖扫描进 CI(第 23 课)
区分直接/传递依赖传递依赖的坑更难查

6、制品管理:制品仓库

制品不该只在某人电脑里,要放进制品仓库(Artifact Repository):

  • 通用:Nexus、Artifactory、GitHub Packages
  • 容器镜像:Docker Hub、Harbor、云厂商镜像仓库
  • 语言专属:npm registry、PyPI、Maven Central

制品仓库要点:

  • 不可变(Immutable):已发布版本永不覆盖。要改就发新版本号。否则"1.4.2"今天和明天可能是两份东西,追溯就废了。
  • 保留策略:旧版本保留期、清理规则。
  • 权限与审计:谁能推、谁能拉,留痕。

7、一个可复现的构建脚本示例

# Makefile
VERSION := $(shell git describe --tags --always)
COMMIT  := $(shell git rev-parse --short HEAD)
DATE    := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

build:
    docker build \
      --build-arg VERSION=$(VERSION) \
      --build-arg COMMIT=$(COMMIT) \
      --build-arg DATE=$(DATE) \
      -t myapp:$(VERSION) .
    @echo "制品: myapp:$(VERSION)  commit=$(COMMIT)"

# 构建完打上版本标签并推入仓库
publish: build
    docker push registry.example.com/myapp:$(VERSION)
# Dockerfile(多阶段构建,产物小、可复现)
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python -m build

FROM python:3.12-slim
ARG VERSION
ARG COMMIT
ENV APP_VERSION=$VERSION APP_COMMIT=$COMMIT
COPY --from=build /app/dist/*.whl /tmp/
RUN pip install --no-cache-dir /tmp/*.whl
CMD ["myapp"]
注意 --no-cache-dir、锁定依赖、固定基础镜像标签(别用 :latest),都是为了可复现。

8、常见误区

误区问题正解
部署时现场构建各环境制品不一致构建一次,处处部署
不锁依赖环境间版本漂移提交 lock 文件
覆盖同版本制品追溯失效制品不可变,改就升版本
版本号随手编下游无法判断兼容性遵守 SemVer
用 :latest 基础镜像构建结果随时间变固定具体版本标签
不记录 commit事故无法定位制品嵌入版本 + commit

9、动手实验 🎯

任务:为贯穿项目「团队任务清单」建立可复现的构建与版本管理。

  1. 写 Dockerfile:用多阶段构建,产出精简镜像,docker run myapp --version 能打出注入的版本。
  2. 注入元信息:把 VERSION、COMMIT、BUILD_DATE 通过构建参数写进制品。
  3. 锁依赖:生成并提交 lock 文件,验证两次构建依赖完全一致。
  4. 验证可复现:连构两次,比较镜像摘要(docker inspect)。一致吗?不一致,找出原因。
  5. 打版本标签:用 git tag v0.1.0,构建时用 git describe 自动读版本号。
  6. 推制品仓库:把镜像推到(本地或云端)仓库,尝试覆盖同 tag,看是否被拒绝(或配置成拒绝)。
  7. 反思:如果生产事故发生时,你能精确找回"当时跑的那份制品"吗?

下一课:持续交付与部署。

10、今天你带走的三句话

  1. 部署的是制品,不是源码;构建一次,处处部署。
  2. 可复现构建 = 锁依赖 + 容器化 + 去随机性;制品不可变。
  3. SemVer 是契约:MAJOR 破坏 / MINOR 新增 / PATCH 修复。

下一课:持续交付与部署。

标签: 软件工程, 项目管理, 开发流程

添加新评论