Shell脚本如何实现安全回滚策略

wen 实用脚本 25

本文目录导读:

Shell脚本如何实现安全回滚策略

  1. 基础回滚结构
  2. 带检查点的安全部署
  3. 高级回滚:版本化目录结构
  4. 信号处理和异常退出
  5. 实用回滚工具函数
  6. 使用建议

在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
}

使用建议

  1. 始终备份原始状态:在执行任何修改前创建完整备份
  2. 使用原子操作:通过符号链接切换实现零停机部署
  3. 保留足够的历史:至少保留3-5个历史版本
  4. 自动验证:部署后自动进行健康检查
  5. 日志记录:详细记录所有操作和回滚过程
  6. 信号处理:处理中断信号,保证异常时也能回滚

这个框架可以根据具体需求调整,关键是保持操作的可逆性原子性

抱歉,评论功能暂时关闭!