外观
GitLab CI 流水线的几点经验
约 849 字大约 3 分钟
2026-09-02
团队用了两年 GitLab CI,从最朴素的「跑一下单测」到现在完整的 build → test → deploy 流水线,踩过的坑零零碎碎。这篇把那些反复出现的问题归一下类。
1. SSH Key 用 File 类型变量最稳
GitLab CI 早期把 SSH Key 放在 File 类型变量里直接用,但 Runner 在某些镜像下会把内容里的换行符处理掉,导致 Load key: invalid format。
最稳的做法:base64 编码后存为普通 Variable,在 .gitlab-ci.yml 里解码:
before_script:
- echo "$SSH_KEY_B64" | base64 -d > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
- eval "$(ssh-agent -s)"
- ssh-add /tmp/deploy_key
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts好处:
- 不依赖 File 类型变量的解析;
- 跨平台(Windows runner / Linux runner)行为一致;
- key 文件落盘,清理
after_script时一并删掉。
2. 临时失败要重试
docker pull、curl、scp 这类网络操作在 CI 里偶发失败,大概率是远端临时抖动。直接让 job 失败太亏,加个 retry:
pull-image:
stage: deploy
script:
- ./pull-image.sh
retry:
max: 2
when:
- runner_system_failure
- script_failurescript_failure 默认会被重试,谨慎使用——脚本本身有 bug 时也会重试,可能掩盖问题。只在确认是网络抖动时再加 script_failure。
3. 把脚本下沉到仓库,而不是写在 YAML 里
.gitlab-ci.yml 一旦超过 100 行就开始难读。把真正干活的脚本放到 scripts/ 目录,YAML 只负责编排:
deploy:
stage: deploy
script:
- bash ./scripts/deploy.sh
only:
- main好处:
- 本地能直接跑脚本调试;
- 脚本可以单测、可以加
set -euo pipefail; - YAML 保持扁平,review 起来快。
4. 镜像清理不要省
CI 跑久了,registry 里会堆满 branch-feature-xxx、MR-123 之类的临时镜像,几个 G 下去都不奇怪。写一个定时任务清理:
#!/usr/bin/env bash
set -euo pipefail
# 保留 main 分支和最近 10 个 tag
git fetch --tags --prune
for tag in $(git tag --sort=-creatordate | head -n 10); do
keep+=("$tag")
done
# 删除其余
docker images --format '{{.Repository}}:{{.Tag}}' \
| grep -Ev "$(IFS='|'; echo "${keep[*]}")" \
| xargs -r docker rmi -f || true配一个 scheduled pipeline,每周跑一次。注意:生产镜像必须加锁,不要让清理脚本误删。
5. Runner 标签要细
别用一个大 docker runner 接所有 job。给 job 打标签:
build-amd64:
tags:
- docker
- amd64
script:
- docker build -t myapp:$CI_COMMIT_SHA .
build-arm64:
tags:
- docker
- arm64
script:
- docker buildx build --platform linux/arm64 -t myapp:$CI_COMMIT_SHA .平台差异、缓存目录、并发上限都不一样,标签分清楚才不会互相挤兑。
6. 缓存的 key 要严格
cache:
key:
files:
- package-lock.json
paths:
- node_modules/key 用锁文件而不是 commit SHA,这样同一份依赖只下载一次。新人首次跑 pipeline 时还是会从零拉,后面就快了。
一些零碎
| 现象 | 原因 |
|---|---|
job 卡在 prepare 阶段 | 镜像太大或网络拉镜像慢,换 Alpine 试试 |
ERROR: Job failed: exit code 137 | OOM 被杀,加 resource_requests.memory |
yaml: line 1: did not find expected key | YAML 缩进错位,八成是混用了 Tab |
| 同一个 job 跑两次 | retry 配错了,或者上游 job 有 parallel: 2 触发了重试 |
总结
CI 本质是把本地能跑通的命令串成流水线。所以:
- 本地怎么跑,CI 就怎么写;
- 不确定的步骤,先在本地 shell 里跑通;
- 失败要可重试,成功要可缓存;
- YAML 越薄越好,逻辑下沉到脚本。
把这四条当底线,流水线就不会太难维护。