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 专题从命令基础、文本处理、进程信号、健壮性、工程化、并行、远程、定时、网络、性能,一路走到交互自动化与部署发布,形成了完整的脚本工程能力闭环。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。