接手了一批服务器,业务上线那天要统一装软件、改配置、起服务。一开始我很老实地一台台登录上去敲命令,敲到后半夜满脑子都是重复的 ssh、yum install 和 systemctl restart。后来一个同事看我实在狼狈,问了一句:你为什么不试试 SaltStack?那天晚上我第一次听说这名字,第二天花了一个上午把它跑通,之后那批机器的所有日常变更,我基本再没登录过单台机器。这篇东西就是那个上午的浓缩版,目标是帮你用最快路径把 SaltStack 用起来——不是讲透所有原理,而是先让你能装、能连、能批量执行、能写第一个状态文件。
如果你是运维、开发或者兼职管服务器的同学,手上有几十台机器要统一管,或者觉得 Ansible 批量跑起来不够快、不够实时,这篇应该适合你。文中我会把安装、认证、执行模块、状态管理、Grains/Pillar 和实战案例串起来讲,最后再分享几个我踩过的坑。极速入门的核心就一句话:先跑通一条完整的链路,再回头补细节。
1. 为什么我最终选了SaltStack而不是Ansible
1.1 从一台台登录的痛苦说起
我先交代一下背景。这批机器数量不算夸张,几十台,但操作系统混着 CentOS 和 Ubuntu,有的是新装的,有的是从别人手里接过来的老环境。我最开始的处理方式非常原始:写一个 Shell 脚本,然后一台台 scp 上去执行。十台以内还能忍,机器一多就开始出问题——有的机器网络不通,有的缺依赖,有的执行到一半报错退出,我得挨个看日志再手动补。
后来我意识到,我需要的不只是一个“能跑命令”的工具,而是一个能告诉它“目标状态是什么”的系统。比如我希望所有 Web 节点都装着 Nginx、都有一份同样的站点配置、服务都是 running 状态且开机自启。如果其中一台配置漂移了,系统能自动帮我纠正回来。SaltStack 的 State 体系正好干这个事,而它附带的远程执行模块又能覆盖日常的临时操作,两者一套搞定。
1.2 真正的分水岭:通信机制不同
很多人纠结 SaltStack 和 Ansible 到底选哪个。有一说一,如果是三五台机器,Ansible 完全够用。Ansible 走 SSH,不需要在被管机器上装东西,学习曲线也平缓。但一旦节点规模上去,SSH 每次执行都要重新握手建连,批量执行几百台时速度会明显拖后腿。
SaltStack 走的是 ZeroMQ 消息总线。Master 和 Minion 之间建立长连接,Master 发布任务,Minion 实时接收并执行,结果再通过专门端口返回。这个“推送 + 长连接”的模型天然适合大规模场景,命令下发基本是毫秒级触达。对我这种要频繁批量执行命令、又要统一维护配置的场景,这种实时性非常重要。
我把两者的核心差异整理了一下,仅供参考:
| 对比项 | Ansible | SaltStack |
|---|---|---|
| 通信方式 | SSH 短连接 | ZeroMQ 长连接 |
| 被管端 Agent | 无 | 需要安装 Minion |
| 大规模批量执行 | 偏慢 | 快,毫秒级推送 |
| 实时性 | 靠手动或定时拉取 | 天然实时 |
| 学习门槛 | 较低 | 略高,但值得投入 |
1.3 哪些场景反而不建议上SaltStack
我也见过一些人把 SaltStack 引进来之后发现用不起来,白折腾。这里说句公道话:如果节点总数不超过十台,且变更频率很低,你直接用脚本或者 Ansible 就行,没必要为了上工具而上工具。另外,如果团队里没人愿意维护一套新的配置中心,工具再好也会慢慢荒废。
反过来,什么情况适合?机器数量上了规模、系统版本混杂、需要频繁批量执行命令、需要保证各节点配置一致、希望把“变更服务器”变成“改配置文件然后执行一次命令”——这些就是 SaltStack 的主场。我当时就是满足了其中三四条,才决定投入一个上午把链路跑通。
2. 十分钟跑通环境:Master和Minion的安装与配置
2.1 安装Master:官方仓库加一条命令
SaltStack 是 C/S 架构,控制端叫 Master,被管机器上装 Minion。我建议先装一台 Master,角色就放在一台配置稍微高一点的机器上,只要网络能连通所有 Minion 就行,不用特别好的配置。
安装本身很简单。以 CentOS 系为例,先添加官方仓库,再执行安装:
yum install -y https://repo.saltproject.io/salt/py3/el/8/x86_64/latest.repo yum clean expire-cache yum install -y salt-master如果是 Debian/Ubuntu 系,一样是配好官方 APT 源后:
apt-get install -y salt-master具体仓库地址以官网安装文档为准,不同大版本可能略有差异。装完直接启动:
systemctl enable --now salt-master检查一下端口有没有监听,Master 默认监听 4505 和 4506 两个端口。4505 是命令发布端口,Minion 从这里订阅任务;4506 是结果返回端口,Minion 把执行结果回传给 Master。
ss -lnt | grep -E '4505|4506'看到这两个端口处于 LISTEN 状态,Master 这边就算成了。
2.2 安装Minion:三个参数决定一切
Minion 的安装步骤和 Master 一样,只是包名换成 salt-minion。装完之后最关键的是改 /etc/salt/minion 配置文件,里面绝大多数参数用默认值就行,但有两个参数我建议你立刻写死。
第一个是 master,也就是告诉这台 Minion 去连哪个 Master。第二个是 id,这是这台 Minion 的唯一身份标识,默认会取主机名。问题在于主机名经常会变,尤其是在云环境里重启后可能被重新分配主机名,所以最好手动固定:
# /etc/salt/minion master: 192.168.10.20 id: app-node-01改完启动即可:
systemctl enable --now salt-minionMinion 启动后会周期性尝试连接 Master 并上报自己的 key,所以装好之后不用做任何事,等它在后台完成注册就行。这里多说一句,id 一旦确定,之后尽量不要改,因为它和认证 key 是绑定的,改了 id 等于换了一台新机器。
2.3 手动认证key:这一步千万不要省
Minion 上来报到之后,Master 并不会直接信任它,而是需要人工确认。这一步叫 key 认证。先看当前有哪些 key 在等待接受:
salt-key -L输出里会分几个列表,重点看 Unaccepted Keys,也就是还没被接受的主机。比如看到 app-node-01 出现在这里,就接受它:
salt-key -a app-node-01如果机器多,也可以接受所有待认证的 key:
salt-key -A为什么要手动认证?因为 Master 和 Minion 之间的通信是加密的,如果 Master 自动信任所有来报到的机器,那任何一台能访问到 Master 端口的机器都能混进来领任务、收配置。生产环境里控制端的安全底线就是从这一步开始的。实践上我也建议你定期执行 salt-key -L 看看 Accepted Keys 里有没有不认识的机器,发现异常直接删除对应 key。
3. 踢出第一脚:远程命令执行与目标匹配
3.1 test.ping:确认master和minion真的连上了
安装和认证都做完之后,第一件事就是验证链路。在 Master 上执行:
salt '*' test.ping通配符 * 表示匹配所有已认证的 Minion。如果一切正常,你会看到类似这样的返回:
app-node-01: True app-node-02: True看到 True 就说明 Master 到 Minion 的推送链路、Minion 到 Master 的返回链路都是通的,加密通信也正常。这一步通过,后面所有操作才有意义。
3.2 常用执行模块:cmd.run、pkg、service
test.ping 只是热身,真正日常用得最多的是这么几类模块。第一类是直接在目标机上执行任意命令:
salt '*' cmd.run 'uptime' salt '*' cmd.run 'df -h | head -5'注意这里 cmd.run 的命令参数会被传达到目标机的 Shell 去执行,所以管道符、重定向都能用。第二类是包管理,比如给所有机器装一个软件:
salt '*' pkg.install vimsalt '*' pkg.install 会自动识别目标机的包管理器,CentOS 上走 yum/dnf,Ubuntu 上走 apt,你不用关心底层差异。第三类是服务管理:
salt '*' service.status sshd salt '*' service.restart sshd这三类模块覆盖了日常至少七八成的临时操作。你可以在 Master 上一条命令遍历几十台机器,每台机器都会实时返回结果,不再需要一台台登录。
3.3 目标匹配:从全量到精准的四种写法
通配符 * 只是最基础的用法。实际运维中经常需要对一批特定的机器操作,SaltStack 提供了多种匹配方式,我挑四种最常用的。
第一种是通配符,比如只操作所有 app 开头的机器:
salt 'app-*' test.ping第二种是正则匹配,用 -E 参数。比如想匹配 app-node-01 到 app-node-03:
salt -E 'app-node-0[1-3]' test.ping第三种是列表匹配,用 -L 参数,适合操作少量明确指定的机器:
salt -L 'app-node-01,app-node-02,db-node-01' test.ping第四种是按系统信息匹配,用 -G 参数配合 Grains。这个后面还会细讲,先看个例子:
salt -G 'os_family:RedHat' test.ping上面这条会把所有 CentOS/RHEL 系的机器选出来,无论它们的 id 是什么。这在实际中非常有用,比如你只想对 RedHat 系的机器换软件源,一条命令就搞定。
3.4 为什么SaltStack这么快:推送模式与长连接
搞清楚 SaltStack 为什么快,你后面用起来心里更有底。Ansible 每次执行都要从控制机通过 SSH 连到目标机,建立连接、认证、执行、断开,这一套流程单台机器还行,几百台叠起来就是很大的开销。而 SaltStack 的 Minion 启动后会和 Master 之间维持一条长连接,Master 发布任务时走的是发布/订阅通道,命令第一时间就推到了每台 Minion 的门口。
执行完成之后,Minion 再通过 4506 端口把结果返回给 Master。整个链路是常驻的,没有反复握手的损耗。这就是所谓“极速”的底层原因。当然,长连接也意味着 Master 和 Minion 之间的网络必须稳定,防火墙也要放行对应端口,这个我在后面的坑里专门说。
4. 从敲命令到声明状态:SLS状态管理入门
4.1 声明式管理:目标是让它“变成这样”
远程执行模块适合临时操作,但它本质上还是“过程式”的——你告诉机器每一步做什么:装包、写文件、重启服务。真正让 SaltStack 发挥威力的是它的 State 系统:你不描述过程,而是描述结果。比如“Nginx 应该安装在所有 Web 节点上”,而不是“先执行 yum install nginx”。
这个思路转化到日常管理里非常舒服。以后有人问你这台机器上应该跑什么、配置应该是什么样,你不需要回忆当时敲过什么命令,打开 Salt 的状态文件看一遍就全清楚了。配置漂移了也不用怕,重新执行一次状态检查就能纠正回来。
4.2 第一个SLS文件:拆开看一个最小状态
状态定义放在 SLS 文件里,SaltStack 默认去 /srv/salt 目录找。先写一个最小的状态文件,比如 /srv/salt/helloworld.sls:
安装curl: pkg.installed: - name: curl这个文件有三个要素:第一行是状态 ID,可以随便起个能看懂的名字;第二行 pkg.installed 是“状态模块.函数”,表示用包管理模块确保某个包被安装;第三行是传给这个函数的参数。它的含义是:确保 curl 这个包在目标机器上是已安装状态。
执行一下:
salt 'app-node-01' state.apply helloworld输出会显示执行摘要,重点关注:
Summary for app-node-01 Succeeded: 1 Failed: 0如果 Succeeded 是 1、Failed 是 0,说明状态已经满足或成功完成。再跑一次你会发现依然 Succeeded: 1,但 Changed 为 0——因为 curl 已经装好了,不需要做任何变更。这个“幂等性”是 State 系统的核心价值:重复执行不会产生副作用。
4.3 状态之间的依赖:require与watch
生产环境里状态之间通常是有依赖关系的。你不能在 Nginx 还没装好的时候就启动 Nginx 服务,也不能在配置文件还没写好的时候就让服务 reload。SaltStack 提供两种常用的依赖声明:require 和 watch。
require 表示“先执行这个前置状态”。下面这个例子:先确保 Nginx 包安装,再确保 Nginx 服务在运行。服务状态依赖包状态,顺序就对了。
nginx: pkg.installed: - name: nginx service.running: - name: nginx - enable: True - require: - pkg: nginxwatch 比 require 更进一步:它不光保证顺序,还监控前置状态的变更。如果被 watch 的状态发生了变化,SaltStack 就会触发当前状态的 reload 或 restart。最典型的场景是:配置文件更新后让服务自动重启,因为光改文件不重启,服务跑的还是老配置。
nginx-app-conf: file.managed: - name: /etc/nginx/conf.d/app.conf - source: salt://nginx/conf.d/app.conf service.running: - name: nginx - enable: True - watch: - file: nginx-app-conf这里 service.running 里声明 watch 了 nginx-app-conf 这个文件状态。只要配置文件内容变了,状态系统就会自动触发 Nginx 的 reload。这个概念是使用 SaltStack 的关键之一,后面实战里还会用到。
4.4 top.sls:把状态分配给对应主机
单个状态文件写好了,怎么让它自动分配到目标机器?答案是 top.sls。在 /srv/salt/top.sls 里定义匹配规则和对应要应用的状态:
base: 'app-*': - helloworld这个文件的意思是:所有 id 以 app- 开头的 Minion,都应用 helloworld 这个状态。之后在 Master 上直接执行:
salt '*' state.applySaltStack 会先把 top.sls 下发到每台 Minion,每台 Minion 根据自己 id 的匹配情况决定要执行哪些状态。这是从“手工指定机器执行”到“机器自己知道自己该是什么样”的关键一步。
5. Grains和Pillar:到底选谁,别再纠结了
5.1 Grains:minion自己报上来的静态信息
玩到一定程度你会发现有两个概念特别容易混:Grains 和 Pillar。我先把话放这儿——区分它俩的关键就是一句话:Grains 是 Minion 自己的自我介绍,Pillar 是 Master 私下发给 Minion 的配置。
Grains 在 Minion 启动时采集,包含操作系统类型、版本、CPU 核数、内存大小、IP 地址这些静态信息。你可以把它理解成每台机器随身携带的名片。查看全部 Grains:
salt '*' grains.items只看某一个值:
salt '*' grains.get osGrains 最常见的用途有两个:一是做目标匹配,也就是前面提到的 -G 参数,比如对 os_family 为 RedHat 的机器执行某个命令;二是作为模板变量,生成配置文件时把系统信息自动填进去。
5.2 Pillar:master私下下发的配置数据
Pillar 是 Master 端定义、按目标下发给 Minion 的数据。和 Grains 不同,Pillar 里的数据是 Master 主动塞给 Minion 的,适合放不同机器之间不同的业务参数,比如 Nginx 监听端口、数据库连接串、或者某些账号口令。
默认 Pillar 文件放在 /srv/pillar 目录。举一个简单的例子,/srv/pillar/nginx.sls:
nginx: port: 8080 worker_processes: 4然后在 /srv/pillar/top.sls 里把它分配给对应主机:
base: 'app-*': - nginx在 Master 上可以查看某台 Minion 最终得到的 Pillar 数据:
salt 'app-node-01' pillar.items5.3 一张表说清区别
直接看对比最快:
| 维度 | Grains | Pillar |
|---|---|---|
| 数据来源 | Minion 本机采集 | Master 定义并下发 |
| 数据性质 | 静态,描述机器本身 | 动态,描述业务配置 |
| 存储位置 | Minion 本机 | Master 的 /srv/pillar |
| 典型用途 | 目标匹配、系统信息 | 端口、口令、业务参数 |
| 修改方式 | Minion 上改或命令设置 | Master 上改 SLS 文件 |
| 生效时机 | 重启/刷新 Grains | refresh_pillar 后生效 |
5.4 我的判断口诀与实践建议
我自己判断该用哪个,就记两句话:描述“这台机器是什么”的用 Grains,比如系统版本、CPU 架构;描述“这台机器该干什么”的用 Pillar,比如要监听哪个端口、连接哪个数据库。
另外有个安全提醒:口令、密钥这类敏感数据一定要放 Pillar,而且建议配合 SaltStack 的 GPG 加密功能,不要明文写在 SLS 文件里。Grains 是 Minion 自己上报的,本身不加密,不适合放敏感信息。这一点新手特别容易踩,我就见过有人把数据库密码写进 Grains 的,等于把口令贴在了每台机器的名片上。
6. 实战:三步写出一个部署Nginx的完整状态
6.1 目标:三台Web节点统一部署Nginx并配置站点
理论讲完,来一个完整的实战。假设现在有一个新业务要上线,三台 Web 节点(id 分别为 web-node-01、web-node-02、web-node-03)需要统一做三件事:安装 Nginx、写入一份站点配置、启动服务并设置开机自启。
这三步如果用传统方式,得在每台机器上手动执行好几条命令,还要保证三台机器的配置内容完全一致。用 SaltStack 的话,只需要写好状态文件和站点配置,然后执行一次 state.apply。
6.2 目录组织:状态文件怎么摆
我习惯在 /srv/salt 下按业务建目录。这个案例的目录结构如下:
/srv/salt/ ├── top.sls └── nginx/ ├── init.sls └── conf.d/ └── app.conf说明一下:nginx/init.sls 是状态入口文件,引用时写 nginx 即可,SaltStack 会优先找 init.sls;conf.d 目录里放的是站点配置文件模板,通过状态文件推送到目标机器的 /etc/nginx/conf.d 下。
站点配置文件 app.conf 的内容就是一个普通的 Nginx server 块模板,这里我给一个最简版本:
server { listen 80; server_name _; location / { root /var/www/html; index index.html; } }6.3 状态文件详解
核心是 /srv/salt/nginx/init.sls,我直接贴出完整内容,然后逐段解释:
nginx: pkg.installed: - name: nginx service.running: - name: nginx - enable: True - require: - pkg: nginx nginx-app-conf: file.managed: - name: /etc/nginx/conf.d/app.conf - source: salt://nginx/conf.d/app.conf - user: root - group: root - mode: 644 - require: - pkg: nginx - watch_in: - service: nginx第一段:确保 nginx 包已安装;确保 nginx 服务处于 running 状态并开机自启。同时通过 require 声明服务必须先于包安装完成。
第二段:file.managed 负责把 Master 上的 app.conf 推送到目标机器的 /etc/nginx/conf.d/app.conf,并设置属主、属组和权限。这里有一个关键细节:watch_in。它的意思是,当这份文件内容发生变化时,反向通知 service: nginx 触发热加载。这样以后你只改 Master 上的模板文件,重新执行状态,Nginx 就会自动 reload 到新配置,不需要手工去每台机器敲 nginx -s reload。
同时还要配置 /srv/salt/top.sls,把状态分配给三台 web 节点:
base: 'web-*': - nginx这里用通配符 web-* 一次性覆盖三台机器,之后再加第十台 web 节点时,只要 id 规范命名,它天然就会继承这套状态。
6.4 执行与验证:不只“能跑”,还要“幂等”
在 Master 上执行:
salt 'web-*' state.apply执行完成后的摘要会逐台列出,比如:
web-node-01: Succeeded: 2 Failed: 0Succeeded 是 2,对应状态文件里的两个状态声明(nginx 和 nginx-app-conf)。Failed 为 0 就说明这批节点已经全部达到目标状态。
接下来验证业务是否真的通了。在目标机上执行:
salt 'web-*' cmd.run 'curl -s http://127.0.0.1/ | head -5'如果能看到 HTML 内容返回,说明 Nginx 已经正常服务。然后我再执行一次 state.apply,正常情况下 Succeeded 依然是 2,但 Changed 为 0——这意味着三台机器的配置和预期完全一致,不需要任何变更。这个“重复执行结果一致”的幂等性,正是状态管理能放心交给机器自动化的原因。
6.5 之后想改配置怎么办
这套体系建好之后,日常维护变得很轻松。比如业务要调整站点配置,我只需要改 Master 上的 /srv/salt/nginx/conf.d/app.conf 文件,然后执行:
salt 'web-*' state.apply nginx状态系统会发现配置文件和目标机器上的不一致,自动推送新文件,并通过 watch_in 触发 Nginx reload。整个过程不需要登录任何一台目标机,也不需要手工处理重启时机。这就是我开头说的“把变更服务器变成编辑文件 + 执行一次命令”的实际形态。
7. 极速入门路上的五个坑,提前帮你踩了
7.1 minion的id变了,机器就“失联”了
我实际碰到过的第一个坑就是 Minion 的 id。云环境里的主机名有时候会在重启后被重新分配,而 SaltStack 默认用主机名作为 Minion id。id 一旦变化,Master 上原有的 key 就对不上了,Minion 会重新注册成一把新 key,表现为:旧 key 还挂在 Accepted Keys 里,新 key 又出现在 Unaccepted Keys 里,test.ping 却怎么都返回不了 True。
解决方式就是在安装 Minion 后立刻在 /etc/salt/minion 里把 id 写死。养成新装机器先固定 id 的习惯,后面能省一堆排查时间。
7.2 时间漂移:加密通信验签失败的隐形杀手
这个坑隐蔽性很强。现象是 key 已经接受了,但 salt '*' test.ping 就是超时,或者执行命令时偶发失败。排查网络没问题、端口没问题,最后发现是 Master 和 Minion 之间的系统时间差太多。
SaltStack 的加密通信依赖时间戳校验,时间漂移会导致消息验签失败。虚拟化环境里新创建的机器特别容易出现这种问题——系统刚起来时硬件时间还没同步好。解决方式就是统一时间同步:
systemctl enable --now chronyd或者用系统自带的 ntp 服务也一样。新环境装完机器先同步时间,能避开这个几乎不报具体错误的通信异常。
7.3 被防火墙拦住的4505/4506端口
前面提过 Master 监听 4505 和 4506 两个端口,分别负责命令发布和结果返回。如果目标机上有防火墙,或者你跑在云环境里有安全组,这两个端口必须放行。
我遇到过一种典型现象:salt-key -L 能看到 Minion 来报到,key 也能接受,但 test.ping 一直失败。看 Minion 日志会发现它在反复尝试连接 Master 4505 端口被拒绝。后来检查是防火墙策略只放行了 22 端口,没放 4505/4506。注意这里不只是入方向,Minion 连出去的方向也可能被安全组策略挡,两边都要确认。
7.4 文件推下去了,服务却没跟着reload
状态管理里最容易忽略的是文件变更和服务重载的联动。早期的坑是这样的:我写了 file.managed 推送配置文件,状态执行 Summary 也显示 Succeeded,但业务方反馈配置没生效。查了半天才发现,文件确实更新了,服务却还在用内存里的旧配置。
本质原因就是状态文件里只写了 require,没有写 watch。require 只能保证“先装包再写文件”的顺序,但文件内容后续发生变化时,只有 watch 才会主动触发服务 reload 或 restart。所以凡是“配置文件 + 服务”的组合,一定要在服务状态的 watch 里挂上文件状态,或者像第 6 章那样在文件状态里写 watch_in。这个坑绕过去之后,配置变更才能做到真正的一条命令闭环。
7.5 pillar改完不生效,先refresh一下
最后一个坑和 Pillar 的生效机制有关。我在 /srv/pillar 里改了一个业务参数,然后直接执行 state.apply,结果目标机用的还是旧值。一开始以为是 top.sls 匹配写错了,反复检查没问题,最后查到原因:Minion 端对 Pillar 数据的拉取是有时机和缓存的,Master 上的文件改了,不代表 Minion 立刻拿得到。
解决方式很简单,改完 Pillar 之后先手动触发一次刷新:
salt '*' saltutil.refresh_pillar然后再执行 state.apply,新参数就生效了。如果那台 Minion 还卡着旧值,可以把 salt-minion 服务重启一次,也能强制重新拉取。这个细节文档里提得不明显,但我敢说不少新手都在这里卡过。
结尾:我实际用下来的几个体会
如果你现在刚装好 Minion,我的建议是先别急着学那些高级特性。把 test.ping 跑通、写一个最小的 SLS 文件、用 state.apply 应用一次,然后亲手试一次“改文件然后 watch 自动 reload 服务”的流程,这一圈跑下来,你就有判断后续知识该往哪里使的感觉了。我个人还有一个习惯:新机器纳入管理后,第一时间跑一遍 salt '*' state.apply,看到 Summary 里全部 Succeeded 才继续干别的;生产环境里任何一次失败的 state.apply,当天必须查清楚原因,因为“配置漂移”这种事不会自己恢复。
另外一个小技巧:把常用的匹配规则固定成几个口头禅——想操作 app 开头就用 app-*,想操作特定两台就用 -L 列表,想按系统类型操作就用 -G。平时只记这四个模式,日常 95% 的批量操作都覆盖了。剩下的功能,等你真正遇到问题的时候再打开官方文档查,会比漫无目的地学快很多。