5. Shell 脚本与可维护自动化:可复用、可观测、可失败
从“能跑”到“可维护”的脚本工程化。
本章目标
把脚本从“能跑就行”升级为“可维护、可观测、可失败、可回滚”的自动化组件。
你会掌握
严格模式、错误处理、日志、幂等、参数解析、重试与退避、清理与回滚,以及常见脚本坑的规避。
真实收益
CI/CD 里脚本失败时,你能快速定位原因;更关键的是,你能提前写出“不容易失败”的脚本。
DevOps 里的脚本像“机械臂”:它们不是在展示优雅,而是在重复做危险的事情。机械臂做错一次,你可能只是浪费 5 分钟;做错一千次,事故就会变成规律。
所以本章的核心不是“写更多脚本”,而是“让脚本像工程一样可靠”。
所以本章的核心不是“写更多脚本”,而是“让脚本像工程一样可靠”。
1) 可维护脚本的四个目标(比语法更重要)
Observable可观测
失败时能快速回答:在哪一步、用的什么参数、输出了什么证据。没有日志的脚本,就是黑盒。
FailFast可失败
错误要尽早且明确地暴露。不要吞错、不要继续“带病运行”。
Idempotent幂等
同一操作重复执行不会把系统越弄越乱。幂等是 CI/CD 的底座:重跑是常态,不是例外。
Rollbackable可回滚
你不可能永远一次成功。脚本要能在失败时清理现场,并尽量恢复到可预测状态。
2) 严格模式:把“隐形错误”变成“显性错误”
很多脚本的灾难来自“默认宽松”:变量为空、命令失败、管道中间失败……脚本却继续跑,最后坏得很难追溯。
推荐的最小严格模式(bash):
set -euo pipefail
IFS=$'\\n\\t'
-e:命令失败就退出(避免带病运行)-u:未定义变量直接报错(避免“空变量删库”)-o pipefail:管道中任一命令失败都算失败
脚本工程化的一句箴言:宁可早失败,也不要晚爆炸。
早失败意味着“离根因近”,晚爆炸意味着“你只能看到碎片”。
早失败意味着“离根因近”,晚爆炸意味着“你只能看到碎片”。
3) 错误处理:不是“try/catch”,而是“把失败变成可读证据”
脚本的错误处理通常由三件事组成:
- 统一日志格式:让人一眼看懂阶段、时间、上下文。
- 统一退出码:让 CI/CD 能判断失败类型。
- 统一清理逻辑:失败后别留下半成品。
log() { printf '%s [%s] %s\\n' \"$(date -Is)\" \"$1\" \"$2\"; }
die() { log ERROR \"$1\"; exit 1; }
cleanup() { log INFO \"cleanup...\"; }
trap cleanup EXIT
图 1:脚本执行流与错误传播(动态)
严格模式 + trap + 统一日志,让失败沿着可预测的路径“冒泡”,而不是在暗处腐烂。
4) 幂等:让“重跑”变得安全
CI/CD 里重跑很常见:网络抖一下、Runner 重启、依赖仓库短暂不可用。脚本如果不幂等,重跑就会把系统越弄越乱。
- 写入前先检查:存在就跳过,不存在才创建。
- 用“期望状态”思维:把系统收敛到某个状态,而不是执行一串不可逆动作。
- 生成式资源要可追溯:文件名/目录名包含版本或唯一 ID,避免覆盖误伤。
图 2:幂等 + 重试 + 退避(动态)
网络型失败通常是暂态:重试能救命;但重试必须建立在幂等之上,否则你是在稳定地制造事故。
5) 参数解析:脚本的“接口设计”
脚本越像产品,越要像 API 一样对待:输入可校验、默认可预测、输出可读、错误可定位。
- 清晰的 usage:错误时告诉用户怎么用。
- 默认值合理:避免必须填一堆参数才能跑。
- 危险操作需要确认:例如删除、覆盖、生产环境操作。
usage() {
cat <<'EOF'
Usage:
deploy.sh --env <dev|staging|prod> --version <v> [--dry-run]
EOF
}
ENV=""
VERSION=""
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--env) ENV="$2"; shift 2;;
--version) VERSION="$2"; shift 2;;
--dry-run) DRY_RUN=1; shift;;
-h|--help) usage; exit 0;;
*) echo "Unknown arg: $1" >&2; usage; exit 2;;
esac
done
[[ -n "$ENV" ]] || { echo "Missing --env" >&2; exit 2; }
[[ -n "$VERSION" ]] || { echo "Missing --version" >&2; exit 2; }
6) 日志:让脚本像“事故记录仪”
推荐把日志分级:INFO(关键节点)、WARN(可继续但不理想)、ERROR(不可继续)。并把关键上下文输出出来:
- 运行环境(env、region、cluster)
- 目标版本(artifact digest/commit)
- 关键依赖(访问的 URL、端口)
- 耗时(每一步的 duration)
一个极实用的技巧:每个阶段输出一行“阶段开始”,结束输出“阶段完成 + 耗时”。
当脚本卡住,你不用猜“卡在哪”,日志会告诉你。
当脚本卡住,你不用猜“卡在哪”,日志会告诉你。
7) 清理与回滚:失败不可怕,现场不可控才可怕
建议把脚本输出分成两类:
- 临时产物:随时可删,失败必须清理(临时目录、下载文件)。
- 关键变更:要么成功,要么回到可预测状态(符号链接切换、配置写入)。
图 3:日志链路与证据(动态)
脚本输出的每一行日志,都是你未来排障的“时间胶囊”:它应该指向真实世界的对象与状态。
本章小结:脚本是 DevOps 的“微服务”
- 严格模式让隐形错误显性化;Fail fast 让你离根因更近。
- 幂等让重跑安全;重试要有边界与证据。
- 日志与退出码让 CI/CD 可判定、可追溯、可复现。
- 清理与回滚让失败可控:失败不可怕,现场不可控才可怕。