在实际服务器维护中,很多人会先经历一段混乱期:要上线一台新机器,就手动登录执行 apt 或 yum;要改同一个配置,就写一个脚本循环 SSH 到所有服务器上跑;等到机器数量超过 20 台,脚本里积累的临时命令越来越多,谁改过哪台机器的配置、当前环境和期望环境差多少,完全变成黑盒。FiNALE盐oωo这个目录名看起来像一个个人练手项目,但它代表了一类更合理的做法:把服务器配置变成代码,交给配置管理工具去执行。这篇文章以FiNALE盐oωo作为示例项目代号,完整走一遍 SaltStack 从安装、认证、State 到 Pillar 的落地路径。
本文面向已经具备 Linux 命令行基础、但还没有系统使用过配置管理工具的开发者和运维工程师。文章会从三个节点的小实验开始:一台作为 Salt Master,两台作为 Salt Minion,最终达到的效果是:在 Master 上执行一条命令,Minion 自动完成 Nginx 安装、配置和启动;不同环境的差异统一放在 Pillar 中;出现认证、端口、包名问题时,能根据返回结果和日志快速定位。
1. 先理解 SaltStack 的定位:为什么需要配置管理工具
1.1 “盐”这个代号背后是一套完整的远程执行框架
SaltStack 取名自“salt”,常被翻译成“盐栈”,它的核心思想不是像 CI/CD 工具那样只负责部署某一类应用,而是直接管理操作系统层面的目标状态。FiNALE盐oωo这个示例项目里,“盐”放在这里并不是随便凑数,而是强调这套配置管理链路使用的是 SaltStack。
SaltStack 中有几个基础概念必须首先对齐:
- Master:中央控制节点,保存 State 和 Pillar 文件,向 Minion 下发指令。
- Minion:被管节点,运行在目标服务器上,默认通过 ZeroMQ 与 Master 通信。
- State:描述“目标状态”的 SLS 文件,比如“nginx 这个包要安装”“nginx 服务要启动”“这个文件内容要修改”。
- Pillar:面向特定 Minion 的变量数据,适合区分生产、测试环境。
- Grains:Minion 启动时上报的静态信息,比如操作系统、CPU 核数、IP 地址。
- Top File:决定哪些 Minion 应用哪些 State 或 Pillar 的匹配规则文件。
举一个通俗的例子:不使用 SaltStack 时,请求是“把这台机器上的 nginx 改成监听 8080 端口”,操作方式是自己登录机器、改配置、重启;使用 SaltStack 时,请求是“这台机器最终要处于 nginx 安装完成、配置文件内容正确、服务处于运行状态”的状态,执行命令后由工具自动对比现状和目标,只做必要操作。
1.2 为什么不建议用“循环 SSH + 自定义脚本”代替配置管理
很多人觉得机器不多时不需要引入 SaltStack。对于三五台机器,这个判断可以成立。但当机器数量增加后,基于 SSH 循环的运维脚本会出现几个不容易绕开的问题。
第一个问题是幂等性。同样的脚本跑两遍,可能出现“包已存在”“服务已运行”的报错,或者更严重的——配置文件被重复追加。脚本写成无条件执行时,第二次执行往往会破坏系统。SaltStack 的 State 是声明式的,CPU 判断包是否已安装、服务是否在运行,未变化的部分不会重复执行。
第二个问题是差异管理。自定义脚本很难表达“这台机器采用生产环境参数,那台机器采用测试环境参数”,通常只能写成变量替换加一堆条件判断,条件分支多了以后根本没法维护。Pillar 的价值就是把这些差异变量从执行逻辑中拆出来。
第三个问题是审计和可视化。SaltStack 执行结束后会返回结构化结果,哪些 Minion 成功、哪些失败、失败在哪个步骤,一清二楚。脚本循环只能靠 echo 输出判断,出了问题很难准确回溯。
1.3 看一个最小管理链路,再决定适用边界
在不考虑高可用和复杂网络的前提下,SaltStack 的最小管理链路如下:
- Master 启动 4505 和 4506 端口。4505 用于消息推送,4506 用于返回结果收集。
- Minion 启动后生成密钥对,把公钥发给 Master,等待 Master 接受。
- Master 接受 Minion 密钥后,通过
salt '目标' 模块.函数执行命令。 - State 和 Pillar 通过 top file 匹配目标 Minion,然后下发执行。
学习阶段建议先在两台或三台独立虚拟机或容器中验证这条链路,不要直接在生产机器上实验。生产环境需要额外考虑高可用、密钥管理、文件服务器、事件监控等,这些会在后面单独说明。
2. 准备实验环境:两台虚拟机就能跑通最小闭环
2.1 环境要求和端口规划
本文实验环境如下,实际环境中版本可以略作调整,但底层逻辑一致。
| 节点 | 操作系统建议 | 内存 | 角色 | 主机名建议 |
|---|---|---|---|---|
| 节点一 | CentOS Stream 9 或 Ubuntu Server 22.04 | 2GB | Salt Master | salt-master |
| 节点二 | 同 Master 系统 | 1GB | Salt Minion | web01 |
| 节点三 | 同 Master 系统 | 1GB | Salt Minion | web02 |
Master 与 Minion 之间需要放行 TCP 4505 和 4506 端口。如果实验环境在云安全组或本机防火墙后面,必须同时确认安全组规则和操作系统防火墙配置。
| 端口 | 用途 | 默认监听地址 |
|---|---|---|
| 4505 | Master 消息发布端口 | 0.0.0.0 |
| 4506 | Minion 返回请求端口 | 0.0.0.0 |
这里有一个容易忽略的点:SaltStack 使用 ZeroMQ 通信,不是普通 HTTP 请求,所以不能只放行 22 端口。很多第一次部署的人把 Master 装好,Minion 也配好了,就是test.ping不通,最后发现是 4505/4506 被防火墙拦截。
2.2 安装 Salt Master 和 Salt Minion
在 Master 节点上,先准备 Salt 软件仓库,再安装 Master。以 CentOS Stream 9 为例:
sudo rpm --import https://repo.saltproject.io/salt/py3/redhat/9/x86_64/latest/SALTSTACK-GPG-KEY.pub sudo curl -fsSL https://repo.saltproject.io/salt/py3/redhat/9/x86_64/latest.repo | sudo tee /etc/yum.repos.d/salt.repo sudo yum clean expire-cache sudo yum install -y salt-master在 Minion 节点上安装 Minion:
sudo rpm --import https://repo.saltproject.io/salt/py3/redhat/9/x86_64/latest/SALTSTACK-GPG-KEY.pub sudo curl -fsSL https://repo.saltproject.io/salt/py3/redhat/9/x86_64/latest.repo | sudo tee /etc/yum.repos.d/salt.repo sudo yum install -y salt-minionUbuntu 系统的安装路径类似,只是包管理器换成 apt。安装之前建议确认系统 Python 版本,Salt 3006 以上版本对 Python 版本有要求,CentOS 7 自带的 Python 2.7 已不适合新版本踩坑,建议优先使用 CentOS Stream 9、Rocky Linux 9 或 Ubuntu 22.04 这类现代系统。
注意:如果原始材料没有给出明确版本,这里安装的是 Salt 3006 系列。落地前要先确认自己的操作系统发行版和官方仓库是否匹配,否则容易出现依赖缺失或版本冲突。
2.3 配置 Minion 指向 Master 并完成密钥认证
编辑每台 Minion 的/etc/salt/minion,找到master配置项:
master: 192.168.56.10 id: web01id是 Minion 在 Salt 体系中的唯一标识,默认取自主机名。这里手动指定id的目的是让目标匹配更可读。master地址可以是 IP,也可以是域名,但学习和测试阶段建议直接用 IP,减少 DNS 不确定因素。
修改后启动服务:
sudo systemctl enable --now salt-minion在 Master 上查看待接受的密钥:
sudo salt-key -L输出类似:
Accepted Keys: Unaccepted Keys: web01 web02 Rejected Keys:接受密钥:
sudo salt-key -a web01 sudo salt-key -a web02也可以在确认所有待接受密钥没有异常后批量接受:
sudo salt-key -A2.4 验证 Master 与 Minion 连通性
密钥接受后,在 Master 上执行:
sudo salt '*' test.ping预期输出:
web01: True web02: True如果某一台返回Minion did not return. [No response],先不要急着查 State,重点检查防火墙、Minion 的master配置、Master 的/var/log/salt/master日志和 Minion 的/var/log/salt/minion日志。
这一步是整个实验链路中的第一个检查点。只有test.ping通过了,后面 State 和 Pillar 才有意义。
3. 用 State 描述目标状态,而不是登录机器敲命令
3.1 SLS 文件、Top File 和 state.apply 的关系
进入到FiNALE盐oωo项目的核心环节。SaltStack 管理配置的基本单位是 SLS 文件,默认放在 Master 的/srv/salt/目录下。SLS 文件使用的语法是 YAML,但需要在开头加一行特殊声明,告诉 Salt 这个文件是 SLS 而不是普通 YAML。
一个最小项目结构如下:
/srv/salt/ ├── top.sls └── nginx.slstop.sls是入口文件,它规定哪些 Minion 应用哪些 State:
base: 'web*': - nginx这个文件的意思是:主机名以web开头的 Minion,应用nginx.sls中描述的状态。文件名不带.sls后缀,这是 Salt 的约定。
在浏览器或文档中经常会看到state.highstate和state.apply两种写法。在旧版本中,state.highstate表示应用 top.sls 中匹配的所有状态;state.apply更灵活,可以只指定某个 SLS,例如salt 'web01' state.apply nginx。现代版本中直接执行state.apply也会默认调用 top.sls 匹配到的状态。建议新项目统一使用state.apply,语义更清晰。
3.2 编写第一个 State:安装 Nginx 并启动服务
创建/srv/salt/nginx.sls:
install_nginx: pkg.installed: - name: nginx start_nginx: service.running: - name: nginx - enable: True - require: - pkg: install_nginx这段 SLS 表达了两件事:
- 名为
install_nginx的状态要求 nginx 包已安装。 - 名为
start_nginx的状态要求 nginx 服务处于运行状态,并且开机自启。 require声明了顺序依赖:start_nginx必须在install_nginx完成后执行。
注意这里的- pkg: install_nginx不是包名,而是对状态 IDinstall_nginx的引用。写成- pkg: nginx是错误的,因为 Salt 找不到名为 nginx 的状态 ID。这是新手最容易犯的错误之一。
3.3 执行 highstate 并理解返回结果
在 Master 上对 web01 单独执行:
sudo salt 'web01' state.apply正常执行过程中,返回内容会按每个状态 ID 展示结果。关键字段包括:
web01: ---------- ID: install_nginx Function: pkg.installed Name: nginx Comment: All specified packages are already installed. Started: 10:28:31.123456 Duration: 523.123 ms Changes: ---------- ID: start_nginx Function: service.running Name: nginx Comment: Service is already running Started: 10:28:31.654321 Duration: 41.001 ms Changes:Changes为空表示没有发生变更,系统已经处于目标状态。如果Changes中出现new和old字段,说明该步骤实际对系统做了修改。
执行salt '*' state.apply可以批量为所有 Minion 应用状态。但在学习阶段,建议先针对一台机器验证,确认无误后再批量执行,避免一次性把错误配置扩散到全部机器。
3.4 State 的幂等性和执行顺序为什么重要
State 与普通 shell 脚本最大的区别是幂等性。脚本执行时,如果 nginx 已经安装,再次执行yum install -y nginx不会报错但也没有意义;如果服务已经运行,systemctl restart nginx可能会造成短暂中断。State 则先收集当前系统状态,再决定是否执行动作。
pkg.installed只保证“包存在”,如果包不在,才安装;service.running只保证“服务运行”,如果服务没在运行,才启动。这种“期望状态与当前状态对比”的模型,是配置管理工具的核心价值。
执行顺序同样重要。Salt 默认会解析各状态 ID 之间的依赖关系,没有依赖关系的状态会并行执行。使用require或watch可以显式控制顺序。require表示前置依赖,watch除了前置依赖外,还会在依赖对象发生变更时触发当前状态重新执行。
4. 使用 Pillar 区分环境差异,避免在 State 里写死参数
4.1 Pillar 与 State、Grains 的分工
假设现在需要让 web01 的 Nginx 监听 8080 端口,而 web02 监听 80 端口。如果把端口写死在nginx.sls中,那这个 State 就无法同时适合两台机器。正确做法是把差异数据放入 Pillar。
三者的分工可以这样理解:
- State 描述“做什么”,是固定的执行模板。
- Pillar 提供“用什么参数做”,按 Minion 的匹配结果提供不同数据。
- Grains 描述“这台机器本身是什么”,比如操作系统类型、硬件信息。
Pillar 的数据默认也是从 Master 下发,但只会下发给被匹配到的 Minion,天然适合存放敏感信息或环境差异。
4.2 配置 Pillar 文件
创建/srv/pillar/top.sls:
base: 'web01': - nginx 'web02': - nginx创建/srv/pillar/nginx.sls:
nginx: listen_port: 8080这个 Pillar 文件只在web01和web02上生效。如果后续增加web03,并且它不匹配任何 Pillar top file,那么从 Pillar 读取listen_port时会返回None,State 里如果没有默认值处理,就会出现配置错误。
4.3 在 State 中引用 Pillar
回到/srv/salt/nginx.sls,为避免端口写死,可以先用一个简单的 Nginx 配置模板来演示。创建/srv/salt/files/nginx.conf:
server { listen {{ pillar.get('nginx:listen_port', 80) }}; root /usr/share/nginx/html; index index.html index.htm; }然后修改/srv/salt/nginx.sls:
install_nginx: pkg.installed: - name: nginx render_nginx_conf: file.managed: - name: /etc/nginx/conf.d/demo.conf - source: salt://files/nginx.conf - template: jinja - require: - pkg: install_nginx start_nginx: service.running: - name: nginx - enable: True - watch: - file: render_nginx_conf这里的关键点有三个:
file.managed会把salt://files/nginx.conf文件下发到 Minion 的指定路径。template: jinja表示模板文件使用 Jinja2 渲染,渲染时需要读取 Pillar。watch监听配置文件变更,一旦模板渲染结果与现有文件不同,就会重启 Nginx 服务。
执行一次:
sudo salt 'web01' state.apply在web01上检查生成的文件:
cat /etc/nginx/conf.d/demo.conf预期输出:
server { listen 8080; root /usr/share/nginx/html; index index.html index.htm; }4.4 验证 Pillar 数据是否到达目标 Minion
如果配置文件里端口没有变化,首先确认 Pillar 是否已经下发。在 Master 上执行:
sudo salt 'web01' pillar.items输出中应该包含:
web01: ---------- nginx: ---------- listen_port: 8080还可以单独取值:
sudo salt 'web01' pillar.get nginx:listen_port如果pillar.items为空或缺少对应 key,说明 Pillar top file 的匹配表达式写错了,或者执行state.apply前没有刷新 Pillar。修改 Pillar 文件后执行:
sudo salt '*' saltutil.refresh_pillar再重新运行 state。
5. 用 Salt CLI、返回数据和日志完成验证与日常排错
5.1 常用命令速查表
日常使用中,下面这些命令出现频率最高。
| 命令 | 作用 | 适用场景 |
|---|---|---|
salt '*' test.ping | 测试所有 Minion 连通性 | 确认基础通信 |
salt '*' pillar.items | 查看所有 Minion 的 Pillar 数据 | 确认差异数据是否下发 |
salt '*' grains.items | 查看所有 Minion 的 Grains 信息 | 排查系统差异 |
salt 'web*' state.apply | 应用 top.sls 匹配的状态 | 发布配置 |
salt 'web01' state.apply nginx | 只应用某个指定 SLS | 单独调试某个配置 |
salt 'web01' state.show_top | 查看该 Minion 匹配到哪些状态 | 检查匹配表达式 |
salt 'web01' state.show_sls nginx | 查看渲染后的 SLS | 检查 Jinja 渲染结果 |
salt-run jobs.list_jobs | 查看任务历史和执行时间 | 审计已执行任务 |
salt 'web01' cmd.run 'systemctl status nginx' | 在 Minion 上执行命令 | 临时排查 |
state.apply是最常用的执行命令,但排错时建议先运行state.show_top和state.show_sls。前者确认目标匹配,后者确认模板渲染后的内容。直接盯着执行结果看,遇到语法错误时不容易判断问题出在匹配规则还是模板。
5.2 返回数据的结构怎么看
Salt 返回结果通常按 Minion ID 分组,一个 Minion 对应一个 key。深层字段如下:
| 字段 | 含义 | 排错作用 |
|---|---|---|
| comment | 该状态步骤的解释说明 | 直接判断为何未执行 |
| result | true/false/null | 该步骤是否成功 |
| changes | 实际系统变更内容 | 确认修改了什么 |
| duration | 耗时 | 性能排查 |
| started | 执行时间 | 审计定位 |
看到result: false时,优先阅读comment。大多数情况下,comment 已经给出了明确原因,比如“Package is not available”或“The following block is out of sync”。
5.3 Master 和 Minion 日志中值得关注的内容
Master 日志路径为/var/log/salt/master,Minion 日志路径为/var/log/salt/minion。日志默认等级是 warning,出现问题时可以用下面的方式临时提高日志等级:
sudo salt-master -l debug或者修改/etc/salt/master中的log_level配置。debug 日志会产生大量输出,定位到问题后要恢复默认等级。
在认证类问题中,Master 日志中会看到类似Authentication attempt from的记录;Minion 日志中会看到SaltReqTimeoutError或No route to host。这些关键字可以帮助快速定位是网络层问题还是认证层问题。
5.4 常见错误排查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
test.ping无响应 | 4505/4506 端口不通 | ss -lntp查看监听,firewall-cmd --list-ports查看防火墙 | 放行端口或关闭实验环境防火墙 |
test.ping返回认证错误 | Minion 密钥未被接受 | salt-key -L | 接受对应密钥 |
| Minion 重启后 ID 变化 | 主机名被修改 | hostname和salt-key -L对比 | 固定/etc/hostname,不要随意改主机名 |
| 找不到 SLS 文件 | top.sls 文件路径或文件名错误 | salt 'web01' state.show_top | 确认 SLS 在/srv/salt下且文件名与 top 中一致 |
| Pillar 值取不到 | Pillar top file 匹配错误或未刷新 | salt 'web01' pillar.items | 修改后执行saltutil.refresh_pillar |
| 包名不适用当前系统 | 不同发行版包名不同 | salt 'web01' grains.get os_family | 用 Pillar 或 Grains 区分系统再安装不同包 |
| 服务重启失败 | 配置文件模板渲染出错 | cat /etc/nginx/conf.d/demo.conf | 先在 Minion 上手动nginx -t |
6. 实际项目中容易踩到的坑和应对方案
6.1 坑一:Minion ID 不稳定导致密钥反复变化
有些环境的主机名会随云主机名称变化,默认情况下 Minion ID 会取主机名。一旦主机名变化,Minion 会重新生成密钥,Master 上会出现新密钥,旧密钥变成 Rejected,下一次执行直接失败。
推荐在安装完 Minion 后,立即在/etc/salt/minion中固定id:
id: web01-prod-001这个 ID 不要包含临时环境信息,也不要随便起名。ID 一旦确定后,在生产环境中改起来很麻烦,因为密钥和 top file 匹配都和它绑定。
6.2 坑二:在 State 中写死业务密码
很多教程为了演示简单,会把数据库密码直接写在 SLS 文件的cmd.run中。这种做法一旦 State 入库,密码就相当于公开了,所有能访问 Git 仓库的人都能看到。
正确做法是使用 Pillar 存储密码,同时开启 Pillar 的加密存储方案,比如外部 Pillar、sdb 或 Vault。最小示范如下:
db_password: {{ salt['pillar.get']('db:password', '') }}这样密码不会出现在 SLS 中,而是来自 Pillar 数据。生产环境还要考虑密钥轮换和访问审计。
6.3 坑三:直接对全量 Minion 执行 state.apply
salt '*' state.apply在只有两台测试机时很舒服,但生产环境一旦有几十台机器,其中部分机器还处于兼容性测试阶段,直接批量应用会造成大面积故障。
推荐的执行策略是分批次发布:
sudo salt -L 'web01,web02' state.apply sudo salt -E 'web(0[3-4])' state.apply先用-L精确指定要发布的 Minion,再用-E进行正则匹配。等全部验证通过后,再考虑对全量执行。
6.4 生产环境还应补齐的检查清单
从学习环境切换到大一点的生产环境之前,建议准备一份启用检查清单,逐项确认,而不是在出现问题后再返工。
| 检查项 | 建议方案 |
|---|---|
| Minion ID 是否固定 | 安装后立即写入/etc/salt/minion |
| State 是否入库并走版本管理 | 使用 gitfs 或专用仓库保存/srv/salt和/srv/pillar |
| 敏感数据是否加密 | 使用外部 Pillar、sdb 或 Vault |
| 是否按批次发布 | 分批次state.apply,避免全量 |
| 是否配置回滚路径 | 在 gitfs 中通过版本标签快速切换回上一个 State 版本 |
| 是否监控任务执行 | 使用事件总线或 Salt Runner 记录任务结果 |
| Master 是否单点 | 生产规模扩大后考虑 Multi-Master 或 Salt 高可用方案 |
| 日志和审计 | 将 Master 和 Minion 日志统一收集,方便故障定位 |
以FiNALE盐oωo这类命名为例,如果你只是在个人实验环境中维护一个自动化配置项目,不涉及模块分拆,那么单 Master、固定 ID、State 和 Pillar 分目录管理已经够用。一旦项目要多人协作或纳入正式运维流程,命名需要改成更有语义的代号,同时把/srv/salt和/srv/pillar纳入 Git 管理。配置管理工具的价值不在于第一次安装能把服务跑起来,而在于半年后一台新机器加入集群时,只需要一行命令就能让它和现有环境保持一致。