第4章 部署脚本编写与调试
部署脚本这东西,看似简单,写起来全是坑。我见过太多团队用一段helloworld级别的shell脚本就去上线,结果环境一变就跑飞了。本章咱们聊聊怎么写出靠谱的部署脚本,以及出问题了怎么快速定位。
4.1 脚本结构:先搭骨架再填肉
写脚本跟盖房子一样,先要有结构。我个人习惯分成五块:前置检查、配置加载、执行动作、结果验证、清理回收。每块之间用明确的退出点隔开。
#!/bin/bash
set -euo pipefail # 严格模式,我每回都加
# ---------- 前置检查 ----------
check_prereqs() {
command -v docker >/dev/null 2>&1 || { echo "需要docker"; exit 1; }
}
# ---------- 配置加载 ----------
load_config() {
source "${DEPLOY_ENV:-./config/default.env}"
}
# ---------- 执行动作 ----------
deploy() {
docker compose -f docker-compose.prod.yml up -d
}
# ---------- 结果验证 ----------
verify() {
curl -sf http://localhost:8080/health | grep -q '"status":"OK"'
}
# ---------- 清理回收 ----------
cleanup() {
echo "清理临时文件..."
}
# 主流程
check_prereqs
load_config
deploy
verify
cleanup
4.2 错误处理:别让脚本偷偷失败
很多人写脚本默认继续执行,哪怕上一步已经失败了。这在部署场景里非常危险——你以为部署成功了,实际中间断了,最后留下一堆半残状态。
我总结了三个必须做的点:
- set -e:任何非零退出码立刻终止。但注意有些命令本身会返回非零(比如grep没找到),需要用
|| true处理。 - set -u:变量未定义直接报错,避免用空值执行危险操作。比如
rm -rf $TARGET/,TARGET为空就成灾难了。 - trap:捕获 ERR 和 EXIT,在脚本异常退出时执行清理。
#!/bin/bash
set -euo pipefail
cleanup() {
echo "脚本退出,执行清理..."
rm -f /tmp/deploy_lock
}
trap cleanup EXIT
echo "开始部署..."
# 正常流程
...
set -u,脚本里引用了 $ENVIRONMENT,但配置文件未加载。结果 rm -rf /${ENVIRONMENT}/deploy 直接变成了 rm -rf /deploy,还好只是dev环境。从那以后我写脚本第一行必加这三个开关。
4.3 参数化设计:别把密码写死在脚本里
部署脚本最忌讳硬编码。环境地址、版本号、密钥,都应该通过外部传入。这样一来,同一套脚本可以在开发、测试、生产环境复用。
| 参数类型 | 推荐方式 | 反例 |
|---|---|---|
| 环境配置 | 环境变量或 .env 文件 | 在脚本里写 DEV_URL='192.168.1.100' |
| 版本号 | 命令行参数 ./deploy.sh v2.3.1 | 在脚本里写 VERSION='latest' |
| 敏感信息 | 密钥管理服务或 vault | 把密码写在脚本里 commit 到git |
我一般这样组织参数加载顺序:优先级从高到低 → 命令行参数 → 环境变量 → 默认值。
#!/bin/bash
DEPLOY_VERSION="${1:-${DEPLOY_VERSION:-latest}}"
DEPLOY_ENV="${2:-${DEPLOY_ENV:-dev}}"
echo "部署版本: $DEPLOY_VERSION"
echo "部署环境: $DEPLOY_ENV"
4.4 日志与调试:脚本出问题怎么查?
脚本执行时一片安静,出错了只有一行"命令不存在",这种体验我受够了。后来我养成了写日志的习惯。
- 带时间戳的日志:
echo "[$(date +'%Y-%m-%d %H:%M:%S')] 开始部署..." - 分级别输出:INFO/WARN/ERROR,便于grep过滤
- 保留上一次的输出:每次部署前将旧日志归档,别覆盖
- set -x 用于调试:在出问题的函数前面加一行
set -x,执行完再set +x
log() { echo "[`date`] $*"; }
info() { log "INFO: $*"; }
warn() { log "WARN: $*"; }
error() { log "ERROR: $*"; exit 1; }
4.5 安全合规检查点:脚本里不能踩的红线
合规要求不仅是最终环境要检查,脚本本身也要过审计。我整理了一张清单,每次写完脚本对着跑一遍。
| 检查项 | 要求 | 检查方法 |
|---|---|---|
| 无明文密码 | 不能出现密码、、证书明文 | grep -i 'password\|secret\|' deploy.sh |
| 文件权限最小化 | 部署脚本不能是777 | ls -l deploy.sh 应为 755 或 700 |
| 不执行未校验的文件 | 下载远程脚本前要校验checksum | shasum -c checksums.txt |
| 不绕过sudo认证 | 不能写死NOPASSWD | 检查 /etc/sudoers 及脚本里的sudo调用 |
| 日志不记录敏感信息 | 环境变量里的密码不能echo | 审计日志输出,加密或脱敏 |
我记得有一次合规审计发现我们脚本里有个 wget http://internal.server/script.sh | bash,审计师直接给了高危。后来我们改成了先下载,校验签名,再执行。这个习惯一直保留到现在。
4.6 调试实战:一个典型的故障定位过程
假设你写好的部署脚本在生产环境报错,但是没有详细日志。你会怎么做?我分享一下我的排查流程:
- 先看退出码:运行 echo $?,缩小范围
- 加 set -x 重跑:能看到每一步执行的命令和展开后的变量值
- 检查依赖命令是否存在:sometimes docker-compose vs docker compose 在新机器上不一致
- 检查环境变量:echo $DEPLOY_ENV,是不是加载了错误的配置文件
- 检查权限:当前用户有没有权限写目标目录、启动服务
- 检查网络连通性:curl -v 内部仓库,是不是防火墙阻断
你看,大部分部署脚本问题其实都不是代码逻辑问题,而是环境差异。所以我在写脚本时尽量多做强校验,把各种异常情况在脚本里提前拦住。
localhost,而生产环境容器网络模式下要用服务名。从那以后我固定推荐用环境变量注入数据库地址,而不是在脚本里硬编码。
4.7 本章核心知识结构
下面这张图总结了部署脚本编写与调试的主要环节和关系。你可以把它当作一个检查清单。
这张图从左到右、从上到下串起了整个脚本的生命周期。不是说你必须每一步都做到完美,但至少心里要有这根弦。当你遇到脚本报错时,按这个框架挨个排查,大多数问题都能在十分钟内定位。