自动化部署脚本:模板与最佳实践 | Automated Deployment Scripts: Templates & Best Practices

章节概述

核心理念:生产环境的部署脚本不是”能跑就行”的简单命令集合,而是需要健壮的错误处理、完整的日志记录、清晰的参数解析和优雅的回滚机制。好的部署脚本应该像 C 语言的健壮程序一样,处理所有边界情况。


第1节:脚本模板

1.1 标准脚本头

#!/usr/bin/env bash
#
# deploy.sh - 应用部署脚本
# Version: 2.1.0
# Author: DevOps Team
# Description: 自动化部署应用到目标服务器
#
 
set -euo pipefail
IFS=$'\n\t'

1.2 set -euo pipefail 详解

# -e: 命令失败时立即退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任一命令失败则整个管道失败
# IFS=$'\n\t': 设置字段分隔符为换行和制表符(安全处理文件名)
 
# 对比 C 语言的防御性编程
# C: if (ptr == NULL) { return -1; }
# Bash: set -e(自动捕获失败)
选项作用不设置的后果
-e命令失败立即退出错误被忽略,继续执行
-u使用未定义变量报错变量名拼写错误不被发现
-o pipefail管道失败传播管道中中间命令失败被忽略

1.3 trap 错误处理

# 捕获错误并清理
cleanup() {
    local exit_code=$?
    echo "[ERROR] Script failed at line $1"
    # 清理临时文件
    rm -rf "${TEMP_DIR:-}"
    # 恢复配置文件
    [ -f "$BACKUP_FILE" ] && cp "$BACKUP_FILE" "$CONFIG_FILE"
    exit $exit_code
}
 
trap 'cleanup $LINENO' ERR EXIT INT TERM
 
# 临时目录管理
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

1.4 日志函数

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
 
# 日志函数
log_info()    { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_warn()    { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_error()   { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
log_debug()   { [ "$VERBOSE" = "true" ] && echo -e "${BLUE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $*"; }
 
# 带时间戳的日志写入文件
exec > >(tee -a "$LOG_FILE") 2>&1

第2节:参数解析(getopts)

2.1 基础 getopts

# 定义选项
ENV="staging"
VERBOSE="false"
DRY_RUN="false"
VERSION=""
 
# 解析参数
while getopts "e:vdn:h" opt; do
    case $opt in
        e) ENV="$OPTARG" ;;
        v) VERBOSE="true" ;;
        d) DRY_RUN="true" ;;
        n) VERSION="$OPTARG" ;;
        h)
            echo "Usage: $0 [-e env] [-v] [-d] [-n version] [-h]"
            echo "  -e  目标环境 (staging|production)"
            echo "  -v  详细输出"
            echo "  -n  版本号"
            echo "  -d  干运行模式"
            echo "  -h  显示帮助"
            exit 0
            ;;
        \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
        :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
    esac
done
shift $((OPTIND - 1))
 
# 验证必填参数
if [ -z "$VERSION" ]; then
    log_error "Version number is required (-n)"
    exit 1
fi

2.2 手动参数解析(更灵活)

# 支持长选项的参数解析
while [[ $# -gt 0 ]]; do
    case $1 in
        --env|-e)
            ENV="$2"
            shift 2
            ;;
        --verbose|-v)
            VERBOSE="true"
            shift
            ;;
        --dry-run|-d)
            DRY_RUN="true"
            shift
            ;;
        --version|-n)
            VERSION="$2"
            shift 2
            ;;
        --help|-h)
            show_help
            exit 0
            ;;
        *)
            log_error "Unknown option: $1"
            exit 1
            ;;
    esac
done

第3节:错误处理与验证

3.1 系统检查

# 检查运行用户
if [ "$(id -u)" -ne 0 ]; then
    log_error "This script must be run as root"
    exit 1
fi
 
# 检查必要命令
check_command() {
    if ! command -v "$1" &>/dev/null; then
        log_error "Required command not found: $1"
        exit 1
    fi
}
 
check_command docker
check_command nginx
check_command systemctl
 
# 检查磁盘空间
check_disk_space() {
    local required_mb=$1
    local available_mb=$(df -m / | awk 'NR==2 {print $4}')
    if [ "$available_mb" -lt "$required_mb" ]; then
        log_error "Insufficient disk space: ${available_mb}MB available, ${required_mb}MB required"
        exit 1
    fi
}
 
check_disk_space 1024  # 需要至少 1GB

3.2 健壮的命令执行

# 带重试的命令执行
retry() {
    local max_attempts=$1
    local delay=$2
    shift 2
    local cmd=("$@")
 
    for ((i = 1; i <= max_attempts; i++)); do
        if "${cmd[@]}"; then
            return 0
        fi
        log_warn "Attempt $i/$max_attempts failed. Retrying in ${delay}s..."
        sleep "$delay"
    done
 
    log_error "Command failed after $max_attempts attempts: ${cmd[*]}"
    return 1
}
 
# 使用示例
retry 3 5 docker pull myapp:latest

第4节:部署脚本示例

4.1 完整部署脚本

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
 
# 配置
APP_NAME="myapp"
DEPLOY_DIR="/opt/$APP_NAME"
BACKUP_DIR="/opt/backups"
LOG_FILE="/var/log/deploy_${APP_NAME}.log"
DOCKER_IMAGE="registry.example.com/$APP_NAME"
HEALTH_URL="http://localhost:8080/health"
 
# 日志函数
log_info()  { echo -e "\033[0;32m[INFO]\033[0m $(date '+%Y-%m-%d %H:%M:%S') $*"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $(date '+%Y-%m-%d %H:%M:%S') $*" >&2; }
 
# 参数解析
VERSION=""
ENV="staging"
DRY_RUN=false
 
while [[ $# -gt 0 ]]; do
    case $1 in
        -v|--version) VERSION="$2"; shift 2 ;;
        -e|--env) ENV="$2"; shift 2 ;;
        -d|--dry-run) DRY_RUN=true; shift ;;
        *) echo "Unknown: $1"; exit 1 ;;
    esac
done
 
[ -z "$VERSION" ] && { log_error "Version required (-v)"; exit 1; }
 
# 错误处理
cleanup() {
    local exit_code=$?
    if [ $exit_code -ne 0 ]; then
        log_error "Deployment failed! Check log: $LOG_FILE"
        # 回滚逻辑
        if [ -d "$BACKUP_DIR/latest" ]; then
            log_info "Rolling back to previous version..."
            rsync -a "$BACKUP_DIR/latest/" "$DEPLOY_DIR/"
            systemctl restart "$APP_NAME"
        fi
    fi
    exit $exit_code
}
trap cleanup EXIT
 
# 主流程
main() {
    log_info "Starting deployment: $APP_NAME v$VERSION ($ENV)"
 
    # 1. 预检查
    log_info "Step 1/6: Pre-flight checks"
    [ "$(id -u)" -eq 0 ] || { log_error "Run as root"; exit 1; }
    command -v docker >/dev/null || { log_error "docker not found"; exit 1; }
    df -m / | awk 'NR==2 {if($4<1024) exit 1}' || { log_error "Low disk space"; exit 1; }
 
    # 2. 备份当前版本
    log_info "Step 2/6: Backing up current version"
    mkdir -p "$BACKUP_DIR"
    rsync -a "$DEPLOY_DIR/" "$BACKUP_DIR/latest/"
 
    # 3. 拉取新镜像
    log_info "Step 3/6: Pulling new image"
    if [ "$DRY_RUN" = false ]; then
        docker pull "$DOCKER_IMAGE:$VERSION"
    else
        log_info "[DRY RUN] Would pull: $DOCKER_IMAGE:$VERSION"
    fi
 
    # 4. 停止旧服务
    log_info "Step 4/6: Stopping old service"
    systemctl stop "$APP_NAME" || true
 
    # 5. 启动新服务
    log_info "Step 5/6: Starting new service"
    if [ "$DRY_RUN" = false ]; then
        docker run -d \
            --name "$APP_NAME" \
            --restart unless-stopped \
            -p 8080:8080 \
            -v "$DEPLOY_DIR/data:/app/data" \
            "$DOCKER_IMAGE:$VERSION"
    fi
 
    # 6. 健康检查
    log_info "Step 6/6: Health check"
    if [ "$DRY_RUN" = false ]; then
        for i in {1..30}; do
            if curl -sf "$HEALTH_URL" >/dev/null; then
                log_info "Health check passed!"
                log_success "Deployment completed successfully!"
                return 0
            fi
            sleep 2
        done
        log_error "Health check failed after 60 seconds"
        exit 1
    fi
}
 
main "$@"

第5节:最佳实践总结

实践说明
set -euo pipefail必须在脚本开头设置
trap 清理捕获 EXIT/ERR/INT/TERM
日志函数统一格式,区分级别
参数验证解析后立即验证必填项
干运行模式-d 选项预览操作
幂等性重复执行结果一致
备份机制操作前备份,失败时回滚
健康检查部署后验证服务状态
超时机制网络操作设置超时
并发锁防止多实例同时部署