Appearance
17|幂等脚本与初始化
上一篇的运维脚本是"执行一次"的。但初始化脚本不一样——它会被反复执行(手动跑、配置管理工具推、重跑修问题)。反复执行不能出问题:第一次跑设好时区,第二次跑不能把时区设坏、不能重复装软件、不能把配置覆盖成错值。
这种"能反复执行、每次结果都一样"的特性叫幂等。本篇讲用 Shell 写幂等脚本的技法。具体初始化哪些配置项(主机名、时区、chrony、SSH 加固等)在 Linux 基础系列第 17 篇,本篇只讲脚本技法。
一、为什么初始化要幂等
先看一个不幂等的脚本会出什么问题:
bash
# 不幂等:每次都执行,不管当前状态
timedatectl set-timezone Asia/Shanghai
echo "127.0.1.1 $(hostname)" >> /etc/hosts
yum install -y nginx反复跑三次的问题:
set-timezone重复执行,虽然不报错但审计日志刷三遍echo >> /etc/hosts会往文件追加三行相同内容,/etc/hosts越来越乱yum install重复执行,浪费时间和网络
幂等版本:检查目标状态,已经对了就不动。
二、幂等的基本模式:检查再执行
每个操作前先检查"目标状态是否已达到",没达到才执行:
bash
# 时区:检查当前值,不对才设
current_tz=$(timedatectl show -p Timezone --value)
if [[ "$current_tz" != "Asia/Shanghai" ]]; then
timedatectl set-timezone Asia/Shanghai
echo "[init] 时区已设置"
else
echo "[init] 时区已正确,跳过"
fi这个"检查 → 不对才执行 → 记录"的模式是幂等的核心。
三、配置文件的幂等写入
往 /etc/hosts 加一行,要先检查这行在不在:
bash
# 不幂等:每次追加,文件越来越乱
echo "127.0.1.1 web-01" >> /etc/hosts
# 幂等:有就不加
if ! grep -q "^127\.0\.1\.1" /etc/hosts; then
echo "127.0.1.1 $(hostname)" >> /etc/hosts
echo "[init] /etc/hosts 已添加"
figrep -q 静默检查(只看返回码不输出),有匹配返回 0。! grep -q 表示"没匹配到"才执行。
已有但要改值——用 sed 替换,不重复追加:
bash
# 把已有的 127.0.1.1 行替换成新值
if grep -q "^127\.0\.1\.1" /etc/hosts; then
sed -i "s/^127\.0\.1\.1.*/127.0.1.1 $(hostname)/" /etc/hosts
else
echo "127.0.1.1 $(hostname)" >> /etc/hosts
fi四、软件安装的幂等
yum install 本身有幂等性——已装就不重装。但每次都跑 yum 会刷网络、查仓库,慢。先检查命令在不在:
bash
if ! command -v chronyc &>/dev/null; then
yum install -y chrony
echo "[init] chrony 已安装"
ficommand -v 检查命令是否存在,比 which 更标准。已装就跳过,不重复跑 yum。
五、服务状态的幂等
启动并设开机自启,已经启动就不重启:
bash
if ! systemctl is-active --quiet chronyd; then
systemctl enable --now chronyd
echo "[init] chronyd 已启动"
fiis-active --quiet 静默判断,--now 同时启动。已经在跑就跳过,避免重启一个正常的服务。
六、dry-run:先看后做
危险操作前先 dry-run——只显示要做什么,不真执行。脚本加个 --dry-run 开关:
bash
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
# 封装一个执行函数
run() {
if $DRY_RUN; then
echo "[dry-run] $*"
else
"$@"
fi
}
# 用法
run timedatectl set-timezone Asia/Shanghai
run systemctl enable --now chronyddry-run 模式下只打印命令,不执行。第一次在新机器上跑时先 --dry-run 看一遍要做什么,确认无误再真跑:
bash
./init.sh --dry-run # 看一遍
./init.sh # 确认后真执行七、配置备份:改前先存
改配置文件前先备份,出问题能回滚:
bash
backup_config() {
local file=$1
[[ -f "$file" ]] || return 0
cp -a "$file" "${file}.bak.$(date +%s)"
}
# 改前备份
backup_config /etc/ssh/sshd_config
sed -i 's/^#Port 22/Port 22/' /etc/ssh/sshd_configcp -a 保留权限和时间戳。备份文件带时间戳,多次执行不会互相覆盖。
八、一个完整的幂等初始化片段
把前面的技法组合:
bash
#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
log() { echo "[$(date '+%T')] $*"; }
run() { if $DRY_RUN; then echo "[dry-run] $*"; else "$@"; fi; }
# === 时区 ===
current_tz=$(timedatectl show -p Timezone --value)
if [[ "$current_tz" != "Asia/Shanghai" ]]; then
log "设置时区"
run timedatectl set-timezone Asia/Shanghai
else
log "时区已正确,跳过"
fi
# === /etc/hosts ===
if ! grep -q "^127\.0\.1\.1" /etc/hosts; then
log "添加 127.0.1.1 到 /etc/hosts"
run bash -c 'echo "127.0.1.1 $(hostname)" >> /etc/hosts'
fi
# === chrony ===
if ! command -v chronyc &>/dev/null; then
log "安装 chrony"
run yum install -y chrony
fi
if ! systemctl is-active --quiet chronyd 2>/dev/null; then
log "启动 chronyd"
run systemctl enable --now chronyd
fi
log "初始化完成"每个步骤都是"检查 → 不对才执行 → 记录",反复跑结果一致。带 --dry-run 能预演。
九、批量执行初始化
对一组机器跑初始化脚本。先把脚本推到每台机器,再执行:
bash
#!/usr/bin/env bash
set -euo pipefail
script="/usr/local/sbin/init-baseline.sh"
hosts=(web-01 web-02 web-03)
# 推送脚本到每台机器
for host in "${hosts[@]}"; do
scp "$script" "$host:/tmp/init.sh"
done
# 并行执行,收集结果
for host in "${hosts[@]}"; do
(
ssh "$host" "bash /tmp/init.sh" > "/tmp/init-${host}.log" 2>&1 \
&& echo "[OK] $host" \
|| echo "[FAIL] $host (见 /tmp/init-${host}.log)"
) &
done
wait& 后台执行,wait 等所有完成。并行执行比串行快,但要注意目标机器的负载别太高、别同时打爆 yum 仓库。控制并发数可以用 xargs -P 或专门的并行工具。
十、幂等的边界
不是所有操作都能幂等。比如:
- 创建数据库/初始化数据:第一次建表、灌数据,第二次跑会重复插入或报错。这类要靠应用层的版本管理(migration),不是 shell 脚本能搞定的
- 生成唯一 ID:每次跑要生成新的,天然不幂等
- 下载文件:每次都下最新的,不算幂等,但可以用版本号或哈希判断要不要更新
识别哪些该幂等、哪些不该,是该幂等的做好幂等,不该幂等的明确只跑一次。
十一、和配置管理工具的关系
手写幂等脚本能控,但配置项多了维护成本高。Ansible、SaltStack 这类配置管理工具内置了幂等——写"期望状态",工具判断"当前状态对不对、要不要改"。
Shell 幂等脚本适合机器不多、配置项不复杂的场景。规模大了换 Ansible,思路是一样的:声明期望状态,让工具保证幂等。手写脚本的"检查再执行"逻辑,在 Ansible 里就是模块的默认行为。
学会了之后
幂等的核心是"检查目标状态,不对才执行"。配置文件用 grep 检查后追加或 sed 替换、软件用 command -v 检查、服务用 is-active 检查。dry-run 预演、改前备份、批量并行执行——这些技法组合起来,能写出可反复跑、安全可靠的初始化脚本。
下一篇讲脚本的测试和工程化——怎么用 shellcheck 查问题、怎么给脚本写测试。