Skip to content

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 已添加"
fi

grep -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 已安装"
fi

command -v 检查命令是否存在,比 which 更标准。已装就跳过,不重复跑 yum。

五、服务状态的幂等

启动并设开机自启,已经启动就不重启:

bash
if ! systemctl is-active --quiet chronyd; then
    systemctl enable --now chronyd
    echo "[init] chronyd 已启动"
fi

is-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 chronyd

dry-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_config

cp -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 查问题、怎么给脚本写测试。