5. Shell 脚本与可维护自动化:可复用、可观测、可失败

从“能跑”到“可维护”的脚本工程化。

本章目标

把脚本从“能跑就行”升级为“可维护、可观测、可失败、可回滚”的自动化组件。

你会掌握

严格模式、错误处理、日志、幂等、参数解析、重试与退避、清理与回滚,以及常见脚本坑的规避。

真实收益

CI/CD 里脚本失败时,你能快速定位原因;更关键的是,你能提前写出“不容易失败”的脚本。

DevOps 里的脚本像“机械臂”:它们不是在展示优雅,而是在重复做危险的事情。机械臂做错一次,你可能只是浪费 5 分钟;做错一千次,事故就会变成规律。
所以本章的核心不是“写更多脚本”,而是“让脚本像工程一样可靠”。

1) 可维护脚本的四个目标(比语法更重要)

Observable可观测

失败时能快速回答:在哪一步用的什么参数输出了什么证据。没有日志的脚本,就是黑盒。

FailFast可失败

错误要尽早明确地暴露。不要吞错、不要继续“带病运行”。

Idempotent幂等

同一操作重复执行不会把系统越弄越乱。幂等是 CI/CD 的底座:重跑是常态,不是例外。

Rollbackable可回滚

你不可能永远一次成功。脚本要能在失败时清理现场,并尽量恢复到可预测状态。

2) 严格模式:把“隐形错误”变成“显性错误”

很多脚本的灾难来自“默认宽松”:变量为空、命令失败、管道中间失败……脚本却继续跑,最后坏得很难追溯。

推荐的最小严格模式(bash):

set -euo pipefail
IFS=$'\\n\\t'
脚本工程化的一句箴言:宁可早失败,也不要晚爆炸。
早失败意味着“离根因近”,晚爆炸意味着“你只能看到碎片”。

3) 错误处理:不是“try/catch”,而是“把失败变成可读证据”

脚本的错误处理通常由三件事组成:

  1. 统一日志格式:让人一眼看懂阶段、时间、上下文。
  2. 统一退出码:让 CI/CD 能判断失败类型。
  3. 统一清理逻辑:失败后别留下半成品。
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 + 统一日志,让失败沿着可预测的路径“冒泡”,而不是在暗处腐烂。

Parse Args 参数校验 / 默认值 Preflight 环境/权限/依赖检查 Do Work 执行核心动作 Verify & Report 验证 + 输出证据 trap cleanup 失败/成功都收尾:清理临时文件 失败时:回滚/解锁/释放资源 读图要点:先检查,再执行;执行后验证;任何阶段失败都要产出“可读证据”。

4) 幂等:让“重跑”变得安全

CI/CD 里重跑很常见:网络抖一下、Runner 重启、依赖仓库短暂不可用。脚本如果不幂等,重跑就会把系统越弄越乱。

图 2:幂等 + 重试 + 退避(动态)

网络型失败通常是暂态:重试能救命;但重试必须建立在幂等之上,否则你是在稳定地制造事故。

Desired State “最终要变成什么样” 存在 → 对齐;不存在 → 创建 Retry 暂态失败 → 重试 限制次数 + 退避 Evidence 失败也要留证据 便于定位与复现 要点:重试不是“多试几次”,而是“有边界、有证据、有幂等”。

5) 参数解析:脚本的“接口设计”

脚本越像产品,越要像 API 一样对待:输入可校验、默认可预测、输出可读、错误可定位。

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(不可继续)。并把关键上下文输出出来:

一个极实用的技巧:每个阶段输出一行“阶段开始”,结束输出“阶段完成 + 耗时”。
当脚本卡住,你不用猜“卡在哪”,日志会告诉你。

7) 清理与回滚:失败不可怕,现场不可控才可怕

建议把脚本输出分成两类:

图 3:日志链路与证据(动态)

脚本输出的每一行日志,都是你未来排障的“时间胶囊”:它应该指向真实世界的对象与状态。

Script 阶段日志 + 上下文 输入/输出/耗时 CI/CD job logs / artifacts 可追溯到一次运行 Observability log search / alert 关联发布与故障 要点:把日志当“证据链”,而不是当“输出垃圾桶”。

本章小结:脚本是 DevOps 的“微服务”

← 上一章:网络基础 下一章:Git 基础 1 →