本文目录导读:

在Shell脚本中实现安全回滚策略的核心是事务性操作和状态管理,以下是一个完整的多级回滚框架:
基础回滚结构
#!/bin/bash
set -euo pipefail # 严格模式
# 日志与状态文件
LOG_FILE="/var/log/deploy.log"
STATE_FILE="/tmp/deploy_state"
ROLLBACK_DIR="/tmp/rollback_backup"
# 初始化回滚环境
init_rollback() {
mkdir -p "$ROLLBACK_DIR"
echo "0" > "$STATE_FILE" # 0=初始, 1=进行中, 2=完成
}
# 记录回滚操作
record_operation() {
local operation="$1"
local backup_file="$2"
echo "$operation:$backup_file" >> "$ROLLBACK_DIR/manifest.txt"
}
# 执行回滚
rollback() {
echo "[ROLLBACK] 开始回滚..."
local status=$(cat "$STATE_FILE")
if [[ "$status" == "0" ]]; then
echo "[ROLLBACK] 无操作可回滚"
return 0
fi
# 反向读取manifest.txt执行回滚
tac "$ROLLBACK_DIR/manifest.txt" | while IFS=':' read -r op backup; do
case "$op" in
"backup_file")
cp "$backup" "${backup%.bak}"
echo "[ROLLBACK] 恢复文件: ${backup%.bak}"
;;
"backup_dir")
rm -rf "${backup%.bak}"
cp -r "$backup" "${backup%.bak}"
echo "[ROLLBACK] 恢复目录: ${backup%.bak}"
;;
"execute_cmd")
eval "$backup" # 逆操作命令
echo "[ROLLBACK] 执行逆操作: $backup"
;;
esac
done
echo "0" > "$STATE_FILE"
echo "[ROLLBACK] 回滚完成"
}
带检查点的安全部署
#!/bin/bash
# 检查点管理
CHECKPOINT_DIR="/tmp/checkpoints"
init_checkpoints() {
rm -rf "$CHECKPOINT_DIR"
mkdir -p "$CHECKPOINT_DIR"
}
save_checkpoint() {
local name="$1"
local timestamp=$(date +%s)
# 保存需要恢复的信息
cat > "$CHECKPOINT_DIR/${name}.ckpt" <<EOF
timestamp=$timestamp
description=$2
EOF
# 记录当前文件系统状态
find /var/www -type f -newer "$CHECKPOINT_DIR/start" \
> "$CHECKPOINT_DIR/${name}_files.txt" 2>/dev/null || true
}
restore_checkpoint() {
local name="$1"
if [[ ! -f "$CHECKPOINT_DIR/${name}.ckpt" ]]; then
echo "错误: 检查点 $name 不存在"
return 1
fi
# 恢复文件
while IFS= read -r file; do
if [[ -f "${file}.bak" ]]; then
cp "${file}.bak" "$file"
echo "恢复: $file"
fi
done < "$CHECKPOINT_DIR/${name}_files.txt"
# 执行自定义恢复脚本
if [[ -f "$CHECKPOINT_DIR/${name}_restore.sh" ]]; then
bash "$CHECKPOINT_DIR/${name}_restore.sh"
fi
}
# 示例部署函数
deploy_application() {
init_checkpoints
# 阶段1: 备份当前状态
echo "阶段1: 备份..."
cp -r /var/www/app /var/www/app.bak
save_checkpoint "backup" "备份原有应用"
# 阶段2: 停止服务
echo "阶段2: 停止服务..."
systemctl stop nginx
save_checkpoint "stop_nginx" "停止Nginx"
# 阶段3: 更新代码
echo "阶段3: 更新代码..."
cd /var/www/app
git pull origin main || {
echo "更新失败,触发回滚"
restore_checkpoint "stop_nginx"
restore_checkpoint "backup"
systemctl start nginx
exit 1
}
save_checkpoint "update_code" "更新代码"
# 阶段4: 数据库迁移
echo "阶段4: 数据库迁移..."
php artisan migrate --force || {
echo "数据库迁移失败,触发回滚"
restore_checkpoint "update_code"
restore_checkpoint "stop_nginx"
restore_checkpoint "backup"
systemctl start nginx
exit 1
}
save_checkpoint "migrate" "数据库迁移"
# 阶段5: 启动服务并验证
echo "阶段5: 启动服务..."
systemctl start nginx
# 健康检查
for i in {1..10}; do
if curl -f http://localhost/health; then
echo "部署成功!"
save_checkpoint "success" "部署成功"
return 0
fi
sleep 3
done
# 健康检查失败
echo "健康检查失败,自动回滚"
restore_checkpoint "migrate"
restore_checkpoint "update_code"
restore_checkpoint "stop_nginx"
restore_checkpoint "backup"
systemctl start nginx
return 1
}
高级回滚:版本化目录结构
#!/bin/bash
# 版本化部署目录
DEPLOY_DIR="/opt/app"
VERSIONS_DIR="${DEPLOY_DIR}/versions"
CURRENT_LINK="${DEPLOY_DIR}/current"
ROLLBACK_COUNT=5 # 保留5个历史版本
# 创建新版本
create_version() {
local version="v$(date +%Y%m%d_%H%M%S)"
local target="${VERSIONS_DIR}/${version}"
mkdir -p "$target"
echo "$version" > "${target}/.version"
# 复制当前版本
if [[ -L "$CURRENT_LINK" ]]; then
cp -r "$(readlink -f $CURRENT_LINK)/." "$target/"
fi
echo "$version"
}
# 部署新版本
deploy_version() {
local version="$1"
local target="${VERSIONS_DIR}/${version}"
# 原子切换
ln -sfn "$target" "${CURRENT_LINK}.tmp"
mv -T "${CURRENT_LINK}.tmp" "$CURRENT_LINK"
# 清理旧版本
cleanup_old_versions
}
# 回滚到指定版本
rollback_to_version() {
local version="$1"
local target="${VERSIONS_DIR}/${version}"
if [[ ! -d "$target" ]]; then
echo "版本 $version 不存在"
return 1
fi
# 原子切换
ln -sfn "$target" "${CURRENT_LINK}.tmp"
mv -T "${CURRENT_LINK}.tmp" "$CURRENT_LINK"
echo "已回滚到版本: $version"
}
# 清理旧版本
cleanup_old_versions() {
local versions=($(ls -1t "$VERSIONS_DIR" 2>/dev/null))
local count=${#versions[@]}
if (( count > ROLLBACK_COUNT )); then
local remove_count=$(( count - ROLLBACK_COUNT ))
for (( i=ROLLBACK_COUNT; i<count; i++ )); do
rm -rf "${VERSIONS_DIR}/${versions[i]}"
echo "删除旧版本: ${versions[i]}"
done
fi
}
# 完整部署流程
safe_deploy() {
echo "=== 开始安全部署 ==="
# 1. 创建新版本
local version=$(create_version)
echo "创建版本: $version"
# 2. 部署代码到新版本目录
cd "${VERSIONS_DIR}/${version}"
git clone --depth=1 https://github.com/your/repo.git . || {
echo "代码下载失败"
rm -rf "${VERSIONS_DIR}/${version}"
exit 1
}
# 3. 运行测试
if ! make test; then
echo "测试失败,自动清理"
rm -rf "${VERSIONS_DIR}/${version}"
exit 1
fi
# 4. 创建配置文件备份
if [[ -L "$CURRENT_LINK" ]]; then
cp "$(readlink -f $CURRENT_LINK)/.env" "${VERSIONS_DIR}/${version}/.env"
fi
# 5. 执行原子切换
deploy_version "$version"
# 6. 健康检查
sleep 5
if curl -f http://localhost; then
echo "部署成功!当前版本: $version"
else
echo "健康检查失败,启动回滚..."
local prev_version=$(ls -1t "$VERSIONS_DIR" | sed -n '2p')
if [[ -n "$prev_version" ]]; then
rollback_to_version "$prev_version"
echo "已回滚到版本: $prev_version"
fi
fi
echo "=== 部署完成 ==="
}
信号处理和异常退出
#!/bin/bash
# 全局回滚标志
ROLLBACK_REQUIRED=false
CURRENT_STEP=""
# 信号处理
cleanup() {
echo ""
echo "收到中断信号!"
if $ROLLBACK_REQUIRED; then
echo "执行紧急回滚..."
rollback_to_last_checkpoint
fi
exit 1
}
trap cleanup SIGINT SIGTERM SIGHUP
# 步骤执行器
run_step() {
local step_name="$1"
local step_function="$2"
CURRENT_STEP="$step_name"
echo "[步骤] $step_name"
# 执行前保存状态
save_step_state "$step_name"
# 执行步骤
if ! $step_function; then
echo "[错误] 步骤 $step_name 失败"
ROLLBACK_REQUIRED=true
rollback_to_step "$step_name"
return 1
fi
echo "[完成] $step_name"
return 0
}
# 主部署流程
main() {
# 初始化
init_rollback_environment
ROLLBACK_REQUIRED=true
# 执行各步骤
run_step "备份数据库" backup_database || exit 1
run_step "更新代码" update_code || exit 1
run_step "安装依赖" install_dependencies || exit 1
run_step "数据库迁移" run_migrations || exit 1
run_step "清理缓存" clear_cache || exit 1
# 全部成功
ROLLBACK_REQUIRED=false
echo "部署成功!"
}
实用回滚工具函数
#!/bin/bash
# 文件级别回滚函数
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
cp "$file" "${file}.rollback.$(date +%s)"
echo "备份: $file"
fi
}
restore_file() {
local file="$1"
local backup=$(ls -t "${file}.rollback."* 2>/dev/null | head -1)
if [[ -n "$backup" ]]; then
cp "$backup" "$file"
echo "恢复: $file"
fi
}
# 数据库回滚
backup_database() {
local db_name="$1"
local backup_file="/tmp/${db_name}_$(date +%s).sql"
mysqldump "$db_name" > "$backup_file"
echo "$backup_file"
}
restore_database() {
local backup_file="$1"
local db_name="$2"
mysql "$db_name" < "$backup_file"
echo "数据库恢复完成"
}
# 配置管理
save_config() {
local config_file="$1"
if [[ -f "$config_file" ]]; then
cp "$config_file" "${config_file}.prev"
fi
}
restore_config() {
local config_file="$1"
if [[ -f "${config_file}.prev" ]]; then
cp "${config_file}.prev" "$config_file"
rm -f "${config_file}.prev"
fi
}
使用建议
- 始终备份原始状态:在执行任何修改前创建完整备份
- 使用原子操作:通过符号链接切换实现零停机部署
- 保留足够的历史:至少保留3-5个历史版本
- 自动验证:部署后自动进行健康检查
- 日志记录:详细记录所有操作和回滚过程
- 信号处理:处理中断信号,保证异常时也能回滚
这个框架可以根据具体需求调整,关键是保持操作的可逆性和原子性。