配置版本化与差异巡检:让服务器可接手

一台跑了两三年的服务器,如果每装一次软件都要登上去敲命令、改配置,日子久了必然出现三件事:机器之间的环境不一致、没人说得清某台机器上究竟装了什么、以及一次误操作之后完全无法重现当年的状态。自动化工具解决了第一个问题,但留下了后两个。这篇文章讲的是怎么用配置版本化与差异巡检把这笔账补上。

一、先算一笔账:配置漂移是从哪一天开始的

配置漂移(configuration drift)指的是同一组服务器,本应一致,却在长期运行中悄悄长出了差异。它很少是某一次大改动造成的,多数时候来自一连串「就这一台,顺手改一下」:

  • 某次故障临时把 worker_processes 从 4 调到 2,修好后忘了改回来;
  • 某台机器上手动加了一条 iptables 规则用于联调,半年后还在;
  • 某台机器的 /etc/security/limits.conf 被人手改过,导致并发上限和别的不一样;
  • 某台机器上留着一个测试用的 crontab,每天凌晨跑一次没人知道是什么的脚本。

漂移真正致命的地方不是「配置不一样」,而是「不一样这件事没人知道」。等到某天一台机器出问题、你想照着另一台修的时候,才发现两台根本不是同一个东西。

二、版本化的第一层:把配置放进 Git,而不是放进备份

很多人以为配好了 rsync 备份就等于配置有管理了,其实差得很远。备份解决的是「丢了能找回来」,版本化解决的是「知道改了什么、谁改的、为什么改」。这两件事完全不同。

最小的可行做法是给每台服务器建一个 Git 仓库,把 /etc 下真正关心的那部分纳管(不要整个 /etc,里面有密码、有机器专属的 SSH host key):

mkdir -p /root/cfg && cd /root/cfg && git init
cat > .gitignore <<'EOF'
*.key
*.pem
shadow
shadow-
ssh/ssh_host_*
ssl/private/*
# 按需纳管,别一把梭
for d in nginx/conf.d nginx/nginx.conf mysql/my.cnf.d \
         php/8.2/fpm/pool.d systemd/system logrotate.d; do
  mkdir -p "$(dirname $d)"; cp -a "/etc/$d" "./$d"
done
git add -A && git commit -q -m "baseline $(date +%F)"

有了这个仓库,后续任何一次改配置都变成「改文件 + commit + 重载服务」。好处是半年后翻 git log -p 能直接看到某条规则是哪天加的,以及当时一起改了什么——这个上下文在排障时价值极高。

再加上一条定时任务,每天把 /etc 下的目标目录同步进仓库,有差异就自动 commit。这样即使有人绕过流程直接上机器改,第二天也会被记录下来,变成一份「漂移日志」。

三、第二层:用配置管理的「无操作模式」做巡检

如果机器数量超过三台,迟早要引入 Ansible 这类配置管理工具。但即使不用它来部署,也应该用它来巡检。Ansible 的 check_mode(--check --diff)可以在不改动系统的前提下,告诉你「按现在的 playbook,这台机器上有哪些地方不一致」:

ansible-playbook -i inventory.ini site.yml --check --diff --limit web01

输出里那些 changed 的条目,就是漂移。在干净的机器上它应该全是 ok;一旦出现 changed,要么是有人手改了,要么是 playbook 变了而机器还没跟上。这两种情况都需要人看一眼,而这正是巡检的意义。

对个人站长来说,Ansible 可以不装,用一个更土的办法替代:把关键配置文件的 sha256 登记在一份清单里,每天定时比对。

# 生成基线(在配置稳定的时刻手工执行一次)
cd /etc && find nginx mysql php systemd -type f \( -name '*.conf' -o -name '*.cnf' -o -name '*.ini' \) \
  -exec sha256sum {} + | sort -k2 > /root/cfg/baseline.sha256

# 每日巡检,输出新增/修改/消失
cd /etc && find nginx mysql php systemd -type f \( -name '*.conf' -o -name '*.cnf' -o -name '*.ini' \) \
  -exec sha256sum {} + | sort -k2 | diff /root/cfg/baseline.sha256 - 

这套东西的杀伤力在于成本极低:一个 cron、一行 diff,任何「配置被动过」都会暴露。改配置是正常的,暴露出来的目的是让人知道并决定要不要更新基线。基线不该由定时任务自动刷新——自动刷新等于把漂移合法化,巡检就失效了。

四、第三层:机器级别的清单,回答「这台机器到底有什么用」

配置对得上,不代表机器能被接手。真正的运维债往往是「没人知道这台机器上跑着什么」。建议每台机器上放一个由脚本自动生成的清单文件,内容包括:监听端口与对应进程、已启用的 systemd 服务、全部 crontab、已安装的软件包数量与最近新增的包、磁盘挂载与剩余空间。

#!/bin/bash
out=/var/log/server-profile.txt
{
  echo "== $(hostname) $(date -Is)"
  echo "-- listen"; ss -lntp | awk 'NR>1{print $4, $6}'
  echo "-- enabled"; systemctl list-unit-files --state=enabled --no-pager | awk 'NR>1{print $1}'
  echo "-- cron"; { crontab -l 2>/dev/null; cat /etc/cron.d/* 2>/dev/null; } | grep -v '^#'
  echo "-- pkgs"; dpkg -l | grep '^ii' | wc -l
  echo "-- disk"; df -h | awk 'NR>1{print $6, $2, $5}'
} > $out

关键点:这个文件只由脚本生成,人不许手改。它是事实,不是愿望。有了它,交接、扩容、迁移时不用再凭记忆打包,也不用担心漏掉一个藏在 /etc/cron.d 里的任务。

五、把「变更」本身变成一个可追溯的对象

配置纳管之后,剩下最大的风险是变更本身:某人要改一条 Nginx 规则,直接在线上编辑。建议立一条简单规矩——所有变更走「先写进仓库、再同步到机器」的单向流程,机器上的文件永远是从仓库拉下来的结果,而不是反向合并。

# 仓库侧
cd /root/cfg && $EDITOR nginx/conf.d/site.conf
git diff && git commit -am "限流:搜索页 burst 从 20 调到 10"

# 部署侧(先测再载入)
nginx -t && rsync -a --delete nginx/ /etc/nginx/ && systemctl reload nginx

注意 nginx -t 必须在 rsync 之后、reload 之前,因为测的是磁盘上的新配置,不是内存里的旧配置。这一点经常被写反:先 reload 再 -t,等于先让错误配置生效再检查,检查通过也只是「错误配置本身语法正确」而已。

更稳妥的做法是加一道闸门:rsync 前先把当前 /etc/nginx 打包进 /root/cfg-rollback/,reload 后立刻用 curl 探一下关键页面的状态码,非 200 就自动回滚并 reload 旧版。

六、巡检的着陆方式:报表,而不是告警风暴

巡检脚本最容易失败的形态是「每天都发一堆告警」。三个月后所有人把它设成已读,价值归零。正确做法是分成两个通道:确定性的变更(配置文件哈希变了、出现新监听端口、新增 crontab)走每日报表,汇总成一条;状态性的异常(磁盘超阈值、服务不是 active、证书快过期)才走告警,并且要有明确的处理动作。

报表的格式越简单越好,我的做法是每天一封纯文本邮件,标题写明「X 项变更」,正文分三段:新增、修改、消失。三段都为空就不发,这样「收到邮件」本身就意味着有事发生,比任何优先级标记都有效。

七、三个阶段的实际推进顺序

不要一次把这套东西全建起来,那样大概率半途而废。按下面的顺序,每一步都能独立产生价值:第一步(half day)给每台机器建 Git 仓库并把关键配置提交一次,从此有历史可查;第二步(one day)加上每日哈希比对和纯文本报表,从此知道「有没有被改」;第三步(一两周内)把常用变更写成脚本而不是手敲,并把生成清单和回滚演练固定成月度动作。

判断这套东西是否真的生效,有个很朴素的指标:某台机器出问题后,你能不能在十分钟内说清它和其它机器的差异。能做到,说明配置漂移已经可控;做不到,说明台账和巡检还停留在纸面上。

所以第一步要做的是「排除」,而不是「收集」。除了 .gitignore 里那几条,还要在提交前跑一次关键词扫描,检查暂存区里有没有出现密码形态的字符串:

git diff --cached -U0 | grep -nE 'passw|secret|token|BEGIN .*PRIVATE' || echo "clean"

如果确实需要在仓库里记录密码类的配置,正确做法是提交一份带占位符的模板(比如 my.cnf.example),真实文件由部署脚本从机器本地的 /root/secrets 里读取并渲染。这样仓库里的内容可以公开、可以给别人看,而机器上的真实值永远不会离开那台机器。

还有一个容易被忽略的类别是「机器专属文件」:SSH host key、机器 ID、网卡配置里的 MAC 绑定、按机器名分岔的 server_name。这些东西看起来像配置,但它们本来就该每台不一样,纳管进同一个仓库只会制造大量无意义的 diff,把真正有价值的变更淹没掉。判断标准很简单:这个文件在所有机器上本应完全相同吗?答案是「不」的,就不要进仓库主干,改用模板加变量渲染。

八、从「有仓库」到「有人用」

建了仓库不等于配置就被管住了。真实的失败模式是:仓库建好、基线提交、然后三个月没人打开过,期间所有变更依然是手敲的。要避免这种结局,关键是把仓库塞进日常动作里,而不是当成一个额外的项目。

我的做法是让每次改配置都必须经过仓库,因为绕过去更麻烦。具体来说,服务器上不放编辑器习惯的便利配置,改配置时要先从仓库拉、改完推、再同步回机器。刚开始会觉得多两步,两周后就变成肌肉记忆。而当某次改崩了需要回滚时,git checkout 加一次 reload 只需要二十秒,这个时刻就是最好的正反馈。

巡检报表也要有人真的看。最有效的强制手段是让它成为每天早上第一件事,并且规定「有变更就当天处理完」:确认是预期的就更新基线,不是预期的就查是谁改的、要不要回退。拖着不处理,报表就会变成第二个没人看的告警通道。

最后值得强调:这套东西的目标不是「配置绝对不变」,而是「每一次变化都是有意的、有记录的、可回退的」。配置本来就应该随着业务变化,问题只出在没人知道它变过。

九、别把密钥和机器专属文件纳管进去

配置仓库最容易出的事故不是丢配置,而是把不该进仓库的东西提交上去。私人密钥、数据库密码、API token 一旦进了 Git 历史,即使后来删除文件,历史里依然存在,必须用 filter-repo 重写整个历史才能清掉——对一个只有几台机器的小站来说,这个操作的风险比收益大得多。

十、小结

仓库要能长期被人用下去,前提是它不含密钥、也不装机器专属文件。配置版本化解决的是「可追溯」,差异巡检解决的是「可发现」,机器清单解决的是「可接手」。三者都到位之后,服务器的状态才真正变成一个可以被人理解的东西,而不是某人脑子里的一段记忆。对一个人维护几台机器的站长来说,这三件事加起来可能只需要两三天,却能省下之后无数个「这台机器到底和那台有什么不一样」的下午。

Last modification:October 28th, 2026 at 08:24 pm

Leave a Comment