如果把服务器数量从三五台涨到几十台、上百台,运维方式一定会经历一次分水岭:靠人 SSH 上去敲命令,已经不够用了。你会发现软件包版本对不上、配置文件漏改、服务没有设置开机自启,任何一个小偏差都会在批量操作中被成倍放大。这个阶段,多数团队会开始引入自动化运维工具。本文要讲的 Salt,也就是 SaltStack,就是其中一个很能打的选择。
这个系列已经写到第二篇了。上一篇做了整体铺垫,这篇就直接“从零开始干”:搭建环境、建立起 master 和 minion 的通信、跑通远程命令,再写一个真正能管理 Nginx 状态的文件。这里提前说清楚,Salt 不是调味料,也不是一些生存游戏里挖的盐块,而是自动化运维领域的老牌工具 SaltStack。因为它名字太普通,刚开始搜索资料时很容易混进一堆厨房菜谱。
SaltStack 和 Ansible 经常被放在一起比较,但 Salt 有一个非常突出的特点:它不只是把命令发给目标机器,更希望你提前描述这台机器“应该长成什么样”,然后由它持续收敛到这个状态。这种思路很好,不过对刚接触的人有一个明显门槛——概念多,要理解 master、minion、key、state、pillar 这些词。所以这篇文章不贪多,只做一件事:用一个最小闭环,把整套链路跑通。读完你可以照着在测试环境里,用 Salt 管理至少一台服务器。
1. 为什么要从零开始搭建一套 Salt
先把场景说清楚。
假设你现在负责十台机器,每次上线要做这些事:装好 JDK、修改 Nginx 配置、创建系统用户、把某个服务设为开机自启、同步一份配置文件。用手工 SSH 操作,十台机器至少半小时,而且很容易在第 7 台和第 9 台之间产生差别。如果团队再加两个人,操作风格不统一,配置漂移只会更严重。
这时候自动化工具的价值就出来了。Salt 能让你用一条命令批量执行操作,也能把整台服务器要达到的“期望状态”写进一个文件。它解决了三个核心问题:
- 批量执行:一条命令同时跑在匹配到的所有机器上,不用逐台登录。
- 状态收敛:通过 SLS 文件描述“这台机器要装 Nginx、要监听 8080 端口、要开机自启”,Salt 会检查当前状态并自动补齐差异。
- 可复用与可追溯:所有配置都是文本文件,可以纳入 Git 管理,变更可以 review,出问题可以回滚。
那是不是所有场景都应该用 Salt?也不是。如果你只有一两台机器,配置简单、很少变更,手工脚本反而更轻量。Salt 适合的是“有一定规模、需要长期维护、希望配置保持一致”的环境。它默认的架构是 Master/Minion 模式,需要装 agent(minion),这和 Ansible 的免代理模式不太一样。如果你的环境里有大量不可预装 agent 的临时机器,Salt 并不是最优解。
学习路径上,建议走这样一条线:先搞清楚 master/minion 怎么通信,再做远程命令,然后写 State 状态文件,最后用 Pillar 和 Grains 做参数化和分组。不要一上来就研究事件驱动、Reactor、Runner,容易劝退。
2. 基础概念与核心原理
2.1 什么是 SaltStack
SaltStack 是一个基于 Python 编写的基础设施自动化工具。它管理节点的逻辑,很像“控制端 + 被控制端”的模式。控制端叫 master,被控制端叫 minion。master 负责下发命令和状态文件,minion 在自己的机器上执行,并把结果返回给 master。
从官方定位看,Salt 的能力范围很广:远程命令执行、配置管理、软件部署、定时任务、事件触发的自动化响应等。但万变不离其宗,所有能力都建立在 master 和 minion 能稳定通信这件事上。
2.2 Master、Minion 与 Key 认证
Master 是管理中枢,默认监听两个端口:
- 4505:publish 端口,负责向 minion 发布命令。
- 4506:ret 端口,负责接收 minion 返回的结果。
Minion 启动后会生成一对密钥,并把公钥发送给 master 请求认证。管理员在 master 上把 minion 的公钥加入“已接受”列表,两者之间才能正常通信。这是 Salt 安全模型的一部分,也意味着认证关系是双向确认过的。
新手最容易误以为 minion 启动后就能直接用,但实际上如果 minion 的 key 还处于未接受状态,master 下发命令时不会收到任何结果。所以在“跑通 Salt”这条路上,接受 key 是第一个必过的关卡。
2.3 远程执行与 State 状态管理
远程执行很好理解:master 下发一个函数调用,minion 执行后返回结果。例如:
salt '*' cmd.run 'uptime'这条命令会向所有 minion 下发uptime命令,并把每台机器返回的文本汇总到终端。
State 状态管理是 Salt 更核心的能力。你可以把它理解成“声明式配置”:不写“怎么装 Nginx”,而写“目标机器上必须有一个叫 nginx 的软件包,必须有一个运行中的 nginx 服务”。Salt 在 minion 上检查现状,如果不符合就执行动作让它符合,如果已经符合就跳过。
在较新版本的 Salt 中,执行状态管理的命令是state.apply。它等价于应用 highstate。老资料里常见的state.highstate也还能用,但新项目建议直接用state.apply。
SLS 文件是 Salt State 的编排文件,本质上是一个 YAML 描述文件。文件名和目录名决定了这个状态叫什么,例如webserver/init.sls对应状态名webserver。
2.4 Grains 与 Pillar 的区别
这两个概念很容易混淆,但定位完全不同。
Grains 是 minion 端的静态信息,比如操作系统类型、CPU 架构、内存大小、IP 地址。它由 minion 自己收集,master 可以查询,也可以用来做目标匹配。可以粗暴理解为“机器自报的户口信息”。
Pillar 是 master 向特定 minion 下发的变量数据。它由管理员在 master 上定义,只有匹配到的 minion 才能拿到对应的数据。Pillar 特别适合存放环境差异参数,比如不同环境的 Nginx 监听端口、数据库地址、应用版本号。
举个例子:同一套 SLS 文件,通过 Pillar 给测试环境的 minion 传入 8080 端口,给生产环境的 minion 传入 80 端口,状态描述不需要改一行。这就是“配置与逻辑分离”的价值。
2.5 和常见自动化工具对比
| 工具 | 架构模式 | 是否需要 agent | 适用场景 | 特点 |
|---|---|---|---|---|
| SaltStack | Master/Minion | 需要 | 大规模服务器、配置管理、事件自动化 | 速度快,状态管理强大,通信模型较重 |
| Ansible | 无中心/SSH | 不需要 | 临时任务、批量命令、轻量配置管理 | 上手简单,依赖 SSH,速度取决于连接数 |
| Puppet | Master/Agent 或单机 | 需要 | 长期配置管理 | 模型成熟,语法学习曲线较陡 |
| 手工脚本 | 无 | 不需要 | 少量机器、一次性任务 | 简单直接,难以复用和收敛差异 |
这里要说明,表里的对比是常规理解,不涉及绝对优劣。选型时还要看团队熟悉度、网络环境、是否需要持续状态修正。
3. 环境准备与安装
3.1 最小环境
学习阶段不需要很复杂的集群,两台机器足够,也可以在一台机器上同时安装 master 和 minion 用于体验。生产环境建议 master 独立部署,不混装业务组件。本文以两台服务器为例:
- master:管理节点,负责下发命令和状态文件。
- minion:被管理节点,可以理解成我们要自动化的那台业务服务器。
操作系统可以是 Ubuntu/Debian 或 CentOS/RHEL 系,安装命令略有差别。下面的命令不写死某一个版本,具体版本以你实际操作环境为准,思路是通用的。
3.2 安装 salt-master
Ubuntu/Debian 系:
sudo apt update sudo apt install -y salt-masterCentOS/RHEL 系:
sudo yum install -y salt-master安装后启动并设置开机自启:
sudo systemctl enable --now salt-master确认 master 服务状态:
sudo systemctl status salt-master看到active (running)说明启动成功。
3.3 安装 salt-minion
在 minion 机器上安装软件包:
sudo apt update sudo apt install -y salt-minion或者:
sudo yum install -y salt-minion注意:测试环境里,如果你只有一台机器,也可以在同一台机器上装 salt-minion,用 localhost 作为 master 地址,体验完整流程。
3.4 启动与防火墙放行
启动 minion 并设置开机自启:
sudo systemctl enable --now salt-minion关键点来了。master 默认监听 4505 和 4506 端口,minion 要能访问这两个端口才能完成通信。如果机器启用了防火墙,需要放行:
sudo firewall-cmd --permanent --add-port=4505/tcp sudo firewall-cmd --permanent --add-port=4506/tcp sudo firewall-cmd --reload如果使用的是云服务器,还要在安全组里放行这两个端口。这里有一个常见误区:只放行 4506 而忘记 4505。实际上 4505 是命令下发端口,不放行时 master 发不了命令,minion 自然没有响应。
4. 核心流程拆解:从认证到第一条远程命令
4.1 配置 master 和 minion
master 的默认配置文件在/etc/salt/master,minion 的默认配置文件在/etc/salt/minion。为了保持主配置干净,推荐在/etc/salt/master.d/和/etc/salt/minion.d/目录下创建自定义配置片段。
在 minion 上创建/etc/salt/minion.d/master.conf:
master: 192.168.1.10 id: "web-test-01"master写 master 机器的 IP 或主机名,id是当前 minion 的唯一标识。id 默认会取主机名,但显式指定更可控。修改后重启 minion:
sudo systemctl restart salt-minion如果配置无误,minion 会主动向 master 发起注册请求。
4.2 接受 minion 密钥
回到 master 机器上,查看当前密钥状态:
salt-key -L输出类似:
Accepted Keys: Unaccepted Keys: web-test-01 Rejected Keys:这时web-test-01还没有被接受。接受单个 minion:
salt-key -a web-test-01如果是测试环境,也可以直接接受所有未接受的 key:
salt-key -A生产环境不建议直接-A,因为可能把不该信任的 minion 也加进来。如果不小心接受了错误的 key,可以用删除命令移除:
salt-key -d web-test-01通常删除后,还需要在 minion 端删除/etc/salt/pki/minion下的密钥文件并重启 minion,才会重新注册一个全新的 key。这个细节在真实排障中经常用到。
4.3 连通性测试
接受 key 后,回到 master 执行最基本的测试命令:
salt '*' test.ping输出:
web-test-01: TrueTrue表示 master 和 minion 链路已经通了。所有后续操作都建立在这个基础上。如果返回Minion did not return,就要回到前面的步骤排查,后面第 7 章会详细讲。
4.4 远程命令执行
连通后,批量执行命令就没问题了:
salt '*' cmd.run 'uptime'输出:
web-test-01: 14:22:01 up 3 days, 12:33, 1 user, load average: 0.00, 0.01, 0.05你还可以用不同的目标匹配方式来选择要执行的机器,而不只是*:
# 通配符匹配 salt 'web-*' test.ping # 正则匹配 salt -E 'web-\d+' test.ping # 列表匹配 salt -L 'web-test-01,web-test-02' test.ping # 基于 grains 匹配 salt -G 'os:Ubuntu' test.ping这些匹配方式在后续执行 State 时同样适用。需要特别提醒的是,cmd.run能力太强,在生产环境执行前一定要确认目标表达式是否准确,不要随手一个*覆盖所有机器。
5. 完整示例:用 State 管理 Nginx 配置
远程命令已经跑通,接下来进入 Salt 的本体:State 状态管理。我们会用一段真实可用的配置,让 minion 上自动完成 Nginx 安装、配置和启动。
5.1 规划目录结构
默认情况下,Salt 会从 master 的/srv/salt目录读取状态文件,从/srv/pillar目录读取 Pillar 数据。如果你的发行版打包时自定义了路径,可以先在 master 配置里显式指定。
在 master 的/etc/salt/master.d/salt_paths.conf中设置:
file_roots: base: - /srv/salt pillar_roots: base: - /srv/pillar修改后重启 salt-master:
sudo systemctl restart salt-master然后创建目录:
sudo mkdir -p /srv/salt/webserver/files sudo mkdir -p /srv/pillar5.2 编写顶层 top.sls
Salt 使用 top file 来决定哪些 minion 应用哪些 State。先在/srv/salt/top.sls中写一个最简单的绑定:
# /srv/salt/top.sls base: 'web-test-01': - webserver这个文件的意思是:在 base 环境中,匹配到web-test-01这个 minion 时,应用名为webserver的状态。webserver对应/srv/salt/webserver/init.sls。
5.3 编写 Nginx 的 SLS 文件
在/srv/salt/webserver/init.sls中定义三段状态:
- 安装 nginx 软件包。
- 通过模板渲染并放置 Nginx 配置文件。
- 确保 nginx 服务运行并开机自启。
# /srv/salt/webserver/init.sls nginx: pkg.installed: - name: nginx nginx-config: file.managed: - name: /etc/nginx/conf.d/salt-demo.conf - source: salt://webserver/files/salt-demo.conf - template: jinja - defaults: listen_port: 8080 server_name: "salt-demo.local" - require: - pkg: nginx nginx-service: service.running: - name: nginx - enable: True - require: - pkg: nginx - watch: - file: nginx-config重点解释几个关键点:
file.managed会把 master 上的模板文件推送到 minion 指定路径。template: jinja表示该文件使用 Jinja 模板渲染。defaults可以给模板传递默认变量。watch的作用是监听配置文件变化。如果nginx-config这个状态在后续执行中发现文件内容变化,就会触发 nginx-service 重启服务。
5.4 添加配置模板
在 master 上创建/srv/salt/webserver/files/salt-demo.conf:
# /srv/salt/webserver/files/salt-demo.conf server { listen {{ listen_port }}; server_name {{ server_name }}; root /usr/share/nginx/html; index index.html; }这是一个缩略版 Nginx 配置,只为了演示模板渲染效果,生产环境需要根据实际场景补全。当 Salt 渲染这个文件时,{{ listen_port }}会被替换成defaults里传入的 8080。
5.5 用 Pillar 传参数
为了让不同环境可以复用同一套状态文件,可以把可变参数放到 Pillar 里。创建/srv/pillar/nginx.sls:
# /srv/pillar/nginx.sls nginx: listen_port: 8080 server_name: "salt-demo.local"再创建/srv/pillar/top.sls:
# /srv/pillar/top.sls base: 'web-test-01': - nginxPillar 数据被修改后,需要通知 minion 重新拉取:
salt 'web-test-01' saltutil.refresh_pillar查看 minion 是否拿到了 Pillar:
salt 'web-test-01' pillar.data接下来,把 SLS 文件里的静态defaults改成从 Pillar 读取,这样更贴近真实用法:
# /srv/salt/webserver/init.sls {% set nginx_cfg = pillar.get('nginx', {}) %} {% set listen_port = nginx_cfg.get('listen_port', 8080) %} {% set server_name = nginx_cfg.get('server_name', 'salt-demo.local') %} nginx: pkg.installed: - name: nginx nginx-config: file.managed: - name: /etc/nginx/conf.d/salt-demo.conf - source: salt://webserver/files/salt-demo.conf - template: jinja - defaults: listen_port: {{ listen_port }} server_name: {{ server_name }} - require: - pkg: nginx nginx-service: service.running: - name: nginx - enable: True - require: - pkg: nginx - watch: - file: nginx-config这里的pillar.get('nginx', {})是一种安全写法。如果某个 minion 没有配置 Nginx 的 Pillar,会使用兜底默认值 8080 和salt-demo.local,不会因为键不存在而渲染失败。
5.6 执行 state.apply
先在 master 上做一次“预演”,确认 SLS 能被正确解析:
salt 'web-test-01' state.show_sls webserver如果输出能列出 nginx、nginx-config、nginx-service 三段状态,说明语法没问题。真正执行时:
salt 'web-test-01' state.apply执行后,Salt 会读取 top.sls,找到web-test-01对应的状态组,逐项检查并应用。
6. 运行结果与效果验证
6.1 预期输出
state.apply执行成功时,输出会给出每段状态的执行结果。简化后类似:
web-test-01: ---------- ID: nginx Function: pkg.installed Result: True Comment: Package nginx is already installed Changes: ID: nginx-config Function: file.managed Result: True Comment: File /etc/nginx/conf.d/salt-demo.conf is in the correct state Changes: ID: nginx-service Function: service.running Result: True Comment: Service nginx is already running Changes: Summary for web-test-01 ------------ Succeeded: 3 Failed: 0 ------------ Total states run: 3看到Succeeded且Failed为 0,说明这一轮状态应用成功。
6.2 验证 Web 服务
到 minion 机器上检查端口监听情况:
ss -lntp | grep 8080再在 master 或本机发起 HTTP 请求:
curl http://192.168.1.20:8080如果 Nginx 返回 HTML 内容,说明配置文件已经被正确渲染并加载。
6.3 再次执行验证幂等性
Salt 的状态管理设计目标之一是幂等。再次执行:
salt 'web-test-01' state.apply第二次执行后,通常Succeeded的状态数量不变,但每段状态的Comment会明确提示“已经处于正确状态”。这里的关键是观察Changes字段:第一次执行时安装软件包可能有Changes,第二次如果所有段都没有实际变更,说明状态已经收敛,没有反复摇摆。
如果第二次执行仍然重复修改配置文件或反复重启服务,就需要检查模板变量是否每次渲染结果不同,或者是否需要调整watch和require的依赖关系。
7. 常见问题与排查思路
Salt 体系比较庞大,刚上手时问题集中在通信、key 和 SLS 渲染三个环节。下面列出几个高频问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
salt '*' test.ping返回 Minion did not return | minion 服务未启动,或 key 未接受,或网络不通 | 检查systemctl status salt-minion,查看salt-key -L,确认 4505 |