部署与发布脚本:版本、回滚与健康检查

系统讲解发布脚本的目标与原则、构建产物管理与版本号语义化、部署步骤编排、健康检查与自动回滚,以及发布门禁与审批流的设计实现。

1. 发布脚本的目标与原则

一句话总结: 发布脚本把「手工操作」变成「可重复、可回滚、可观测」的确定性流程,核心原则是幂等、失败即停、全程留痕。

一次发布包含构建、迁移、启动、验证多个环节,任何一个失败都可能让线上受损。

#!/usr/bin/env bash
set -euo pipefail   # 任一环节失败即停

# 发布前检查环境
[ -d /opt/app ] || { echo "目标目录不存在"; exit 1; }
[ -f deploy.env ] && { echo "缺少 deploy.env"; exit 1; }

echo "开始发布 $(date '+%F %T')"

1.1 发布脚本应回答的问题

每个发布脚本都要能回答四个问题:部署什么、部署到哪、如何验证、如何回退。

#!/usr/bin/env bash
set -euo pipefail
# 部署什么: 版本/分支
APP_NAME="web"
VERSION="${VERSION:-$(git describe --tags --abbrev=0)}"
# 部署到哪: 主机/目录
TARGET="/opt/$APP_NAME"
# 如何验证: 健康检查地址
HEALTH_URL="http://localhost:8080/health"
# 如何回退: 保留上一版本目录
echo "目标: $TARGET, 版本: $VERSION, 检查: $HEALTH_URL"

一句话总结: 把「版本、目标、健康检查、回退策略」四个参数显式化,脚本才具备可评审、可移植的基础。

1.2 幂等与确定性

同一条命令重复执行结果一致,才允许重试与自动化触发。

#!/usr/bin/env bash
set -euo pipefail
# 创建目录用 -p,重复执行不报错
mkdir -p /opt/app/bin

# 复制用覆盖 + 保留权限
install -m 0755 ./app /opt/app/bin/app

# 写入配置用「临时文件 + mv」保证原子
cat > /tmp/app.conf <<'EOF'
port=8080
EOF
mv /tmp/app.conf /opt/app/app.conf

2. 构建产物管理

一句话总结: 构建产物按「版本目录」存放、用软链切换当前版本,发布即切换软链、回滚即指回旧链,这是最经典的零停机部署模型。

#!/usr/bin/env bash
set -euo pipefail

VERSION="${1:?用法: deploy.sh <版本>}"
RELEASES_DIR=/opt/app/releases
CURRENT=/opt/app/current

# 1. 发布目录: 版本号命名的完整拷贝
mkdir -p "$RELEASES_DIR/$VERSION"
cp -r ./dist/* "$RELEASES_DIR/$VERSION/"

# 2. 切换软链: 原子更新当前版本
ln -sfn "$RELEASES_DIR/$VERSION" "$CURRENT"
ls -l "$CURRENT"

2.1 保留策略与磁盘控制

#!/usr/bin/env bash
set -euo pipefail
# 保留最近 5 个版本,删除更旧的
ls -1d /opt/app/releases/v* \
  | sort -V \
  | head -n -5 \
  | xargs -r rm -rf
echo "当前 release 目录:"
ls -1 /opt/app/releases/

一句话总结: 版本目录 + 软链 + 保留 N 份,是部署空间管理与快速回滚的物理基础。

3. 版本号与语义化

一句话总结: 语义化版本 主.次.补丁 承载发布语义,脚本负责读取、递增、校验与写入,避免人工改错。

#!/usr/bin/env bash
set -euo pipefail
# 校验版本号格式
ver="v1.2.3"
if [[ ! "$ver" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "版本号格式非法: $ver"; exit 1
fi

# 递增 patch
IFS='.' read -r _ minor patch <<< "${ver#v}"
new_patch=$((patch + 1))
echo "v1.$minor.$new_patch"

3.1 版本号与构建元数据

#!/usr/bin/env bash
set -euo pipefail
# 支持 build 元数据与预发布标记
VERSION="${VERSION:-$(date +%Y.%m.%d)}"
COMMIT_SHORT=$(git rev-parse --short HEAD)
BUILD="${VERSION}+${COMMIT_SHORT}"
echo "本次构建: $BUILD"

# 预发布: alpha/beta/rc
if [ "${PRERELEASE:-false}" = "true" ]; then
  TAG="$VERSION-rc.$(git rev-list --count HEAD)"
  echo "预发布标签: $TAG"
fi

一句话总结: 正式版本用 主.次.补丁,CI 构建产物加 +提交号,预发布加 -rc.N,三者可脚本化生成。

3.2 版本文件与标签同步

#!/usr/bin/env bash
set -euo pipefail
ver=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
# 写入版本文件
echo "$ver" > VERSION
# 生成带时间戳的展示版本
cat > /opt/app/VERSION.json <<EOF
{"version": "$ver", "built_at": "$(date -Iseconds)"}
EOF

4. 部署步骤编排

一句话总结: 部署编排遵循「构建 → 迁移 → 放置 → 启动 → 验证」的顺序,每步独立成函数并记录日志,失败即可定位。

#!/usr/bin/env bash
set -euo pipefail

step() { printf '\n=== %s ===\n' "$*"; }

build() {
  step "构建"
  make build || return 1
}

migrate() {
  step "数据库迁移"
  ./bin/migrate up || return 1
}

start() {
  step "启动服务"
  systemctl restart "$APP_NAME" || return 1
}

verify() {
  step "验证服务"
  curl -fsS "$HEALTH_URL" || return 1
}

build && migrate && start && verify
echo "部署成功"

一句话总结: && 串联的短接求值天然实现「失败即停」,每步函数化让日志与报错有清晰归属。

4.1 失败时跳到回滚

#!/usr/bin/env bash
set -euo pipefail

deploy() {
  build || return 1
  migrate || return 1
  start  || return 1
  verify || return 1
}

if deploy; then
  echo "发布成功"
else
  echo "发布失败, 执行回滚" >&2
  rollback
  exit 1
fi

4.2 发布窗口与超时

#!/usr/bin/env bash
set -euo pipefail
# 每个阶段限时,防止卡死
timeout 120 ./bin/migrate up || { echo "迁移超时"; exit 1; }
timeout 30 systemctl start "$APP_NAME" || { echo "启动超时"; exit 1; }
echo "各阶段限时完成"

5. 健康检查与探活

一句话总结: 健康检查是发布成功的唯一判据:用 curl 带重试与超时探测健康端点,连续失败即判定发布失败并触发回滚。

#!/usr/bin/env bash
set -euo pipefail
# 带重试的健康检查
check_health() {
  local url="$1" tries="${2:-10}" delay="${3:-2}"
  for ((i=1; i<=tries; i++)); do
    if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then
      echo "健康检查通过 (第 ${i} 次)"
      return 0
    fi
    echo "第 ${i} 次检查失败, ${delay}s 后重试"
    sleep "$delay"
  done
  echo "健康检查失败" >&2
  return 1
}
check_health "http://localhost:8080/health" 5 2

5.1 多指标健康检查

#!/usr/bin/env bash
set -euo pipefail
# 检查状态码、响应体与进程
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health)
[ "$code" = "200" ] || { echo "HTTP $code"; exit 1; }

body=$(curl -s http://localhost:8080/health)
echo "$body" | grep -q '"status":"ok"' || { echo "状态体异常"; exit 1; }

pgrep -f "$APP_NAME" >/dev/null || { echo "进程不存在"; exit 1; }
echo "三项检查全部通过"

一句话总结: 单一 200 不够,要「HTTP 码 + 响应内容 + 进程存在」三重确认,才敢判定发布成功。

5.2 平滑启动窗口

#!/usr/bin/env bash
set -euo pipefail
# 新版本启动后给足预热时间再切换流量
systemctl start "$APP_NAME"
sleep 10   # 预热
check_health "$HEALTH_URL" || { systemctl stop "$APP_NAME"; exit 1; }
ln -sfn "$RELEASES_DIR/$VERSION" "$CURRENT"
echo "流量已切换至 $VERSION"

6. 回滚机制

一句话总结: 回滚分两级:软链回退到上一版本(秒级、零停机),或整目录恢复备份(用于数据变更事故)。

#!/usr/bin/env bash
set -euo pipefail
# 软链回滚: 回到上一个版本
rollback() {
  local releases=(/opt/app/releases/v*)
  local last
  last=$(printf '%s\n' "${releases[@]}" | sort -V | tail -n 2 | head -n 1)
  [ -n "$last" ] || { echo "无可用回滚版本"; return 1; }
  ln -sfn "$last" /opt/app/current
  echo "已回滚到 $(basename "$last")"
  systemctl restart "$APP_NAME"
  check_health "$HEALTH_URL" || { echo "回滚后健康检查失败"; return 1; }
}
rollback

6.1 回滚前快照

#!/usr/bin/env bash
set -euo pipefail
# 发布前备份当前版本,回滚可精确恢复
BACKUP_DIR=/opt/app/backups
snapshot() {
  local ver="${1:-pre_$(date +%Y%m%d%H%M%S)}"
  cp -a /opt/app/current "$BACKUP_DIR/$ver"
  echo "已快照当前版本: $ver"
}
snapshot

一句话总结: 快照 + 软链双保险:快照保证「随时可回到发布前一刻」,软链保证「回退瞬间完成」。

7. 发布门禁与审批

一句话总结: 发布门禁 = 前置校验(分支/环境/变更)+ 人工确认 + 环境隔离,把误发风险挡在真正执行之前。

#!/usr/bin/env bash
set -euo pipefail
# 前置门禁
check_gate() {
  local env="${1:?缺少环境参数: prod/staging}"
  [ "$env" = "prod" ] || [ "$env" = "staging" ] || { echo "非法环境"; return 1; }

  branch=$(git symbolic-ref --short HEAD)
  [ "$branch" = "main" ] || { echo "仅允许 main 分支发布"; return 1; }

  git diff --quiet || { echo "工作区不干净"; return 1; }
  echo "门禁检查通过"
}
check_gate "${ENV:-staging}"

7.1 人工确认发布

#!/usr/bin/env bash
set -euo pipefail
confirm() {
  local env="$1" version="$2"
  read -r -p "确认向 ${env} 发布 ${version}? [yes/NO] " ans
  [ "$ans" = "yes" ] || { echo "已取消"; exit 1; }
}
confirm "生产环境" "v1.2.0"

一句话总结: 高危环境强制人工二次确认,yes/NO 默认否定,防止脚本在无人看管时误发。

7.2 CI 环境隔离

#!/usr/bin/env bash
set -euo pipefail
# 仅 tag 触发生产发布
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
  ENV=prod
else
  ENV=staging
fi
echo "发布到 $ENV"

# 干跑模式验证脚本本身
if [ "${DRY_RUN:-false}" = "true" ]; then
  echo "[干跑] 跳过实际部署"
  exit 0
fi

8. 总结

环节要点
原则幂等、失败即停、全程留痕、可回滚
产物管理版本目录 + 软链 + 保留 N 份
版本号主.次.补丁,加 +提交号/-rc.N 元数据
步骤编排构建→迁移→放置→启动→验证,函数化 + &&
健康检查HTTP 码 + 响应体 + 进程三重确认,重试探活
回滚软链秒级回退 + 快照整目录恢复双保险
门禁分支/环境/工作区校验 + 人工确认
审计时间、操作者、环境、版本写入日志

部署与发布脚本是把前面所有 Shell 能力凝结的终点:变量与流程控制做编排、jq 解析健康响应、ANSI 与交互做发布确认、文件锁与原子写保证安全、Git 自动化管版本。发布不是「一条命令跑完」,而是「每一步都可验证、可回退、可追溯」。至此 shell 专题从命令基础、文本处理、进程信号、健壮性、工程化、并行、远程、定时、网络、性能,一路走到交互自动化与部署发布,形成了完整的脚本工程能力闭环。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 任务编排与 Makefile 实战
  2. 文件监控与事件驱动流水线实战
  3. 结构化数据清洗与报表生成实战