609 lines
18 KiB
Bash
Executable File
609 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
||
# Docker镜像和配置文件推送脚本
|
||
# 将生产环境所需的文件和镜像推送到远程服务器
|
||
|
||
set -e
|
||
|
||
# ===========================================
|
||
# 脚本配置
|
||
# ===========================================
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||
|
||
# 默认配置
|
||
DEFAULT_REMOTE_HOST=""
|
||
DEFAULT_REMOTE_USER="root"
|
||
DEFAULT_REMOTE_PATH="/root/product/anxin"
|
||
DEFAULT_SSH_PORT="22"
|
||
|
||
# 镜像配置
|
||
FRONTEND_IMAGE="anxin-frontend"
|
||
BACKEND_IMAGE="anxin-backend"
|
||
DEFAULT_TAG="latest"
|
||
|
||
# 临时目录
|
||
TEMP_DIR="/tmp/anxin-deploy-$(date +%s)"
|
||
|
||
# ===========================================
|
||
# 颜色定义
|
||
# ===========================================
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
PURPLE='\033[0;35m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# ===========================================
|
||
# 日志函数
|
||
# ===========================================
|
||
log_info() {
|
||
echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||
}
|
||
|
||
log_step() {
|
||
echo -e "${CYAN}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||
}
|
||
|
||
# ===========================================
|
||
# 帮助信息
|
||
# ===========================================
|
||
show_help() {
|
||
cat << EOF
|
||
若依框架Docker部署 - 推送脚本
|
||
|
||
用法: $0 [选项]
|
||
|
||
选项:
|
||
-h, --host HOST 远程服务器地址 (必需)
|
||
-u, --user USER 远程服务器用户名 [默认: root]
|
||
-p, --port PORT SSH端口 [默认: 22]
|
||
-d, --dest PATH 远程服务器目标路径 [默认: /opt/anxin]
|
||
-e, --env ENV 环境 (development|staging|production) [默认: production]
|
||
-t, --tag TAG 镜像标签 [默认: latest,会自动使用环境标签]
|
||
--skip-images 跳过镜像推送,仅推送配置文件
|
||
--skip-files 跳过配置文件推送,仅推送镜像
|
||
--help 显示此帮助信息
|
||
|
||
示例:
|
||
$0 -h 192.168.1.100 # 推送生产环境到指定服务器
|
||
$0 -h 192.168.1.100 -e staging # 推送测试环境
|
||
$0 -h 192.168.1.100 -u deploy -p 2222 # 指定用户和端口
|
||
$0 -h 192.168.1.100 -d /home/deploy/anxin # 指定目标路径
|
||
$0 -h 192.168.1.100 -t v1.0.0 # 指定镜像标签
|
||
$0 -h 192.168.1.100 --skip-images # 仅推送配置文件
|
||
|
||
推送内容:
|
||
配置文件:
|
||
- docker-compose.{ENVIRONMENT}.yml
|
||
- deploy.sh
|
||
- environments/ 目录
|
||
- configs/ 目录 (如果存在)
|
||
- database/ 目录 (如果存在)
|
||
|
||
Docker镜像:
|
||
- anxin-frontend:{ENV_TAG}
|
||
- anxin-backend:{ENV_TAG}
|
||
|
||
注意:
|
||
- ENV_TAG 根据环境自动确定: dev/staging/prod
|
||
- 如果指定 -t 参数,将使用自定义标签
|
||
- 推送前请确保已运行 ./build.sh -e {ENVIRONMENT} 构建镜像
|
||
|
||
EOF
|
||
}
|
||
|
||
# ===========================================
|
||
# 参数解析
|
||
# ===========================================
|
||
parse_args() {
|
||
SKIP_IMAGES=false
|
||
SKIP_FILES=false
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
-h|--host)
|
||
REMOTE_HOST="$2"
|
||
shift 2
|
||
;;
|
||
-u|--user)
|
||
REMOTE_USER="$2"
|
||
shift 2
|
||
;;
|
||
-p|--port)
|
||
SSH_PORT="$2"
|
||
shift 2
|
||
;;
|
||
-d|--dest)
|
||
REMOTE_PATH="$2"
|
||
shift 2
|
||
;;
|
||
-e|--env)
|
||
ENVIRONMENT="$2"
|
||
shift 2
|
||
;;
|
||
-t|--tag)
|
||
TAG="$2"
|
||
shift 2
|
||
;;
|
||
--skip-images)
|
||
SKIP_IMAGES=true
|
||
shift
|
||
;;
|
||
--skip-files)
|
||
SKIP_FILES=true
|
||
shift
|
||
;;
|
||
--help)
|
||
show_help
|
||
exit 0
|
||
;;
|
||
*)
|
||
log_error "未知参数: $1"
|
||
show_help
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# 设置默认值
|
||
REMOTE_HOST=${REMOTE_HOST:-$DEFAULT_REMOTE_HOST}
|
||
REMOTE_USER=${REMOTE_USER:-$DEFAULT_REMOTE_USER}
|
||
SSH_PORT=${SSH_PORT:-$DEFAULT_SSH_PORT}
|
||
REMOTE_PATH=${REMOTE_PATH:-$DEFAULT_REMOTE_PATH}
|
||
ENVIRONMENT=${ENVIRONMENT:-"production"} # 推送脚本默认为生产环境
|
||
TAG=${TAG:-$DEFAULT_TAG}
|
||
|
||
# 验证必需参数
|
||
if [[ -z "$REMOTE_HOST" ]]; then
|
||
log_error "必须指定远程服务器地址 (-h|--host)"
|
||
show_help
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ===========================================
|
||
# 系统检查
|
||
# ===========================================
|
||
check_prerequisites() {
|
||
log_step "检查系统依赖..."
|
||
|
||
# 检查SSH
|
||
if ! command -v ssh &> /dev/null; then
|
||
log_error "SSH未安装或不在PATH中"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查SCP
|
||
if ! command -v scp &> /dev/null; then
|
||
log_error "SCP未安装或不在PATH中"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查Docker (如果需要推送镜像)
|
||
if [[ "$SKIP_IMAGES" != "true" ]] && ! command -v docker &> /dev/null; then
|
||
log_error "Docker未安装或不在PATH中"
|
||
exit 1
|
||
fi
|
||
|
||
log_success "系统依赖检查通过"
|
||
}
|
||
|
||
# ===========================================
|
||
# SSH执行函数
|
||
# ===========================================
|
||
# ===========================================
|
||
# SSH连接管理
|
||
# ===========================================
|
||
SSH_CONTROL_PATH=""
|
||
SSH_MASTER_STARTED=false
|
||
|
||
# 启动SSH主连接
|
||
start_ssh_master() {
|
||
if [[ "$SSH_MASTER_STARTED" == "true" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
SSH_CONTROL_PATH="/tmp/ssh-anxin-deploy-$$"
|
||
|
||
log_info "启动SSH主连接 (连接复用)..."
|
||
|
||
# 启动SSH主连接
|
||
ssh -M -S "$SSH_CONTROL_PATH" -f -N -p "$SSH_PORT" \
|
||
-o ControlPersist=600 \
|
||
-o ConnectTimeout=30 \
|
||
-o ServerAliveInterval=60 \
|
||
-o ServerAliveCountMax=3 \
|
||
"$REMOTE_USER@$REMOTE_HOST"
|
||
|
||
if [[ $? -eq 0 ]]; then
|
||
SSH_MASTER_STARTED=true
|
||
log_success "SSH主连接启动成功"
|
||
else
|
||
log_error "SSH主连接启动失败"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 停止SSH主连接
|
||
stop_ssh_master() {
|
||
if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then
|
||
log_info "关闭SSH主连接..."
|
||
ssh -S "$SSH_CONTROL_PATH" -O exit "$REMOTE_USER@$REMOTE_HOST" 2>/dev/null || true
|
||
SSH_MASTER_STARTED=false
|
||
fi
|
||
}
|
||
|
||
# 通用SCP传输函数,支持大文件传输和连接复用
|
||
scp_transfer() {
|
||
local source="$1"
|
||
local destination="$2"
|
||
local is_recursive=${3:-false}
|
||
|
||
local scp_options="-P $SSH_PORT -o ConnectTimeout=30 -o ServerAliveInterval=60 -o ServerAliveCountMax=3"
|
||
|
||
# 如果SSH主连接已启动,使用连接复用
|
||
if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then
|
||
scp_options="$scp_options -o ControlPath=$SSH_CONTROL_PATH"
|
||
fi
|
||
|
||
if [[ "$is_recursive" == "true" ]]; then
|
||
scp_options="$scp_options -r"
|
||
fi
|
||
|
||
# 对于大文件,显示传输进度
|
||
if [[ -f "$source" ]]; then
|
||
local file_size=$(stat -f%z "$source" 2>/dev/null || stat -c%s "$source" 2>/dev/null || echo "0")
|
||
if [[ $file_size -gt 10485760 ]]; then # 大于10MB的文件显示进度
|
||
scp_options="$scp_options -v"
|
||
fi
|
||
fi
|
||
|
||
scp $scp_options "$source" "$destination"
|
||
}
|
||
|
||
# 通用SSH执行函数,支持密钥和密码认证
|
||
ssh_execute() {
|
||
local command="$1"
|
||
local show_output=${2:-true}
|
||
local timeout=${3:-300} # 默认5分钟超时
|
||
|
||
local ssh_options="-p $SSH_PORT -o ConnectTimeout=30 -o ServerAliveInterval=60 -o ServerAliveCountMax=3"
|
||
|
||
# 如果SSH主连接已启动,使用连接复用
|
||
if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then
|
||
ssh_options="$ssh_options -o ControlPath=$SSH_CONTROL_PATH"
|
||
|
||
if [[ "$show_output" == "true" ]]; then
|
||
ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command"
|
||
else
|
||
ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null
|
||
fi
|
||
return $?
|
||
fi
|
||
|
||
# 首先尝试使用SSH密钥连接(非交互式)
|
||
if ssh $ssh_options -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
|
||
# 如果SSH密钥失败,使用交互式连接(允许密码输入)
|
||
if [[ "$show_output" == "true" ]]; then
|
||
ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command"
|
||
else
|
||
ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# ===========================================
|
||
# 连接测试
|
||
# ===========================================
|
||
test_connection() {
|
||
log_step "测试远程服务器连接..."
|
||
|
||
# 首先尝试使用SSH密钥连接(非交互式)
|
||
if ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'Connection test successful'" &>/dev/null; then
|
||
log_success "远程服务器连接测试成功 (使用SSH密钥)"
|
||
return 0
|
||
fi
|
||
|
||
# 如果SSH密钥失败,尝试交互式连接(允许密码输入)
|
||
log_info "SSH密钥认证失败,尝试密码认证..."
|
||
if ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o PasswordAuthentication=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'Connection test successful'"; then
|
||
log_success "远程服务器连接测试成功 (使用密码认证)"
|
||
return 0
|
||
else
|
||
log_error "无法连接到远程服务器: $REMOTE_USER@$REMOTE_HOST:$SSH_PORT"
|
||
log_info "请确保:"
|
||
log_info "1. 服务器地址和端口正确"
|
||
log_info "2. SSH密钥已配置或密码正确"
|
||
log_info "3. 用户有相应权限"
|
||
log_info "4. 服务器SSH服务正在运行"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ===========================================
|
||
# 准备推送
|
||
# ===========================================
|
||
prepare_push() {
|
||
log_step "准备推送环境..."
|
||
|
||
# 创建临时目录
|
||
mkdir -p "$TEMP_DIR"
|
||
log_info "创建临时目录: $TEMP_DIR"
|
||
|
||
# 在远程服务器创建目标目录
|
||
log_info "在远程服务器创建目标目录..."
|
||
ssh_execute "mkdir -p $REMOTE_PATH"
|
||
|
||
log_success "推送环境准备完成"
|
||
}
|
||
|
||
# ===========================================
|
||
# 推送配置文件
|
||
# ===========================================
|
||
push_config_files() {
|
||
if [[ "$SKIP_FILES" == "true" ]]; then
|
||
log_info "跳过配置文件推送"
|
||
return 0
|
||
fi
|
||
|
||
log_step "推送配置文件..."
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# 推送对应环境的docker-compose文件
|
||
local compose_file="docker-compose.${ENVIRONMENT}.yml"
|
||
log_info "推送 $compose_file..."
|
||
if [[ -f "$compose_file" ]]; then
|
||
scp_transfer "$compose_file" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
|
||
else
|
||
log_error "Docker Compose文件不存在: $compose_file"
|
||
return 1
|
||
fi
|
||
|
||
# 推送deploy.sh
|
||
log_info "推送 deploy.sh..."
|
||
scp_transfer "deploy.sh" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
|
||
|
||
# 推送environments目录
|
||
log_info "推送 environments/ 目录..."
|
||
scp_transfer "environments" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true
|
||
|
||
# 推送configs目录(如果存在)
|
||
if [[ -d "configs" ]]; then
|
||
log_info "推送 configs/ 目录..."
|
||
scp_transfer "configs" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true
|
||
fi
|
||
|
||
# 推送database目录(如果存在)
|
||
if [[ -d "database" ]]; then
|
||
log_info "推送 database/ 目录..."
|
||
scp_transfer "database" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true
|
||
fi
|
||
|
||
# 设置deploy.sh执行权限
|
||
log_info "设置deploy.sh执行权限..."
|
||
ssh_execute "chmod +x $REMOTE_PATH/deploy.sh"
|
||
|
||
log_success "配置文件推送完成"
|
||
}
|
||
|
||
# ===========================================
|
||
# 推送Docker镜像
|
||
# ===========================================
|
||
push_docker_images() {
|
||
if [[ "$SKIP_IMAGES" == "true" ]]; then
|
||
log_info "跳过Docker镜像推送"
|
||
return 0
|
||
fi
|
||
|
||
log_step "推送Docker镜像..."
|
||
|
||
# 根据环境确定镜像标签
|
||
local env_tag
|
||
case $ENVIRONMENT in
|
||
production) env_tag="prod" ;;
|
||
staging) env_tag="staging" ;;
|
||
development) env_tag="dev" ;;
|
||
esac
|
||
|
||
# 如果指定了自定义标签,使用自定义标签,否则使用环境标签
|
||
local image_tag
|
||
if [[ "$TAG" != "latest" ]]; then
|
||
image_tag="$TAG"
|
||
else
|
||
image_tag="$env_tag"
|
||
fi
|
||
|
||
local images=("${FRONTEND_IMAGE}:${image_tag}" "${BACKEND_IMAGE}:${image_tag}")
|
||
|
||
for image in "${images[@]}"; do
|
||
log_info "处理镜像: $image"
|
||
|
||
# 检查本地镜像是否存在
|
||
if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$image$"; then
|
||
log_error "本地镜像不存在: $image"
|
||
log_info "请先运行构建脚本: ./build.sh -e $ENVIRONMENT"
|
||
if [[ "$TAG" != "latest" ]]; then
|
||
log_info "或者: ./build.sh -e $ENVIRONMENT -t $TAG"
|
||
fi
|
||
exit 1
|
||
fi
|
||
|
||
# 导出镜像到tar文件
|
||
local image_file="$TEMP_DIR/$(echo $image | tr ':/' '_').tar"
|
||
log_info "导出镜像到: $image_file"
|
||
docker save -o "$image_file" "$image"
|
||
|
||
# 推送镜像文件到远程服务器
|
||
log_info "推送镜像文件到远程服务器..."
|
||
scp_transfer "$image_file" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
|
||
|
||
# 在远程服务器加载镜像
|
||
local remote_image_file="$REMOTE_PATH/$(basename $image_file)"
|
||
log_info "在远程服务器加载镜像..."
|
||
ssh_execute "docker load -i $remote_image_file && rm $remote_image_file"
|
||
|
||
log_success "镜像推送完成: $image"
|
||
done
|
||
|
||
log_success "所有Docker镜像推送完成"
|
||
}
|
||
|
||
# ===========================================
|
||
# 验证推送结果
|
||
# ===========================================
|
||
verify_push() {
|
||
log_step "验证推送结果..."
|
||
|
||
# 验证配置文件
|
||
if [[ "$SKIP_FILES" != "true" ]]; then
|
||
log_info "验证配置文件..."
|
||
local files=("docker-compose.${ENVIRONMENT}.yml" "deploy.sh" "environments")
|
||
for file in "${files[@]}"; do
|
||
if ssh_execute "test -e $REMOTE_PATH/$file" false; then
|
||
log_success "✓ $file"
|
||
else
|
||
log_error "✗ $file 不存在"
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# 验证Docker镜像
|
||
if [[ "$SKIP_IMAGES" != "true" ]]; then
|
||
log_info "验证Docker镜像..."
|
||
|
||
# 根据环境确定镜像标签
|
||
local env_tag
|
||
case $ENVIRONMENT in
|
||
production) env_tag="prod" ;;
|
||
staging) env_tag="staging" ;;
|
||
development) env_tag="dev" ;;
|
||
esac
|
||
|
||
# 如果指定了自定义标签,使用自定义标签,否则使用环境标签
|
||
local image_tag
|
||
if [[ "$TAG" != "latest" ]]; then
|
||
image_tag="$TAG"
|
||
else
|
||
image_tag="$env_tag"
|
||
fi
|
||
|
||
local images=("${FRONTEND_IMAGE}:${image_tag}" "${BACKEND_IMAGE}:${image_tag}")
|
||
for image in "${images[@]}"; do
|
||
if ssh_execute "docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '^$image$'" false; then
|
||
log_success "✓ $image"
|
||
else
|
||
log_error "✗ $image 不存在"
|
||
fi
|
||
done
|
||
fi
|
||
|
||
log_success "推送结果验证完成"
|
||
}
|
||
|
||
# ===========================================
|
||
# 显示部署信息
|
||
# ===========================================
|
||
show_deploy_info() {
|
||
log_info "推送完成! 部署信息:"
|
||
echo "----------------------------------------"
|
||
echo "远程服务器: $REMOTE_USER@$REMOTE_HOST:$SSH_PORT"
|
||
echo "部署路径: $REMOTE_PATH"
|
||
echo "环境: $ENVIRONMENT"
|
||
echo "镜像标签: $TAG"
|
||
echo "----------------------------------------"
|
||
echo "下一步操作:"
|
||
echo "1. 登录远程服务器:"
|
||
echo " ssh -p $SSH_PORT $REMOTE_USER@$REMOTE_HOST"
|
||
echo ""
|
||
echo "2. 进入部署目录:"
|
||
echo " cd $REMOTE_PATH"
|
||
echo ""
|
||
echo "3. 启动服务:"
|
||
echo " ./deploy.sh start -e $ENVIRONMENT"
|
||
echo ""
|
||
echo "4. 查看服务状态:"
|
||
echo " ./deploy.sh status -e $ENVIRONMENT"
|
||
echo ""
|
||
echo "5. 查看服务日志:"
|
||
echo " ./deploy.sh logs -e $ENVIRONMENT"
|
||
echo "----------------------------------------"
|
||
}
|
||
|
||
# ===========================================
|
||
# 主函数
|
||
# ===========================================
|
||
main() {
|
||
# 显示脚本信息
|
||
log_info "若依框架Docker部署 - 推送脚本"
|
||
log_info "执行时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||
|
||
# 解析参数
|
||
parse_args "$@"
|
||
|
||
# 检查系统依赖
|
||
check_prerequisites
|
||
|
||
# 测试连接
|
||
test_connection
|
||
|
||
# 如果连接测试成功但不是使用SSH密钥,启动SSH主连接进行连接复用
|
||
if ! ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'test'" &>/dev/null; then
|
||
log_info "检测到密码认证,启用SSH连接复用以减少密码输入次数"
|
||
start_ssh_master
|
||
fi
|
||
|
||
# 准备推送
|
||
prepare_push
|
||
|
||
# 推送配置文件
|
||
push_config_files
|
||
|
||
# 推送Docker镜像
|
||
push_docker_images
|
||
|
||
# 验证推送结果
|
||
verify_push
|
||
|
||
# 显示部署信息
|
||
show_deploy_info
|
||
|
||
log_success "推送脚本执行完成!"
|
||
}
|
||
|
||
# ===========================================
|
||
# 错误处理和清理
|
||
# ===========================================
|
||
cleanup() {
|
||
# 停止SSH主连接
|
||
stop_ssh_master
|
||
|
||
# 清理临时文件
|
||
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
||
log_info "清理临时文件..."
|
||
rm -rf "$TEMP_DIR"
|
||
log_success "临时文件清理完成"
|
||
fi
|
||
}
|
||
|
||
trap 'log_error "脚本执行被中断"; cleanup; exit 1' INT TERM
|
||
trap 'cleanup' EXIT
|
||
|
||
# 执行主函数
|
||
main "$@" |