CI/CD 十分钟入门

持续集成证明的是每一次改动都能并入主干。持续交付证明的是同一个已知 artifact 能安全地到达生产环境。重点不是一份写得很英勇的 YAML,而是一条从 commit 到证据再到发布的短路径,而且可以重复走。

🎙️ 发布并录制于: ·

01把 pipeline 设计成快速反馈

一条 pipeline 是一串证据关卡。把格式化、静态检查和小范围单元测试放最前面,因为它们失败得便宜。互不依赖的 job 并行跑。集成测试和打包放后面,然后部署刚刚通过检查的那个包。如果开发者要等四十分钟才知道自己打错了一个字,他们就会攒着改动一起提,也不会再信任这条 pipeline。

# Fail cheap, then spend more confidence
lint ───────────────┐
unit tests ─────────┼─→ integration → build once → deploy staging → production
secret scan ────────┘

# Every command must also work on a clean developer machine
./ci/lint
./ci/test-unit
./ci/test-integration
./ci/build
真实失败:exit code 1
Error: Process completed with exit code 1.

这一行是结论,不是原因。第一步:打开失败的那个 step,找到第一个返回非零的命令。第二步:往上翻到它的第一条具体报错,别只看最后那段堆栈。第三步:用同样的运行时版本,在本地把这个脚本原样跑一遍。第四步:修命令或者修代码。别在后面接 || true,那是把关卡变成摆设。

02让构建可复现

同一个 commit 加同一组声明好的输入,应该产出功能上相同的 artifact。锁定运行时版本,把 lockfile 提交进仓库,从干净的工作目录开始,别让构建时的网络行为决定你的关键路径。把源码 commit 和工具版本记进 artifact 里。「latest」不是版本号,它是一场延后发生的事故。

# Pin base image by immutable digest
FROM node:22.17.0-alpine@sha256:0123456789abcdef...
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm test && npm run build
ARG GIT_SHA
LABEL org.opencontainers.image.revision=$GIT_SHA

可复现不代表压缩包里每个字节都自动一致;不做归一化的话,时间戳和文件顺序都可能变。它的意思是,没人能在重建昨天那个 commit 时,偷偷换上一个更新的依赖。优先部署 CI 已经构建好的那个 artifact,而不是在每个环境里重新构建一遍。

03缓存下载内容,不要缓存正确性

缓存是一种随时可能消失的优化。依赖缓存的 key 要包含操作系统、架构、运行时版本和 lockfile 的哈希。永远不要在没涵盖全部输入的情况下缓存测试结果。永远不要把密钥或构建好的发布 artifact 塞进通用共享缓存。

# Good cache identity
key: npm-${{ runner.os }}-node-22-${{ hashFiles('package-lock.json') }}
path: ~/.npm

# Prove the pipeline works without yesterday's state
weekly-clean-build: restore-keys: []

# Artifact storage is not a cache
release/app-8f31c2a.tar.gz  retention: 90 days  immutable: true
真实失败:校验和不匹配
npm error code EINTEGRITY
npm error sha512 checksum failed when using sha512: wanted ... but got ...

第一步:把 lockfile 和日志里出错的包名保存下来。第二步:禁用依赖缓存重跑一次。第三步:如果干净重跑通过了,就删掉损坏的缓存条目,并且换一个缓存 key。第四步:如果还是失败,检查 registry 或代理,只有在评审过依赖变更之后才重新生成 lockfile。别去关掉完整性校验。

04给密钥短寿命和窄权限

不要把凭据放进仓库、workflow 文件、镜像层、artifact、缓存或日志。优先用工作负载身份:CI job 证明自己是谁,然后领到一份短期的云凭据。把生产部署限制在受保护的分支和环境上。来自 fork 的 pull request 绝不能拿到特权密钥。

# Prefer identity exchange over a permanent cloud key
permissions:
  contents: read
  id-token: write

environment: production
# Production environment adds approval and branch restrictions.
真实失败:集成权限不足
Resource not accessible by integration

第一步:找出返回这条消息的是哪一次 API 调用。第二步:看这个 job 实际拿到的 token 权限,仓库默认值可能是只读。第三步:只在 job 级别补上缺的那一项权限,比如 pull requests write,并确认这类事件允许拿到它。第四步:如果对方是不可信的 fork,就重新设计流程,而不是把更强的 token 暴露出去。别一上手就掏个人访问令牌。

05让同一个 artifact 逐环境推进

只构建一次,用摘要标识结果,然后让同一个 artifact 依次经过测试、预发和生产。环境相关的配置在运行时注入。如果预发从源码重建一次,生产又重建一次,那你测的是部署物的两个表亲,不是部署物本身。

# CI records an immutable identity
IMAGE=registry.example/app@sha256:4c3f...9a10

# Staging and production differ in configuration, not application bytes
deploy staging    $IMAGE --config=config/staging
verify staging    $IMAGE
approve production
deploy production $IMAGE --config=config/production

测试数据别进生产,生产凭据别进预发。让各环境在架构上尽量相似,但别假装它们可以互换。把 commit、构建记录、依赖清单、校验和以及发布说明挂到 artifact 上,这样值班的人才能准确回答线上跑的到底是什么。

06先让数据库迁移保持兼容

只有数据库还接受旧版应用时,应用回滚才是容易的。用 expand and contract。先加一个可空列、新表或者兼容的索引。上线能同时读懂新旧两种结构的代码。分批回填,每批都不大。切换读路径。等回滚风险过去之后,在后面某个版本里删掉旧结构。

# Release A: expand
ALTER TABLE users ADD COLUMN display_name text;
# App writes both old name and display_name.

# Background job: small batches, observable progress
UPDATE users SET display_name = name
WHERE display_name IS NULL AND id > $1 AND id <= $2;

# Later release: contract after every reader moved
ALTER TABLE users DROP COLUMN name;
别把表锁藏在启动流程里

把评审过的迁移当成一个明确的发布步骤来跑,不要让每个应用副本在启动时各跑一遍。用接近生产规模的数据量测一遍。给危险语句加锁超时,监控复制延迟,一旦回填影响到线上流量就停掉。在同一个版本里做破坏性重命名再叠一次代码上线,会让回滚变成一句空话。

07按失败代价选部署策略

滚动部署逐步替换实例,几乎不需要额外容量,但新旧版本会并存一段时间。蓝绿保留两套完整环境,切流量很快,逆转也快,代价是成本更高。金丝雀先把一小部分流量给新版本,只有真实指标一直健康时才扩大比例。

# Canary progression with explicit gates
1%  for 10 min → 10% for 20 min → 50% for 20 min → 100%

stop if:
  error_rate > baseline + 0.5 percentage points
  p95_latency > baseline * 1.20
  checkout_success < agreed floor

# Health means dependency-ready, not merely process-alive.

别为了显得成熟去选 Kubernetes、金丝雀或蓝绿。一个小服务,只要回滚是即时的,一次谨慎的滚动发布就够了。当生产行为能提供预发拿不到的证据时才用金丝雀,而且要让这道关卡自动生效。流量往上涨的时候盯着看板,如果没人负责一条停止规则,那只是在演戏。

08发布之前先设计回滚

回滚是一个正常的控制手段,不是认输。让上一个 artifact 保持可部署,记录当前运行的版本,把逆转做成一条测过的命令。错误率上升就回滚应用代码。如果数据已经被不兼容地改过,或者一个安全修复没法安全地重新引入,那就往前修。功能开关关掉行为比这两种都快,但陈旧的开关会变成第二套配置系统。

# A release record gives the operator concrete choices
current:  app@sha256:4c3f...9a10  commit: 8f31c2a
previous: app@sha256:119d...7b42  commit: 60ab911

# Reversal changes the desired digest; it does not rebuild.
./deploy production app@sha256:119d...7b42
./smoke-test https://app.example
./verify-metrics --compare-to=pre-release

这条路径要演练。半年没跑过的回滚脚本只是文档,不是能力。定清楚谁能触发它、哪个指标越线算触发、数据库兼容性怎么检查、怎么通知客户。服务恢复之后,把失败的 artifact 和日志留下来做诊断。

09排查出问题的那一层

把 pipeline 失败分到源码、runner、依赖、凭据、artifact、部署和运行时这几层。先看第一个失败的命令、runner 的镜像和版本、commit,以及干净重跑会不会改变结果。把这些事实和上一次成功的运行对一遍。乱改 YAML 只会毁掉证据。

/bin/sh: ./ci/build: Permission denied

# Inspect the repository mode, not just local filesystem behavior
git ls-files --stage ci/build
# wanted: 100755 ... ci/build

# Fix and commit the executable bit
git update-index --chmod=+x ci/build
# Also verify the first line, for example: #!/bin/sh
Permission denied 的修法

第一步:确认出错的路径和当前工作目录。第二步:git ls-files --stage 看它在仓库里记录的权限位。第三步:把可执行位提交进去,并确认 shebang 里那个解释器在 runner 上确实存在。第四步:从干净的 checkout 重跑一次。在每次 pipeline 里都调一遍 chmod,只是把仓库里的缺陷盖住,换个 job 往往又会失败。

10CI/CD 速查表

把它当一份发布评审清单用,不是用来收集徽章。

# commit path
fast checks in parallel → integration → build once → checksum and attest
→ deploy same digest to staging → verify → approve → production

# trust boundaries
untrusted fork: no privileged secrets
CI identity: short-lived credential, least privilege
cache: disposable speed-up · artifact: immutable release input

# database
expand → deploy compatible code → backfill → switch reads → contract later

# release
rolling: simple, versions overlap
blue-green: quick switch, double capacity
canary: production evidence, automatic stop rules

# failure order
first failed command → first concrete error → clean local reproduction
→ compare runner, versions, permissions, cache, artifact digest

# rollback
keep previous digest · test the command · preserve database compatibility

我的立场很明确:把目标定在「发布很无聊」。一条十分钟的 pipeline,只要产出一个可追溯的 artifact,就好过一条两分钟的 pipeline 偶尔把缓存里的旧内容部署上线。一条命令能回滚,好过一套没人演练过的精巧策略。当上线一个小改动感觉像例行公事、拦下一个坏改动感觉是随手就能做到的事,CI/CD 才算成功。

Tell me what missed

A correction is more useful than a compliment. This goes straight to the person who writes SwiftGrasp.

Was this page useful?
0/1000

Please do not include passwords, private keys, or personal information.