从零构建企业级自动化部署脚本

从零构建企业级自动化部署脚本:支持多项目并发部署与智能备份

前言

在现代软件开发中,部署是一个频繁且重要的环节。传统的手动部署方式不仅效率低下,还容易出现人为错误。本文将详细介绍一个企业级的自动化部署脚本,它支持多项目并发部署、智能备份、文件校验等功能,大大提升了部署效率和可靠性。

脚本设计目标

核心需求

  1. 多项目支持:从JSON配置文件读取多个项目的部署信息

  2. 智能备份:自动备份服务器上的现有项目,避免数据丢失

  3. 并发部署:多个项目同时部署,提升整体效率

  4. 文件校验:部署完成后验证文件完整性

  5. 用户友好:提供进度条和详细的状态反馈

  6. 安全可靠:支持SSH密钥认证,防止意外交互

技术特性

  • 支持断点续传(rsync的–partial选项)

  • 彩色输出和进度条显示

  • 详细的成功/失败报告

  • 可选的单项目或多项目部署

核心架构设计

1. 配置驱动

脚本采用JSON配置文件的方式管理部署信息,每个项目包含:

  • 项目名称

  • 服务器信息(IP、端口、用户)

  • 本地源码目录

  • 远程目标目录

2. 并发控制

使用bash的后台作业管理,通过&wait命令实现并发部署,同时限制最大并发数避免服务器过载。

3. 状态管理

通过临时文件在子进程和父进程之间传递状态信息,确保部署结果的准确记录。

代码实现详解

第一步:环境准备和依赖检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#!/bin/bash



# 颜色输出函数

green() { printf '\033[32m%b\033[0m\n' "$1"; }

red() { printf '\033[31m%b\033[0m\n' "$1"; }

yellow() { printf '\033[33m%b\033[0m\n' "$1"; }

blue() { printf '\033[34m%b\033[0m\n' "$1"; }



# 依赖检查

command -v rsync >/dev/null || { red "请先安装 rsync"; exit 1; }

command -v jq >/dev/null || { red "请先安装 jq"; exit 1; }

设计思路

  • 使用ANSI转义序列实现彩色输出,提升用户体验

  • 在脚本开始时检查必要工具,避免运行时才发现依赖缺失

第二步:进度条实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

progress_bar() {

local current=$1

local total=$2

local width=50

local percentage=$((current * 100 / total))

local filled=$((current * width / total))

local empty=$((width - filled))



printf '\r\033[36m['

printf '%*s' $filled | tr ' ' '#'

printf '%*s' $empty | tr ' ' '-'

printf '] %d%%\033[0m' $percentage

}

设计思路

  • 使用\r实现同行刷新,避免输出过多行

  • 通过计算填充和空白字符数量实现可视化进度

  • 使用#-字符确保在不同终端环境下的兼容性

第三步:配置解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 读取配置文件

DEPLOY_CONFIG="scripts/deploy.json"

PROJECTS_DATA=$(jq -r '.' "$DEPLOY_CONFIG")

PROJECT_COUNT=$(echo "$PROJECTS_DATA" | jq 'length')



# 解析项目信息

get_project_info() {

local index=$1

local field=$2

echo "$PROJECTS_DATA" | jq -r ".[$index].$field"

}

设计思路

  • 使用jq一次性读取整个JSON文件,避免多次文件IO

  • 通过函数封装项目信息获取,提高代码复用性

  • 预先计算项目总数,用于进度条显示

第四步:用户交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

echo "请选择部署方式:"

echo "a) 部署所有项目"

echo "s) 选择特定项目"

echo "q) 退出"



read -p "请输入选择 (a/s/q): " choice



case $choice in

a) SELECTED_INDICES=$(seq 0 $((PROJECT_COUNT - 1)));;

s)

echo "可用项目:"

for i in $(seq 0 $((PROJECT_COUNT - 1))); do

echo "$i) $(get_project_info $i name)"

done

read -p "请输入项目编号(多个用空格分隔): " -a indices

SELECTED_INDICES="${indices[@]}"

;;

q) exit 0;;

*) red "无效选择"; exit 1;;

esac

设计思路

  • 提供灵活的部署选择,支持全量或选择性部署

  • 使用数组存储用户选择,便于后续处理

  • 清晰的用户界面,降低使用门槛

第五步:核心部署函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

deploy_project() {

local index=$1

local PROJECT_NAME=$(get_project_info $index name)

local SERVER=$(get_project_info $index server)

local PORT=$(get_project_info $index port)

local TARGET_DIR=$(get_project_info $index target_dir)

local SRC_DIR=$(get_project_info $index src_dir)

local SSH_KEY="~/.ssh/id_rsa"

local log_file="/tmp/deploy_${PROJECT_NAME}_$$.log"

local exit_code=0

# 检查源目录

if [[ ! -d "$SRC_DIR" ]]; then

echo "FAILED:$PROJECT_NAME:源目录不存在 ($SRC_DIR)" >> "$log_file"

return 1

fi

# 检查SSH连接

if ! ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "echo 'SSH连接测试'" >/dev/null 2>&1; then

echo "FAILED:$PROJECT_NAME:SSH连接失败" >> "$log_file"

return 1

fi

# 备份现有目录

if ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "[ -d '$TARGET_DIR' ]"; then

local backup_suffix=$(date +%Y%m%d%H%M)

local dir_name=$(basename "$TARGET_DIR")

local BACKUP_NAME="${dir_name}_back${backup_suffix}"

local backup_path=$(dirname "$TARGET_DIR")/$BACKUP_NAME

ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "mv '$TARGET_DIR' '$backup_path'"

echo "$PROJECT_NAME -> $BACKUP_NAME" > "/tmp/renamed_${PROJECT_NAME}_$$.tmp"

fi

# 执行rsync同步

if rsync -avz --delete --progress \

-e "ssh -i $SSH_KEY -p $PORT -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \

--iconv=UTF-8-MAC,UTF-8 \

--partial \

--partial-dir=.rsync-partial \

"$SRC_DIR/" "$SERVER:$TARGET_DIR/" > "$log_file" 2>&1; then

# 文件数量校验

local_file_count=$(find "$SRC_DIR" -type f \( -name ".*" -o -name "*.tmp" -o -name "*.log" -o -name ".DS_Store" \) -prune -o -type f -print | wc -l)

remote_file_count=$(ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "find $TARGET_DIR -type f \( -name '.*' -o -name '*.tmp' -o -name '*.log' -o -name '.DS_Store' \) -prune -o -type f -print | wc -l" 2>/dev/null)

if [[ "$local_file_count" == "$remote_file_count" && "$local_file_count" -gt 0 ]]; then

echo "SUCCESS:$PROJECT_NAME:$local_file_count" >> "$log_file"

else

echo "FAILED:$PROJECT_NAME:文件数量不匹配 (本地:$local_file_count, 远程:$remote_file_count)" >> "$log_file"

exit_code=1

fi

else

echo "FAILED:$PROJECT_NAME:rsync同步失败" >> "$log_file"

exit_code=1

fi

return $exit_code

}

设计思路

  • 分层错误处理:每个关键步骤都有独立的错误检查

  • 智能备份:自动检测并备份现有目录,使用时间戳避免冲突

  • 断点续传:rsync的--partial选项支持中断后继续传输

  • 文件校验:通过文件数量对比验证部署完整性

  • 状态记录:使用临时文件记录详细状态,便于父进程处理

第六步:并发管理和结果处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 并发控制

MAX_CONCURRENT=3

current_jobs=0

job_pids=""

job_names=""



# 部署所有选中的项目

for index in $SELECTED_INDICES; do

# 控制并发数

while [[ $current_jobs -ge $MAX_CONCURRENT ]]; do

wait_for_job

done

# 启动新任务

deploy_project $index &

local pid=$!

job_pids="$job_pids $pid"

job_names="$job_names $index"

current_jobs=$((current_jobs + 1))

done



# 等待所有任务完成

while [[ $current_jobs -gt 0 ]]; do

wait_for_job

done

设计思路

  • 并发限制:通过MAX_CONCURRENT控制同时运行的任务数

  • 动态等待:使用wait_for_job函数处理完成的任务

  • 状态跟踪:通过字符串存储PID和项目名称,便于管理

第七步:结果汇总和展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 显示最终结果

echo

if [[ -n "$RENAMED_DETAILS" ]]; then

blue "📁 重命名备份的项目:$RENAMED_DETAILS"

fi



if [[ $SUCCESS_COUNT -gt 0 ]]; then

green "✅ 部署成功的项目:$SUCCESS_PROJECTS"

fi



if [[ $FAILED_COUNT -gt 0 ]]; then

red "❌ 部署失败的项目:$FAILED_PROJECTS"

echo

red "失败原因:"

for reason in "${FAILED_REASONS[@]}"; do

echo " - $reason"

done

fi



# 根据结果选择颜色

if [[ $FAILED_COUNT -eq 0 ]]; then

green "部署完成!共部署 $SUCCESS_COUNT 个项目,失败 $FAILED_COUNT 个项目"

else

red "部署完成!共部署 $SUCCESS_COUNT 个项目,失败 $FAILED_COUNT 个项目"

fi

设计思路

  • 分类展示:分别显示备份、成功、失败的项目

  • 详细反馈:提供具体的失败原因,便于问题排查

  • 动态颜色:根据部署结果选择不同的颜色显示

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581

#!/bin/bash



# -------------------------------------------------

# deploy.sh — 一键把本地文件/目录推到远端 Linux 服务器

# 依赖: rsync (本地+远端都要有), jq (用于解析JSON)

# -------------------------------------------------

# 从 deploy.json 读取配置并遍历推送



# 颜色输出函数

green() { printf '\033[32m%b\033[0m\n' "$1"; }

red() { printf '\033[31m%b\033[0m\n' "$1"; }

yellow() { printf '\033[33m%b\033[0m\n' "$1"; }

blue() { printf '\033[34m%b\033[0m\n' "$1"; }



# 进度条函数

progress_bar() {

local current=$1

local total=$2

local width=50

local percentage=$((current * 100 / total))

local filled=$((current * width / total))

local empty=$((width - filled))



printf '\r\033[36m['

printf '%*s' $filled | tr ' ' '#'

printf '%*s' $empty | tr ' ' '-'

printf '] %d%%\033[0m' $percentage

}



# 检查依赖

command -v rsync >/dev/null || { red "请先安装 rsync"; exit 1; }

command -v jq >/dev/null || { red "请先安装 jq"; exit 1; }



# 配置文件

DEPLOY_CONFIG="scripts/deploy.json"

[[ -f "$DEPLOY_CONFIG" ]] || { red "配置文件不存在:$DEPLOY_CONFIG"; exit 1; }



# 读取项目数据

PROJECTS_DATA=$(jq -r '.' "$DEPLOY_CONFIG")

PROJECT_COUNT=$(echo "$PROJECTS_DATA" | jq 'length')



# 获取项目信息的辅助函数

get_project_info() {

local index=$1

local field=$2

echo "$PROJECTS_DATA" | jq -r ".[$index].$field"

}



# 获取项目名称的辅助函数

get_project_name() {

local pid=$1

local index=0

for p in $job_pids; do

if [[ $p -eq $pid ]]; then

local name_index=0

for n in $job_names; do

if [[ $name_index -eq $index ]]; then

get_project_info $n name

return

fi

name_index=$((name_index + 1))

done

fi

index=$((index + 1))

done

}



# 从PID列表中移除指定PID

remove_pid() {

local target_pid=$1

local new_pids=""

local new_names=""

local index=0

for pid in $job_pids; do

if [[ $pid -ne $target_pid ]]; then

new_pids="$new_pids $pid"

# 获取对应的项目名称

local name_index=0

for name in $job_names; do

if [[ $name_index -eq $index ]]; then

new_names="$new_names $name"

break

fi

name_index=$((name_index + 1))

done

fi

index=$((index + 1))

done

job_pids="$new_pids"

job_names="$new_names"

}



# 等待作业完成的函数

wait_for_job() {

local completed_pid

for pid in $job_pids; do

if ! kill -0 $pid 2>/dev/null; then

completed_pid=$pid

break

fi

done

if [[ -n "$completed_pid" ]]; then

local project_name=$(get_project_name $completed_pid)

local log_file="/tmp/deploy_${project_name}_$$.log"

# 检查部署结果

if grep -q "SUCCESS:$project_name:" "$log_file"; then

# 获取文件数量信息

file_count=$(grep "SUCCESS:$project_name:" "/tmp/deploy_${project_name}_$$.log" | cut -d':' -f3)

if [[ $SUCCESS_COUNT -eq 0 ]]; then

SUCCESS_PROJECTS="$project_name -> 共${file_count}个文件"

else

SUCCESS_PROJECTS="$SUCCESS_PROJECTS, $project_name -> 共${file_count}个文件"

fi

SUCCESS_COUNT=$((SUCCESS_COUNT + 1))

else

if [[ $FAILED_COUNT -eq 0 ]]; then

FAILED_PROJECTS="$project_name"

else

FAILED_PROJECTS="$FAILED_PROJECTS, $project_name"

fi

FAILED_COUNT=$((FAILED_COUNT + 1))

# 获取失败原因

local reason=$(grep "FAILED:$project_name:" "$log_file" | cut -d':' -f3-)

FAILED_REASONS+=("$project_name: $reason")

fi

# 检查是否有重命名记录

if [[ -f "/tmp/renamed_${project_name}_$$.tmp" ]]; then

local renamed_info=$(cat "/tmp/renamed_${project_name}_$$.tmp")

if [[ -z "$RENAMED_DETAILS" ]]; then

RENAMED_DETAILS="$renamed_info"

else

RENAMED_DETAILS="$RENAMED_DETAILS, $renamed_info"

fi

rm -f "/tmp/renamed_${project_name}_$$.tmp"

fi

# 清理日志文件

rm -f "$log_file"

# 从PID列表中移除

remove_pid $completed_pid

current_jobs=$((current_jobs - 1))

# 更新进度条

local completed=$((TOTAL_JOBS - current_jobs))

progress_bar $completed $TOTAL_JOBS

fi

}



# 部署单个项目的函数

deploy_project() {

local index=$1

local PROJECT_NAME=$(get_project_info $index name)

local SERVER=$(get_project_info $index server)

local PORT=$(get_project_info $index port)

local TARGET_DIR=$(get_project_info $index target_dir)

local SRC_DIR=$(get_project_info $index src_dir)

local SSH_KEY="~/.ssh/id_rsa"

local log_file="/tmp/deploy_${PROJECT_NAME}_$$.log"

local exit_code=0

# 检查源目录

if [[ ! -d "$SRC_DIR" ]]; then

echo "FAILED:$PROJECT_NAME:源目录不存在 ($SRC_DIR)" >> "$log_file"

return 1

fi

# 检查SSH连接

if ! ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "echo 'SSH连接测试'" >/dev/null 2>&1; then

echo "FAILED:$PROJECT_NAME:SSH连接失败" >> "$log_file"

return 1

fi

# 备份现有目录

if ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "[ -d '$TARGET_DIR' ]"; then

local backup_suffix=$(date +%Y%m%d%H%M)

local dir_name=$(basename "$TARGET_DIR")

local BACKUP_NAME="${dir_name}_back${backup_suffix}"

local backup_path=$(dirname "$TARGET_DIR")/$BACKUP_NAME

ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "mv '$TARGET_DIR' '$backup_path'"

echo "$PROJECT_NAME -> $BACKUP_NAME" > "/tmp/renamed_${PROJECT_NAME}_$$.tmp"

fi

# 执行rsync同步

if rsync -avz --delete --progress \

-e "ssh -i $SSH_KEY -p $PORT -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \

--iconv=UTF-8-MAC,UTF-8 \

--partial \

--partial-dir=.rsync-partial \

"$SRC_DIR/" "$SERVER:$TARGET_DIR/" > "$log_file" 2>&1; then

# 计算文件数量校验

echo "计算文件数量..."

local_file_count=$(find "$SRC_DIR" -type f \( -name ".*" -o -name "*.tmp" -o -name "*.log" -o -name ".DS_Store" \) -prune -o -type f -print | wc -l)

remote_file_count=$(ssh -i "$SSH_KEY" -p "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SERVER" "find $TARGET_DIR -type f \( -name '.*' -o -name '*.tmp' -o -name '*.log' -o -name '.DS_Store' \) -prune -o -type f -print | wc -l" 2>/dev/null)

echo "本地文件数量: $local_file_count"

echo "远程文件数量: $remote_file_count"

if [[ "$local_file_count" == "$remote_file_count" && "$local_file_count" -gt 0 ]]; then

echo "✅ 文件数量匹配,部署成功"

echo "SUCCESS:$PROJECT_NAME:$local_file_count" >> "$log_file"

else

echo "❌ 文件数量不匹配,部署失败"

echo "FAILED:$PROJECT_NAME:文件数量不匹配 (本地:$local_file_count, 远程:$remote_file_count)" >> "$log_file"

exit_code=1

fi

else

echo "FAILED:$PROJECT_NAME:rsync同步失败" >> "$log_file"

exit_code=1

fi

return $exit_code

}



# 主程序开始

echo "🚀 自动化部署脚本启动"

echo "📋 发现 $PROJECT_COUNT 个配置项目"



# 用户选择部署方式

echo "请选择部署方式:"

echo "a) 部署所有项目"

echo "s) 选择特定项目"

echo "q) 退出"



read -p "请输入选择 (a/s/q): " choice



case $choice in

a) SELECTED_INDICES=$(seq 0 $((PROJECT_COUNT - 1)));;

s)

echo "可用项目:"

for i in $(seq 0 $((PROJECT_COUNT - 1))); do

echo "$i) $(get_project_info $i name)"

done

read -p "请输入项目编号(多个用空格分隔): " -a indices

SELECTED_INDICES="${indices[@]}"

;;

q) exit 0;;

*) red "无效选择"; exit 1;;

esac



# 初始化变量

SUCCESS_COUNT=0

FAILED_COUNT=0

SUCCESS_PROJECTS=""

FAILED_PROJECTS=""

RENAMED_DETAILS=""

FAILED_REASONS=()

MAX_CONCURRENT=3

current_jobs=0

job_pids=""

job_names=""

TOTAL_JOBS=$(echo $SELECTED_INDICES | wc -w)



echo

blue "开始部署 $TOTAL_JOBS 个项目..."



# 部署所有选中的项目

for index in $SELECTED_INDICES; do

# 控制并发数

while [[ $current_jobs -ge $MAX_CONCURRENT ]]; do

wait_for_job

done

# 启动新任务

deploy_project $index &

local pid=$!

job_pids="$job_pids $pid"

job_names="$job_names $index"

current_jobs=$((current_jobs + 1))

done



# 等待所有任务完成

while [[ $current_jobs -gt 0 ]]; do

wait_for_job

done



# 显示最终结果

echo

if [[ -n "$RENAMED_DETAILS" ]]; then

blue "📁 重命名备份的项目:$RENAMED_DETAILS"

fi



if [[ $SUCCESS_COUNT -gt 0 ]]; then

green "✅ 部署成功的项目:$SUCCESS_PROJECTS"

fi



if [[ $FAILED_COUNT -gt 0 ]]; then

red "❌ 部署失败的项目:$FAILED_PROJECTS"

echo

red "失败原因:"

for reason in "${FAILED_REASONS[@]}"; do

echo " - $reason"

done

fi



# 根据结果选择颜色

if [[ $FAILED_COUNT -eq 0 ]]; then

green "部署完成!共部署 $SUCCESS_COUNT 个项目,失败 $FAILED_COUNT 个项目"

else

red "部署完成!共部署 $SUCCESS_COUNT 个项目,失败 $FAILED_COUNT 个项目"

fi

配置文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

[

{

"name": "前端项目",

"server": "deploy@192.168.1.100",

"port": 2222,

"target_dir": "/var/www/frontend",

"src_dir": "dist/frontend"

},

{

"name": "管理后台",

"server": "deploy@192.168.1.100",

"port": 2222,

"target_dir": "/var/www/admin",

"src_dir": "dist/admin"

},

{

"name": "API服务",

"server": "deploy@192.168.1.101",

"port": 2222,

"target_dir": "/var/www/api",

"src_dir": "dist/api"

}

]

配置说明

  • name: 项目名称,用于显示和日志记录

  • server: 服务器地址,格式为 用户@主机

  • port: SSH端口号,建议使用非标准端口

  • target_dir: 远程服务器上的目标目录

  • src_dir: 本地源码目录,相对于脚本执行位置

使用方式

1. 环境准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 安装依赖工具

# Ubuntu/Debian

sudo apt-get install rsync jq



# CentOS/RHEL

sudo yum install rsync jq



# macOS

brew install rsync jq

2. SSH密钥配置

1
2
3
4
5
6
7
8
9
10
11

# 生成SSH密钥(如果还没有)

ssh-keygen -t rsa -b 4096



# 将公钥复制到服务器

ssh-copy-id user@server.example.com

3. 配置文件设置

scripts/deploy.json中配置您的项目信息,注意脱敏处理:

  • 使用内网IP或域名替代公网IP

  • 使用非标准端口

  • 配置适当的用户权限

4. 执行部署

1
2
3
4
5
6
7
8
9
10
11

# 给脚本执行权限

chmod +x scripts/deploy.sh



# 执行部署

./scripts/deploy.sh

实际使用示例

1. 脚本执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

$ ./scripts/deploy.sh



🚀 自动化部署脚本启动

📋 发现 3 个配置项目



请选择部署方式:

a) 部署所有项目

s) 选择特定项目

q) 退出



请输入选择 (a/s/q): a



开始部署 3 个项目...



[##################################################] 100%



📁 重命名备份的项目:前端项目 -> frontend_back202412230145, 管理后台 -> admin_back202412230145

✅ 部署成功的项目:前端项目 -> 共1250个文件, 管理后台 -> 共892个文件, API服务 -> 共567个文件

部署完成!共部署 3 个项目,失败 0 个项目

2. 选择性部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

$ ./scripts/deploy.sh



🚀 自动化部署脚本启动

📋 发现 3 个配置项目



请选择部署方式:

a) 部署所有项目

s) 选择特定项目

q) 退出



请输入选择 (a/s/q): s



可用项目:

0) 前端项目

1) 管理后台

2) API服务



请输入项目编号(多个用空格分隔): 0 2



开始部署 2 个项目...



[##################################################] 100%



✅ 部署成功的项目:前端项目 -> 共1250个文件, API服务 -> 共567个文件

部署完成!共部署 2 个项目,失败 0 个项目

3. 部署失败处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

$ ./scripts/deploy.sh



🚀 自动化部署脚本启动

📋 发现 3 个配置项目



请选择部署方式:

a) 部署所有项目

s) 选择特定项目

q) 退出



请输入选择 (a/s/q): a



开始部署 3 个项目...



[##################################################] 100%



✅ 部署成功的项目:前端项目 -> 共1250个文件

❌ 部署失败的项目:管理后台, API服务



失败原因:

- 管理后台: SSH连接失败

- API服务: 源目录不存在 (dist/api)



部署完成!共部署 1 个项目,失败 2 个项目

脚本优势

1. 高效性

  • 并发部署:多个项目同时进行,大幅提升效率

  • 断点续传:支持网络中断后继续传输

  • 增量同步:只传输变更的文件

2. 可靠性

  • 智能备份:自动备份现有项目,避免数据丢失

  • 文件校验:部署完成后验证文件完整性

  • 错误处理:详细的错误信息和失败原因

3. 易用性

  • 配置驱动:通过JSON文件管理项目配置

  • 交互选择:支持全量或选择性部署

  • 进度显示:实时显示部署进度和状态

4. 安全性

  • SSH密钥认证:避免密码明文传输

  • 批量模式:防止意外交互

  • 权限控制:支持不同用户和端口配置

总结

这个自动化部署脚本通过合理的设计和实现,解决了传统部署方式中的效率低下、容易出错等问题。它支持多项目并发部署、智能备份、文件校验等企业级功能,大大提升了部署的效率和可靠性。

脚本采用模块化设计,每个功能都有清晰的职责划分,便于维护和扩展。通过配置文件驱动的方式,使得添加新项目变得非常简单。同时,详细的错误处理和状态反馈,让用户能够快速定位和解决问题。

在实际使用中,建议根据具体环境调整并发数量、SSH超时时间等参数,以获得最佳的性能和稳定性。


本文详细介绍了从零构建企业级自动化部署脚本的完整过程,包括设计思路、实现细节和使用方法。希望对您的自动化部署实践有所帮助!


从零构建企业级自动化部署脚本
https://jhyjhy.cn/posts/后端/Shell/从零构建企业级自动化部署脚本/36122/
作者
Hongyu
发布于
2025年8月23日
许可协议