SaltStack配置部署落地与踩坑总结

描述

一、背景与定位

我们最早接触 SaltStack 是 2017 年,当时手上有一批新上的 IDC 机器,2000 多台同质的 CentOS 7, 要做初始化、SSH 加固、账号清理、Nginx 统一上线、MySQL 主从配置管理。 在那之前,团队一直用 expect 写半自动脚本,但脚本没人维护、参数到处飞、出了问题回滚基本靠人肉。 我们试过 Puppet(太重、Ruby 改起来累)、Chef(也重,knife 操作反人类)、 Ansible(2017 年那会儿还叫 1.x,连个像样的 inventory 缓存都没有), 最终选 SaltStack 的原因很朴素:默认有 agent,性能能打,SLS 写起来像写代码。

到了 2024 年,Salt 已经发布到 3006,传输层依然默认 ZeroMQ pub/sub, 架构、命令、配置几乎没有本质变化,新版本主要在 Python 3 兼容、加密传输、returner 性能上做文章。 所以这一篇里讲的东西,对 Salt 2018、2019、3000、3004、3006 都基本适用, 个别参数和路径以官方 docs.saltproject.io 为准。

1.1 适用场景

我们把 SaltStack 用在下面这几类场景上,每一类都跑过几千台规模。

大规模同质机配置中心化。 IDC 自购服务器,云上 ECS,操作系统 80% 是 CentOS 7,少量 Rocky 8、Ubuntu。 机器之间差异主要在 IP、hostname、机房 / 可用区,业务镜像基本一致。 这种情况,SLS 里写一次,所有机器收敛。

配置漂移治理。 团队里有人手改 /etc/ssh/sshd_config,有人改了没记下来,结果一收 salt '*' state.sls ssh.hardening, diff 一看一堆变更。state.sls 自带幂等,没改就跳过,改了才下发,正好治这个毛病。

跨 IDC 批量执行运维动作。 重启、清理日志、改 crontab、抓日志、临时拉黑 IP。 这种短任务用 salt-run 或者 salt -G 比写 SSH 脚本快太多。

初始化基线落地。 新机器从装机开始就接 minion,跑一次 state.highstate 把 sysctl、limits、user、repo、ntp 全部装好。 这块我们和 PXE + Cobbler 联动,10 分钟一台机器从裸机到上线。

1.2 不适用场景

不是所有场景都适合用 SaltStack,我们踩过的坑也写下来。

纯云上短期项目。上 20 台机器用 3 个月就拆,Ansible 或者 Terraform 更轻。

容器化为主的集群。K8s 自己一套声明式,Salt 去管容器化节点属于重复造轮子。

变更极少、维护人员少的团队。SaltStack 的回报要靠"统一收敛"来体现, 一年改不了几次的东西,不如写个文档让人手改。

1.3 选型对比的真实体感

我们和 Ansible / Puppet / Chef 都打过几年交道,下面这张表不是理论比较,是 "2018–2024 年我们用它们踩过什么坑"的总结。

工具 传输方式 性能(千台并发) 学习曲线 排错难度 团队接受度
SaltStack ZeroMQ pub/sub 长连接 强,master 多 worker 撑得住 中,SLS 关键字要记 中,returner 写不全会丢日志 高,命令风格像 ssh
Ansible SSH 短连接 弱,串行默认 5 fork,开了也吃力 低,YAML 直接写 难,错误信息散在 stdout 高,但 500 台开始卡
Puppet 拉模式,agent 周期同步 中等 高,DSL 难上手 难,catalog 排错痛苦 中,老牌但新人不愿意学
Chef 拉模式 + Ruby DSL 中等 高,Ruby 门槛 难,knife 操作复杂 低,新人基本不来

注意:上面这些是"我们的体感",不是 benchmark。新版本 Ansible 加了 async、 Puppet 加了 Bolt、Salt 加了 RAET 和 TCP transport,性能差距没有表格里那么夸张。 但在我们这种"几千台同质机、配置反复改"的场景里,Salt 的长连接模型确实省事。

1.4 我们踩过的最痛的一个坑

2018 年第一次上生产的时候,我们以为"装好 master、装好 minion、salt-key -A 一下就完事了"。 结果当 minion 数量上到 2000 的时候,master 莫名其妙 OOM 被 kill。 后来查到三个原因叠在一起:

默认 worker_threads 只开 5 个,几千并发直接打满。

returner 没配,作业结果全堆在 master 内存。

event bus 接收的认证事件没归档,越积越大。

那次故障之后我们做了三件事:给 master 加 worker、调 returner 落库、 按业务拆分多 master。这一篇后面"高可用"那一节会详细讲怎么调。

二、架构与组件

SaltStack 的架构在文档里写得很抽象,第一次看很容易蒙。 我们按"角色 + 数据流 + 端口"三件事讲一次,后面所有命令都能对得上。

2.1 角色

SaltStack 一共五种角色,生产里最常用前三种。

salt-master。 中心节点,对外接收 minion 的认证请求,对内负责编译 SLS、维护 Pillar、调度作业。 默认监听两个端口:publish_port = 4505(pub/sub)、ret_port = 4506(return)。

salt-minion。 装在被管机器上的 agent。 启动时按 minion 配置里的 master 地址去连,连上后做密钥交换,然后保持长连接。 收到作业就执行,执行完把 return 发回 master。

salt-syndic。 中转节点,自己既是 master 又是 minion。 适合"总控 + 分区"的多层级管理,比如集团下面有多个 BU,每个 BU 自己的 master, BU 上面再架 syndic 连集团 master。 我们没用过 syndic,10K 规模用不到,syndic 的问题在于它要做协议转换,调试链路长。

salt-api。 独立服务,提供 RESTful 接口,底层走 CherryPy。 装好后 master 上的 wheel / runner / local 模块都能通过 HTTP 调用。 这是后面接 CI / 平台的关键组件。

salt-ssh。 纯 SSH 模式,没有 minion 也能跑 Salt 命令。 适合网络隔离、不能装 agent 的场景,比如一些银行的 DMZ 区。 性能比 ZeroMQ 模式差很多,不建议大规模用。

2.2 传输层

Salt 的传输层在历史上换过几次,3000 之前默认 ZeroMQ,3000+ 还能选 RAET(基本不用了) 和 TCP(实验性)。

ZeroMQ 模式下数据流是这样的:

 

minion  --pub-->  master:4505  (认证、心跳、作业下发)
minion  <--ret--  master:4506  (return 结果,minion 主动连回)

 

注意方向是反直觉的:

4505(publish_port)由 master 监听,minion 主动连进来。 minion 一连上就订阅一个 topic,master 推送作业时是广播到所有订阅者。 所以这个端口是"minion 主动连 master"。

4506(ret_port)也是 master 监听,minion 主动连进来。 但方向是 minion 把 return 发回 master。 也就是说 两个端口都是 minion 主动连 master,master 不主动连 minion。

这个细节对排错和防火墙规则都很重要,后面的故障案例会用到。

2.3 认证

minion 第一次启动时,会生成自己的密钥对,公钥发给 master。 master 把这个公钥放在 /etc/salt/pki/master/minions/,状态是 unaccepted。 运维在 master 上执行 salt-key -A 接受(或者 -a  接受单个), master 重启自己的 minion 缓存,minion 下一次心跳会拉到 master 的公钥, 双方建立信任。

接受之后,minion 就可以发作业请求了。master 会校验 minion_id 和 minion 公钥是否匹配。

常见误区:

直接 salt-key -A 接受所有 unaccepted 主机,这在生产里是大忌。 我们公司要求:"salt-key 必须按主机名白名单接受",由堡垒机工单系统推过来。

改完 hostname 之后 minion 重新生成密钥对,老的 accepted 列表会失效, 表现为 Minion did not return. [No response]。需要先 salt-key -d old_hostname 删旧的,再 -A 收新的。

2.4 拓扑

我们生产用的是双 master(failover)拓扑,结构如下:

 

                 +------------------+
                 |   salt-master-A  |  <--- ops / ci 调用
                 +------------------+         ↑
                          ↑                   ↑
                  master_type: failover      ↑
                          ↑                   ↑
+-------------+    +------+-------+    +------+-------+
| minion-1    |--->|              |    |              |
+-------------+    | salt-master-B|<-->|  ops host    |
+-------------+    |              |    |  (admin)     |
| minion-2    |--->|              |    +--------------+
+-------------+    +--------------+

(实际环境两个 master 都做 master,互不依赖。
minion 配置 master: [A, B],master_type: failover,按列表顺序尝试。
我们不用双主热切换,因为状态文件用 Git 分发,比 keepalived 简单可靠。)

 

单机小规模:单 master 就够。 中大规模(500–5000):多 master failover。 超大规模(5000+):拆 BU + syndic 或者直接上多个独立 master 集群。

2.5 与 Ansible 的本质差异

简单说三件事:

长连接 vs 短连接。 Salt 是 minion 启动就常驻,连一次保活,作业直接通过 ZeroMQ 推下去,毫秒级。 Ansible 默认是 SSH 短连接,每次执行都要建链。 这点在小规模上没差别,几千台就有差别。

有 agent vs 无 agent。 Salt 必须装 minion。 Ansible 不用,但代价是每台机器都得有 Python 和 SSH 服务。 装机量大的团队,"装 minion" 反而是优势,因为装机过程就会把 minion 一起装上。

状态 vs 任务。 Salt 鼓励你写 SLS(声明式状态),系统自己收敛到期望状态。 Ansible 偏任务式,playbook 是步骤清单。 这个差异在配置管理场景里影响很大。

2.6 目录结构

master 上几个关键目录,生产环境建议按这个走:

 

/etc/salt/
  master                              # master 主配置
  minion                              # master 自己也可以跑 minion
  pki/
    master/
      master.pub                      # master 公钥
      master.pem                      # master 私钥
      minions/                        # 已接受的 minion 公钥
      minions_pre/                    # 待接受
      minions_rejected/               # 已拒绝
/srv/salt/                            # state 文件根(file_roots: base)
  top.sls
  nginx/
  ssh/
  ...
/srv/pillar/                          # pillar 文件根(pillar_roots: base)
  top.sls
  nginx/
  ...
/var/log/salt/                        # master / minion 日志
  master
  minion
/var/cache/salt/                      # job cache、minion cache
/etc/salt/minion                      # minion 配置(被管机器上)

 

/srv/salt 和 /srv/pillar 在 master 和 minion 上的角色不一样: master 上是源数据,minion 上是 master 推过来的渲染结果,路径不一定在 /srv, 可以通过 file_client: local 或者 roots 配置改。

三、环境准备与安装

3.1 操作系统与 Python 版本矩阵

生产里我们见过最稳的组合:

操作系统 Python 默认版本 Salt 推荐版本 备注
CentOS 7.9 2.7(系统) Salt 3006.6+(用 SCL 装 Py3) 装 python36 SCL,让 minion 走 Py3
Rocky Linux 8.6 3.6(系统) Salt 3006.x 直接 pip 或官方 repo
Rocky Linux 9.x 3.9 / 3.11 Salt 3006.x 默认 Py3 兼容好
Ubuntu 20.04 3.8 Salt 3006.x apt 源直接装
Ubuntu 22.04 3.10 Salt 3006.x apt 源直接装

不要在生产里用 Salt 2018 / 2019 这种老版本跑 Py2.7 master。 Py2.7 已经 EOL,各种库不再维护,安全漏洞没人修。 如果机器上还有 Py2 的脚本共存,让 minion 跑在 SCL / venv 的 Py3 环境里, 不要去改系统默认 Python。

3.2 仓库配置

CentOS 7 配官方源(盐项目官方源在国内访问不太稳,建议走阿里云镜像):

 

# /etc/yum.repos.d/salt.repo
[salt]
name=SaltStack repo for RHEL/CentOS $releasever
baseurl=https://mirrors.aliyun.com/salt/rpm/latest/$releasever/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/salt/rpm/latest/$releasever/SALTSTACK-GPG-KEY.pub

 

Rocky 8 / RHEL 8:

 

dnf install -y epel-release
dnf config-manager --add-repo https://mirrors.aliyun.com/salt/rpm/latest/8/

 

Ubuntu 20.04:

 

curl -fsSL -o /usr/share/keyrings/salt-archive-keyring.gpg 
  https://mirrors.aliyun.com/salt/deb/ubuntu/20.04/amd64/latest/salt-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/salt-archive-keyring.gpg] 
  https://mirrors.aliyun.com/salt/deb/ubuntu/20.04/amd64/latest focal main" 
  > /etc/apt/sources.list.d/salt.list
apt-get update

 

这里有个坑:很多教程让你装 salt-master 包名,但 Salt 3000+ 的包名变了。 请按下面这套来:

角色 包名(CentOS) 包名(Ubuntu)
master salt-master salt-master
minion salt-minion salt-minion
syndic salt-syndic salt-syndic
api salt-api salt-api
通用依赖 salt-common salt-common

salt-common 包含所有 Python 库依赖,单独装一份常用来排错(rpm -ql salt-common 看一下)。

3.3 安装

3.3.1 YUM / APT 安装(生产推荐)

master:

 

yum install -y salt-master salt-api salt-cloud salt-ssh
systemctl enable salt-master
systemctl start salt-master

 

minion:

 

yum install -y salt-minion
systemctl enable salt-minion
systemctl start salt-minion

 

3.3.2 pip 安装(容器化场景)

容器里我们更倾向 pip,因为基础镜像通常不带 systemd:

 

pip3 install salt==3006.6 salt-api==3006.6

 

容器化 Salt 的 master 进程启动方式比较 hack,需要自己写个 entrypoint 起 salt-master:

 

#!/bin/bash
# /entrypoint.sh
set -e

if [ "$1" = "master" ]; then
    exec salt-master -l warning
elif [ "$1" = "minion" ]; then
    exec salt-minion -l warning
elif [ "$1" = "api" ]; then
    exec salt-api -l warning
else
    exec "$@"
fi

 

注意:容器化 master 不适合做大规模(性能不如物理机),适合做 dev / test 沙箱。 生产 master 我们坚持用物理机或稳定 VM。

3.4 关键依赖

rpm -qR salt-master 能看到所有依赖,下面这几个出问题最多:

python3-pyzmp:ZeroMQ 的 Python 绑定。版本不匹配会报zmq.error.ZMQError: Protocol not supported,重装就能好。

python3-crypto / pycryptodome:AES 加密、pillar gpg 都靠它。

python3-msgpack:作业 / pillar 序列化。

python3-jinja2:SLS 模板渲染。

python3-looseversion:版本比较。

python3-yaml:YAML 解析。

YUM 装的版本是 Salt 官方测过的,最稳。不要自己 pip install 升级这些库,会和包管理器的版本冲突。

3.5 启动参数

master 启动参数:

 

# 守护进程
salt-master -d
# debug 模式(前台、详细日志、排错用)
salt-master -l debug
# 改日志输出位置
salt-master --log-file=/var/log/salt/master.log
# 改配置文件路径
salt-master -c /etc/salt
# 改 user
salt-master --user salt

 

minion 启动参数和 master 类似,常见排错组合:

 

salt-minion -l debug

 

把 minion 跑在前台 + debug 日志,是定位"为什么连不上 master"最快的办法。 看 /var/log/salt/minion 里的关键报错,比如 Failed to authenticate、No master found、Key exchange failed,90% 的断连问题能从日志里直接看出原因。

3.6 minion 注册流程

这是新机器上线的标准动作:

 

# 1. minion 端配置 /etc/salt/minion
master: 10.20.0.10
id: web-prod-01

# 2. 启动 minion
systemctl start salt-minion

# 3. minion 端查看自己的公钥
cat /etc/salt/pki/minion/minion.pub

# 4. master 端查看待接受列表
salt-key -L
# 输出:
# Unaccepted Keys:
#   web-prod-01
# Accepted Keys:
#   ...

# 5. 接受单个
salt-key -a web-prod-01
# 6. 接受所有(仅测试环境)
salt-key -A
# 7. 拒绝单个
salt-key -D
# 8. 删除一个已接受的
salt-key -d web-prod-01

 

salt-key 的参数:

-L:列出所有(分 Unaccepted / Accepted / Rejected 三组)。

-A:接受所有 unaccepted。

-a :接受指定 id。

-D:删除所有 accepted(生产里不要用)。

-d :删除指定 id。

-R:拒绝所有 unaccepted(标记为 Rejected)。

-r :拒绝指定 id。

-F:打印 fingerprint,配合 -l 可以看哪个 key 对应哪个主机。

风险提醒:salt-key -D 会把所有已接受 minion 全部踢下线。 生产里执行前一定要先 salt-key -L | tee /tmp/before-rm.txt 留个底。

3.7 防火墙与端口

master 端要开放 4505、4506 给 minion 段。 不要把这两个端口暴露到公网,只在 IDC 内部网或 VPN 通道开放。

 

# master 端防火墙
firewall-cmd --permanent --add-port=4505/tcp
firewall-cmd --permanent --add-port=4506/tcp
firewall-cmd --reload

# 或者 iptables
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 4505 -j ACCEPT
iptables -A INPUT -p tcp -s 10.20.0.0/16 --dport 4506 -j ACCEPT

 

minion 端不需要开放端口(minion 是主动连 master 的)。

3.8 安装后的快速验证

 

# 看 master 上接受的 minion 数量
salt-key -L | grep -c "^[a-zA-Z]"

# 给所有 minion 发个 ping
salt '*' test.ping
# 输出形如:
# web-prod-01:
#   True
# web-prod-02:
#   True

# 看版本
salt '*' test.version
# 输出形如:
# web-prod-01:
#   3006.6

# 看 grains(基础信息)
salt '*' grains.items
# 看网络信息
salt '*' network.ip_addrs
salt '*' network.interfaces

# 看 disk / cpu / mem
salt '*' disk.usage
salt '*' status.diskstats
salt '*' status.meminfo
salt '*' status.cpustats
salt '*' status.loadavg
salt '*' status.uptime
salt '*' status.all_status
salt '*' status.w
salt '*' status.version
salt '*' status.pid

 

这几条命令是后面所有排错的基础,先确保它们在 test=True(不实际改动)下都跑通。

四、核心概念深入

SaltStack 的"概念"在文档里写得零零散散,我们按工程上理解的先后顺序重新组织一遍。

4.1 Grains

Grains 是 minion 启动时收集的静态信息,保存在 minion 端。 常见 grains 字段:

 

os: CentOS
os_family:RedHat
osarch:x86_64
kernel:Linux
cpuarch:x86_64
num_cpus:16
mem_total:64258
ipv4:[10.20.0.11,10.20.0.12]
fqdn:web-prod-01.example.com
id:web-prod-01

 

自定义 grains,写在 minion 的 /etc/salt/grains(YAML 格式):

 

# /etc/salt/grains
role: nginx
env: prod
zone: cn-bj-1

 

或者在 minion 配置里指定:

 

# /etc/salt/minion
grains:
  role: nginx
  env: prod
  zone: cn-bj-1

 

自定义 grains 用法:

 

# 按 role 过滤
salt -G 'role:nginx' test.ping
# 按 env 过滤
salt -G 'env:canary' test.ping
# 多条件
salt -G 'role:nginx and env:prod' test.ping
salt -G 'role:mysql or role:redis' test.ping

 

Grains 适合做"机器特征",比如机房、角色、OS 版本。不要把变化频繁的数据写到 grains,比如"今天是不是在灰度名单里"。

4.2 Pillar

Pillar 是 master 端维护的、给特定 minion 用的数据。和 grains 的区别:

维度 Grains Pillar
存在端 minion master
谁维护 minion 启动时自己 运维在 master 上写
适用 静态、机器特征 动态、配置、敏感数据
同步 主动 push minion 拉
加密 不支持 支持 gpg

Pillar 的目录结构:

 

/srv/pillar/
  top.sls
  base/
    init.sls
  nginx/
    init.sls
  mysql/
    init.sls

 

/srv/pillar/top.sls:

 

base:
  '*':
    -base.init
'role:nginx':
    -match:grain
    -nginx.init
'role:mysql and env:prod':
    -match:grain
    -mysql.init
    -mysql.prod

 

/srv/pillar/base/init.sls:

 

timezone: Asia/Shanghai
ntp_servers:
  - ntp1.example.com
  - ntp2.example.com

 

/srv/pillar/nginx/init.sls:

 

nginx:
  port: 80
  workers: "{{ grains['num_cpus'] }}"
  domains:
    - example.com
    - example.org

 

/srv/pillar/mysql/init.sls:

 

mysql:
  server_id: "10{{ grains['id'][-3:]|int }}"
  binlog_format: ROW
  innodb_buffer_pool_size: "{{ (grains['mem_total'] * 0.6) | int }}M"

 

注意:server_id 这种数字如果直接用 jinja 拼接,minion_id 又不是数字前缀, 会拼出错。我们用 [-3:]|int 这种方式做尾巴截取 + 转 int, 对 ID 是 web-prod-001 的机器就能正常生成 server_id = 101。

Pillar 调试命令:

 

# 看某台机器的 pillar 全量
salt 'web-prod-01' pillar.items
# 拿一个 key
salt 'web-prod-01' pillar.get nginx:port
# 强制刷新(minion 端拉一次)
salt 'web-prod-01' saltutil.refresh_pillar
# 看哪些 SLS 命中了
salt 'web-prod-01' pillar.show_top

 

敏感数据加密:用 gpg-render 把 Pillar 文件用 gpg key 加密, master 配置:

 

# /etc/salt/master
pillar:
  gpg_render: True
  gpg_keydir: /etc/salt/gpgkeys

 

加密命令:

 

gpg --gen-key
# 导出公钥到 master
gpg --export --armor > /etc/salt/gpgkeys/pub.key
# 用私钥加密 SLS
gpg --encrypt --sign --armor -r user@example.com /srv/pillar/secret.sls

 

加密后的 pillar 文件是 ASCII armor 格式,可以放心进 Git。 不过实际项目里我们更倾向用 vault 拉取 token 注入,gpg 维护起来很重。

4.3 State(SLS)

SLS 是 Salt 的核心,结构是 YAML 渲染 + Jinja2 模板。

4.3.1 基础结构

 

# /srv/salt/nginx/init.sls
nginx:
pkg.installed:
    -name:nginx
service.running:
    -name:nginx
    -enable:True
    -watch:
      -file:/etc/nginx/nginx.conf

/etc/nginx/nginx.conf:
file.managed:
    -source:salt://nginx/files/nginx.conf
    -template:jinja
    -user:root
    -group:root
    -mode:644
    -require:
      -pkg:nginx

 

几个关键字段解释:

ID:state 文件里的资源 ID,state 调用时按 ID 寻址。

state.module:状态模块,常见有 pkg.installed、file.managed、service.running。

name:模块实际操作的资源名。省略时用 ID。

require:依赖关系,声明 A 必须在 B 之后执行。

watch:和 require 类似,但当被 watch 的资源变更时,触发 service reload。

unless:条件守卫,条件为真则跳过。

onlyif:条件守卫,条件为真才执行。

extend:在其他 state 文件中追加属性。

4.3.2 优先级

SLS 优先级(requisite 链)的执行顺序:

require / require_in:强依赖。

watch / watch_in:变更触发。

onfail / onfail_in:失败触发。

onchanges / onchanges_in:变化触发。

prereq / prereq_in:被依赖方先执行。

最常用的就是 require 和 watch,其他的进阶场景才用。

4.3.3 顺序控制

 

# A 必须在 B 之前
A:
...
-require:
    -pkg:B

# B 必须在 A 之前
A:
...
-require_in:
    -pkg:B

 

require_in 写在 A 里,但语义是"为了让 B 成功,A 必须先成功"。 两个方向都对,区别在于"以谁为锚点写状态"。

4.3.4 条件判断

 

/etc/nginx/conf.d/default.conf:
  file.absent:
    - name: /etc/nginx/conf.d/default.conf
    - unless:
      - ls /etc/nginx/conf.d/default.conf
nginx:
  pkg.installed:
    - name: nginx
    - onlyif:
      - test "$(rpm -q nginx | wc -l)" == "0"

 

onlyif 和 unless 后面跟的是 shell 命令列表。注意:unless/onlyif 在 minion 端执行,如果 minion 是 root 跑命令,就别在命令里写需要登录 shell 的语法。

4.3.5 引用和继承

 

# /srv/salt/top.sls
base:
'*':
    -common
    -ssh.hardening
'role:nginx':
    -match:grain
    -nginx
'role:mysql':
    -match:grain
    -mysql

 

include 在 state 文件内引用其他 SLS:

 

# /srv/salt/web/init.sls
include:
  - common
  - nginx
  - ssh.hardening

 

extend 在不修改原 SLS 的情况下追加:

 

# /srv/salt/nginx/ext.sls
include:
  - nginx

extend:
  /etc/nginx/nginx.conf:
    file.managed:
      - mode: 600

 

这在"上游有公共 base,子公司要本地化微调"的场景里非常有用。

4.4 Render Pipeline

state 文件从写出来到执行,要经过四步:

YAML 解析:把 SLS 文件解析成 Python 字典。

Jinja 渲染:把 {{ grains['os'] }} 这种变量替换成实际值。

PyObjects 转换:把字典转成 state 内部对象(PyObjects)。

HighState 排序:按 require 链把执行顺序排出来,输出执行计划。

调试时如果渲染出问题,可以用 state.show_sls 单独看渲染结果:

 

salt 'web-prod-01' state.show_sls nginx
salt 'web-prod-01' state.show_lowstate
salt 'web-prod-01' state.show_highstate

 

show_highstate 能看到所有 minion 应当执行的状态、当前实际状态、以及 diff。这是排查"为什么这个 state 改不下去"的第一步。

4.5 Orchestrate

Orchestrate 是 runner 模块里的 state 子模块,作用是跨 minion 编排执行顺序。 比如:"先在 A 上跑 SQL 升级,成功后再在 B 上跑"。

 

salt-run state.orchestrate orchestrate.web_upgrade

 

对应的 SLS 在 /srv/salt/orchestrate/web_upgrade.sls(或 /srv/salt/orchestrate/web_upgrade/init.sls):

 

# /srv/salt/orchestrate/web_upgrade.sls
upgrade_db_master:
salt.state:
    -tgt:'role:db and role_master:True'
    -sls:
      -mysql.upgrade
    -failhard:True

upgrade_db_slave:
salt.state:
    -tgt:'role:db and role_master:False'
    -sls:
      -mysql.upgrade
    -require:
      -salt:upgrade_db_master
    -failhard:True

upgrade_app:
salt.state:
    -tgt:'role:web'
    -sls:
      -web.reload
    -require:
      -salt:upgrade_db_slave
    -failhard:True

 

failhard: True 是关键——一个环节失败立刻终止整个编排, 避免雪崩式故障。

4.6 Returner

默认情况下,作业 return 存在 master 内存里。 但 master 重启就没了,对审计和排错不友好。 returner 把 return 落到外部存储。

常见 returner:

Returner 配置 适用
local /var/cache/salt/job 单机调试
mysql 写到 MySQL jids / salt_returns 表 长期审计
redis 写到 Redis list 高吞吐场景
kafka 写 topic 接 ELK 做日志分析
sentry 失败时上报 异常监控
slack 失败时发通知 简单告警

MySQL returner 配置示例:

 

# /etc/salt/master
master_job_cache: mysql
mysql.host: '127.0.0.1'
mysql.user: 'salt'
mysql.pass: 'salt_pwd'
mysql.db: 'salt'
mysql.port: 3306

 

建表 SQL(官方自带):

 

CREATE DATABASEsaltCHARACTERSET utf8mb4;
USEsalt;
-- jids 表
CREATETABLE jids (
  jid varchar(20) NOTNULL PRIMARY KEY,
load mediumtext NOTNULL
) ENGINE=InnoDB;
-- salt_returns 表
CREATETABLE salt_returns (
  fun varchar(50) NOTNULL,
  jid varchar(20) NOTNULL,
return mediumtext NOTNULL,
idvarchar(255) NOTNULL,
successvarchar(20) NOTNULL,
  full_ret mediumtext NOTNULL,
  alter_time TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP,
KEYid (id),
KEY jid (jid),
KEY fun (fun)
) ENGINE=InnoDB;
-- salt_events 表(事件流)
CREATETABLE salt_events (
idBIGINTNOTNULL AUTO_INCREMENT PRIMARY KEY,
  tag varchar(255) NOTNULL,
data mediumtext NOTNULL,
  alter_time TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP,
KEY tag (tag)
) ENGINE=InnoDB;

 

风险提醒:MySQL returner 的写频率是按作业来算的,5000 台并发时 SQL 写入很快。 要给 salt 用户的 salt_returns 表加合适索引,并把 master 的event_match_type 调成 startswith 或 pcre,否则 master 端 CPU 会打满。

4.7 Reactor

Reactor 在 master 监听 event bus,事件触发执行 state / runner。 比如"minion 启动事件触发注册动作"。

 

# /etc/salt/master
reactor:
  - 'salt/minion/*/start':
    - /srv/salt/reactor/minion_start.sls
  - 'salt/job/*/ret/*':
    - /srv/salt/reactor/job_ret.sls

 

reactor SLS:

 

# /srv/salt/reactor/minion_start.sls
log_start:
  local.cmd.run:
    - tgt: '*'
    - expr_form: glob
    - arg:
      - echo "{{ data['id'] }} started at {{ data['stamp'] }}"

 

注意:reactor 写起来方便但滥用会让 master 事件流死循环,不要在 reactor 里再触发会写 event 的 action。

4.8 Scheduler 和 Beacon

Scheduler:minion 端定时任务,类似 cron。 Beacon:minion 端系统事件采集。

 

# /etc/salt/minion
schedule:
highstate:
    function:state.highstate
    minutes:30

beacons:
load:
    -averages:
        1m:
          -0.0
          -10.0
        5m:
          -0.0
          -5.0
    -emit_at_rest:True
inotify:
    -/etc/nginx:{}

 

beacon 把系统状态变化转成 event,master 端用 reactor 接住就能联动。 比如发现某台机器 load 飙高,自动发 salt job 抓现场。

五、常用命令与排查路径

5.1 目标匹配

Salt 的目标匹配(targeting)非常灵活,是日常 80% 的命令入口。

 

# 通配
salt 'web*' test.ping
# 正则
salt -E 'web-(prod|canary)-.*' test.ping
# 列表
salt -L 'web-01,web-02,web-03' test.ping
# Grains
salt -G 'role:nginx and env:prod' test.ping
salt -G 'os:CentOS and mem_total:>30000' test.ping
# Pillar
salt -P 'nginx80' test.ping
# CIDR
salt -S '10.20.0.0/24' test.ping
# Nodegroup(在 master 配置里预定义)
salt -N web-cluster test.ping

 

master 端 nodegroups 配置:

 

# /etc/salt/master
nodegroups:
  web-cluster: 'G@role:web and G@env:prod'
  db-cluster: 'G@role:mysql and G@env:prod'
  canary: 'G@env:canary'

 

风险提醒:-L 后面跟一千个 ID 是合法的,但容易拼错。 我们要求生产环境的批量执行强制走 nodegroup 或者 -G,避免误操作。

5.2 常用模块

5.2.1 cmd

 

# 单条命令
salt '*' cmd.run 'uptime'
# 带超时
salt '*' cmd.run 'long_task.sh' timeout=60
# stdin 传参
salt '*' cmd.run_stdin 'echo $0'
# 看完整 shell 环境
salt '*' cmd.run 'env'
# 拿 exit code
salt '*' cmd.retcode 'nginx -t'
# 跑脚本(注意 shell 转义)
salt '*' cmd.script 'salt://scripts/check.sh'
salt '*' cmd.script 'https://example.com/check.sh'
# 看进程树
salt '*' cmd.run 'ps -ef | head'

 

风险提醒:cmd.run 是"任意命令执行"。 生产里 cmd.run 必须走 salt-api,不允许直接 SSH 到 master 跑。 salt-api 后台要开 audit returner 留痕。

5.2.2 service

 

# 看运行状态
salt '*' service.status nginx
salt '*' service.available nginx
salt '*' service.enabled nginx
# 启停
salt '*' service.start nginx
salt '*' service.stop nginx
salt '*' service.restart nginx
salt '*' service.reload nginx
# enable / disable
salt '*' service.enable nginx
salt '*' service.disable nginx

 

5.2.3 pkg

 

# 装包
salt '*' pkg.install nginx
salt '*' pkg.install pkgs='["nginx","php-fpm"]'
# 卸包
salt '*' pkg.remove nginx
# 升级
salt '*' pkg.upgrade
salt '*' pkg.upgrade available=True
# 看列表
salt '*' pkg.list_installed
salt '*' pkg.list_upgrades
salt '*' pkg.version nginx
# 加源(CentOS)
salt '*' pkg.installed https://example.com/repo.rpm
# 加源(Ubuntu)
salt '*' pkgrepo.managed name='example' uri='https://example.com/repo'

 

5.2.4 file

 

# 看文件信息
salt '*' file.stats /etc/nginx/nginx.conf
# 看内容
salt '*' file.read /etc/nginx/nginx.conf
# 查文件是否存在
salt '*' file.file_exists /etc/nginx/nginx.conf
# 替换文本
salt '*' file.replace /etc/nginx/nginx.conf pattern='^worker_processes.*' repl='worker_processes auto;'
# 加一行
salt '*' file.append /etc/hosts text='10.20.0.100 db.example.com'
# 删文件
salt '*' file.remove /tmp/old.log
# 改权限
salt '*' file.set_mode /etc/redis.conf mode='0640'
salt '*' file.set_user /etc/redis.conf user=redis

 

风险提醒:file.remove 会真删文件,必须配合 unless。file.replace 用错了正则可能把配置改坏,先 dry-run 看一下。

5.2.5 user / group

 

salt '*' user.add deployer uid=2001 shell=/bin/bash
salt '*' user.present name=deployer shell=/bin/bash groups=['sudo','docker']
salt '*' user.delete deployer remove=True
salt '*' group.present name=deploy
salt '*' group.adduser deploy deployer

 

5.2.6 cron

 

salt '*' cron.present name='logrotate' user=root minute=0 hour=2 command='/usr/sbin/logrotate /etc/logrotate.conf'
salt '*' cron.absent name='logrotate' user=root
salt '*' cron.list_tab root

 

5.2.7 mount

 

salt '*' mount.mounted /data fstype=nfs device='nfs.example.com:/data' opts='defaults,_netdev' dump=0 pass_num=0
salt '*' mount.swapon /swapfile
salt '*' mount.fstab
salt '*' mount.umount /data

 

风险提醒:mount.mounted 默认会写 /etc/fstab,挂错的 NFS 会让机器重启卡住。 生产里要 test=True 先验证。

5.2.8 network

 

salt '*' network.ip_addrs
salt '*' network.interfaces
salt '*' network.routes
salt '*' network.active_tcp
salt '*' network.dig www.example.com
salt '*' network.ping host=8.8.8.8
salt '*' network.traceroute host=8.8.8.8
salt '*' network.netstat
salt '*' network.ss

 

5.3 状态执行与 dry-run

 

# 单 SLS
salt 'web*' state.sls nginx
# 加 test=True 不实际改动
salt 'web*' state.sls nginx test=True
# 显式指定 env
salt 'web*' state.sls nginx saltenv=base
# 全量
salt '*' state.highstate
salt '*' state.highstate test=True
# 应用(推 top.sls 命中之外的所有 state)
salt '*' state.apply
# 单独跑一个 state(带参数)
salt '*' state.single pkg.installed name=nginx
# 高 detail 输出
salt '*' state.sls nginx -l debug --state-output=mixed
# 详细显示 change / diff
salt '*' state.sls nginx --state-output=changes

 

--state-output 的几个值:

full:默认,显示所有字段。

terse:精简。

mixed:change 块。

changes:只显示 diff。

no:不显示。

排错时 mixed 最好用,能看到本次到底改了什么。

5.4 异步执行

 

# 同步等所有 minion 跑完
salt '*' cmd.run 'sleep 5' --timeout=10
# 异步立即返回 jid
salt '*' cmd.run 'sleep 5' --async
# 输出形如:
# Executed command with job ID: 20250506102030123456
# 之后查结果
salt-run jobs.lookup_jid 20250506102030123456
# 查正在跑的
salt-run jobs.active
# 查最近一次
salt 'web*' jobs.last
# 杀作业
salt 'web*' jobs.kill 20250506102030123456
salt-run jobs.kill_job 20250506102030123456

 

风险提醒:jobs.kill 只能杀 minion 上的 salt-minion 进程,不能杀底层命令。 如果要彻底杀掉进程,用 cmd.run 'pkill -f xxx',但更彻底的做法是写 SLS。

5.5 超时控制

 

# 单条命令超时
salt '*' cmd.run 'long_task' timeout=300
# 全局超时
salt '*' cmd.run 'long_task' --timeout=300
# state 执行超时
salt '*' state.sls nginx --timeout=600

 

注意 timeout 单位是秒,Salt 默认 5 秒,远低于很多实际场景。 我们生产里把 global timeout 调到 60,单独任务可以 --timeout 覆盖。

5.6 排查命令

 

# master 端看 minion 在线
salt-run manage.status
salt-run manage.up
salt-run manage.down
# 看 minion 端到 master 的连接状态
salt '*' test.ping
# 强制 minion 重新连 master
salt 'web*' service.restart salt-minion
# 强制 minion 重新读 pillar
salt 'web*' saltutil.refresh_pillar
# 强制 minion 重新读 grains
salt 'web*' saltutil.sync_grains
# 强制 minion 重新同步 module
salt 'web*' saltutil.sync_modules
salt 'web*' saltutil.sync_all
# 看 master 上的 jobs
salt-run jobs.list_jobs
salt-run jobs.list_jobs search_function='cmd.run'
# 跟踪一个 jid
salt-run jobs.lookup_jid 20250506102030123456
# 看 master 上的 event
salt-run state.event pretty=True
# 看 master 内部状态
salt-run status
salt-run config.get
# 看 pillar / grains
salt '*' pillar.items
salt '*' grains.items
# 本地执行(minion 不需要 master)
salt-call --local test.ping
salt-call --local state.highstate test=True
# 同步 state 文件到 minion
salt '*' saltutil.sync_states
# 看某个 SLS 编译后的结果
salt '*' state.show_sls nginx
# 详细 diff
salt '*' state.sls nginx test=True -l debug --state-output=changes

 

test=True + -l debug + --state-output=changes 是排 state 问题的"三件套"。

六、实战配置示例

下面这些 SLS 是我们生产里跑过的真实配置(去敏感化),每一个都带:

业务背景

pillar / state 文件

灰度步骤

回滚方案

风险提醒

6.1 Nginx 统一部署

6.1.1 背景

公司 600 多台 Web 机器,跑同一个编译版的 Nginx。 每个机房一份 vhost 列表,PHP-FPM 监听端口不一致。 统一用 SLS 收敛以后,新机器装机 10 分钟内就有完整的 Nginx。

6.1.2 Pillar

 

# /srv/pillar/nginx/init.sls
nginx:
port:80
workers:"{{ grains['num_cpus'] }}"
user:nginx
group:nginx
vhosts:
    -name:example.com
      root:/var/www/example.com
      php:True
    -name:static.example.com
      root:/var/www/static
      php:False
ssl:
    enabled:True
    cert:/etc/pki/tls/certs/example.com.crt
    key:/etc/pki/tls/private/example.com.key

 

6.1.3 State

 

# /srv/salt/nginx/init.sls
nginx-repo:
pkgrepo.managed:
    -name:nginx-stable
    -humanname:NginxStableRepo
    -baseurl:https://nginx.org/packages/centos/$releasever/$basearch/
    -gpgcheck:1
    -gpgkey:https://nginx.org/keys/nginx_signing.key

nginx:
pkg.installed:
    -name:nginx
    -require:
      -pkgrepo:nginx-repo
service.running:
    -name:nginx
    -enable:True
    -watch:
      -file:/etc/nginx/nginx.conf
      -file:/etc/nginx/conf.d

nginx-user:
user.present:
    -name:nginx
    -uid:998
    -gid:998
    -shell:/sbin/nologin

/etc/nginx/nginx.conf:
file.managed:
    -source:salt://nginx/files/nginx.conf
    -template:jinja
    -user:root
    -group:root
    -mode:'0644'
    -require:
      -pkg:nginx

/etc/nginx/conf.d:
file.directory:
    -name:/etc/nginx/conf.d
    -user:root
    -group:root
    -mode:'0755'
    -makedirs:True

{%forvhostinpillar.get('nginx:vhosts',[])%}
/etc/nginx/conf.d/{{vhost.name}}.conf:
file.managed:
    -source:salt://nginx/files/vhost.conf
    -template:jinja
    -user:root
    -group:root
    -mode:'0644'
    -defaults:
        vhost:{{vhost}}
        ssl:{{pillar['nginx']['ssl']}}
    -require:
      -file:/etc/nginx/conf.d
    -watch_in:
      -service:nginx
{%endfor%}

 

/srv/salt/nginx/files/nginx.conf(模板片段):

 

user {{ pillar['nginx']['user'] }};
worker_processes {{ pillar['nginx']['workers'] }};
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 10240;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    server_tokens off;

    {% if pillar['nginx']['ssl']['enabled'] %}
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH!MD5;
    {% endif %}

    include /etc/nginx/conf.d/*.conf;
}

 

6.1.4 灰度

 

# 1. canary 机器
salt -G 'env:canary and role:web' state.sls nginx test=True
salt -G 'env:canary and role:web' state.sls nginx

# 2. 10% 抽样
salt -G 'env:prod and role:web' state.sls nginx test=True --batch-size=10%

# 3. 全量
salt -G 'role:web' state.sls nginx

 

--batch-size=10% 是 Salt 3000+ 的批量执行参数,每批之间有间隔, 出问题能及时停。

6.1.5 回滚

 

# 回退前先备份老配置
salt -G 'role:web' cmd.run 'cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%s)'

# 如果新版有问题,强制恢复
salt -G 'role:web' cmd.run 'cp /etc/nginx/nginx.conf.bak.* /etc/nginx/nginx.conf && nginx -t && nginx -s reload'

 

6.1.6 风险提醒

模板渲染失败会导致 nginx -t 不过,从而 service.running 失败。 一定要 test=True 先看 diff。

启停脚本里默认 nginx -s reload 在 worker 错配时不会成功, 加上 test=True 的 service.status 二次确认。

vhost 列表改了以后,老的 vhost.conf 不会被自动删除, 要么手写 file.absent,要么先在 file.directory 上加 clean: True(Salt 3006+)。

6.2 系统初始化基线

6.2.1 背景

新机器从装机到上线,OS、kernel、ssh、limits、ntp、timezone、hosts 都要按基线打。 我们用 orchestrate 编排整个流程。

6.2.2 Pillar

 

# /srv/pillar/common/init.sls
timezone:Asia/Shanghai
ntp_servers:
-ntp1.example.com
-ntp2.example.com
-ntp3.example.com
sysctl:
net.core.somaxconn:65535
net.ipv4.tcp_max_syn_backlog:65535
vm.swappiness:10
limits:
-domain:'*'
    type:soft
    item:nofile
    value:65535
-domain:'*'
    type:hard
    item:nofile
    value:65535

 

6.2.3 State

 

# /srv/salt/common/init.sls
timezone:
file.symlink:
    -name:/etc/localtime
    -target:/usr/share/zoneinfo/{{pillar['timezone']}}
    -force:True

ntp:
pkg.installed:
    -name:chrony
service.running:
    -name:chronyd
    -enable:True
    -watch:
      -file:/etc/chrony.conf

/etc/chrony.conf:
file.managed:
    -source:salt://common/files/chrony.conf
    -template:jinja
    -mode:'0644'

{%fork,vinpillar.get('sysctl',{}).items()%}
sysctl-{{k}}:
sysctl.present:
    -name:{{k}}
    -value:{{v}}
{%endfor%}

{%forlimitinpillar.get('limits',[])%}
limits-{{limit.item}}-{{limit.type}}:
pam_limits.present:
    -name:{{limit.item}}
    -type:{{limit.type}}
    -value:{{limit.value}}
{%endfor%}

hostname:
network.system:
    -hostname:{{grains['id']}}
    -retain_hosts:True

/etc/hosts:
file.managed:
    -source:salt://common/files/hosts
    -template:jinja
    -mode:'0644'

 

6.2.4 灰度

 

# 先在 1 台机器上
salt -L 'web-canary-01' state.sls common test=True
# 没问题再批量
salt -G 'env:canary' state.sls common
salt -G 'env:prod' state.sls common --batch-size=10%

 

6.2.5 回滚

sysctl 和 limits 都是回写到文件,机器重启前不会生效。 出问题回滚只要恢复 /etc/sysctl.conf 和 /etc/security/limits.conf 即可。 hosts 改坏了会导致服务连不上数据库,**先 test=True**,再加 --batch-size=10%。

6.2.6 风险提醒

改 hostname 必须同步改 hosts,否则 minion 自身重启后无法连回 master。network.system 模块会自动写 /etc/hostname 和 /etc/sysconfig/network(CentOS), 如果 minion 跑在容器里要小心,hostname 改了容器 ID 变了。

sysctl 改完不会立即生效,需要 sysctl -p 或重启网络服务。 Salt 的 sysctl.present 模块会执行 sysctl -w,但持久化在 /etc/sysctl.conf。

6.3 SSH 安全加固

6.3.1 背景

每次出新机器,SSH 端口是 22、root 能登录、密码认证开着。 我们用 SLS 把这些都收敛到"加固"基线。

6.3.2 State

 

# /srv/salt/ssh/hardening.sls
sshd:
pkg.installed:
    -name:openssh-server

sshd_config:
file.managed:
    -name:/etc/ssh/sshd_config
    -source:salt://ssh/files/sshd_config
    -template:jinja
    -user:root
    -group:root
    -mode:'0600'

ssh-banner:
file.managed:
    -name:/etc/ssh/banner
    -source:salt://ssh/files/banner
    -mode:'0644'

sshd-service:
service.running:
    -name:sshd
    -enable:True
    -watch:
      -file:sshd_config

# 防止把自己锁在外面:检查加固完成后是否还允许 root + 密码登录
ssh-hardening-guard:
cmd.run:
    -name:echo"sshd_hardened_ok"
    -unless:
      -grep-E'^[#s]*PermitRootLogins+yes'/etc/ssh/sshd_config

 

/srv/salt/ssh/files/sshd_config 关键配置:

 

Port 2222
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
Banner /etc/ssh/banner

 

6.3.3 灰度

SSH 加固有一个最大的坑:如果你把当前 SSH 连接的端口 22 关了、密码禁了, 会直接把自己锁在机器外面。 所以正确做法:

灰度批次机器(不要是当前会话所在机器)。

新配置里先保留 Port 22、保留 PasswordAuthentication yes, 把 PermitRootLogin no 加上试一次。

验证无误后,分两步发:

第一步:关密码、保留端口 22、保留 2222。

第二步:把 22 改 2222。

中间任何一步都保留应急的 console / 带外管理通道。

我们生产里禁止直接全量执行 SSH 加固 SLS,必须走 canary 批次。

6.3.4 回滚

 

# 在被改的机器上,把 sshd_config 还原
cp /etc/ssh/sshd_config.rpmsave /etc/ssh/sshd_config
systemctl reload sshd

 

Salt 在改配置前会自动备份原文件到 .rpmsave。 如果是新机器,没有原文件,则必须保留一份手动的应急 SSH 配置。

6.3.5 风险提醒

任何时候都不要在 SLS 里写 PermitRootLogin yes + PasswordAuthentication yes。

多端口(Port 22 + Port 2222)下,新配置要把老端口显式删掉,否则 salt-minion 重启 sshd 不会失败,但实际监听会多。

用堡垒机的环境,加固时把堡垒机 IP 加入 AllowUsers,否则堡垒机自己也连不上。

6.4 防火墙规则批量下发

6.4.1 背景

公司 8 个机房,每个机房有自己的网络策略。 手动写 iptables 容易写错规则导致 SSH 不通,严重的要进机房连显示器。

6.4.2 Pillar

 

# /srv/pillar/firewall/init.sls
firewall:
default_policy:drop
allow_ssh_from:
    -10.20.0.0/16
    -10.30.0.0/16
allow_db_from:
    -10.20.10.0/24
custom_rules:
    -comment:"Allow DNS"
      dport:53
      proto:udp
    -comment:"Allow NTP"
      dport:123
      proto:udp

 

6.4.3 State(iptables)

 

# /srv/salt/firewall/iptables.sls
{%ifgrains['os_family']=='RedHat'%}

iptables_installed:
pkg.installed:
    -name:iptables-services
service.dead:
    -name:firewalld
    -enable:False
service.running:
    -name:iptables
    -enable:True

/etc/sysconfig/iptables:
file.managed:
    -source:salt://firewall/files/iptables.rules
    -template:jinja
    -user:root
    -group:root
    -mode:'0600'
    -watch_in:
      -service:iptables

{%elifgrains['os_family']=='Debian'%}

iptables_installed:
pkg.installed:
    -name:iptables-persistent
service.running:
    -name:netfilter-persistent
    -enable:True

/etc/iptables/rules.v4:
file.managed:
    -source:salt://firewall/files/iptables.rules
    -template:jinja
    -mode:'0640'
    -watch_in:
      -service:netfilter-persistent

{%endif%}

 

/srv/salt/firewall/files/iptables.rules 模板片段:

 

*filter
:INPUT {{ pillar['firewall']['default_policy'] | upper }} [0:0]
:FORWARD {{ pillar['firewall']['default_policy'] | upper }} [0:0]
:OUTPUT ACCEPT [0:0]

-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p icmp -j ACCEPT

{% for src in pillar.get('firewall:allow_ssh_from', []) %}
-A INPUT -s {{ src }} -p tcp --dport 22 -j ACCEPT
{% endfor %}

{% for rule in pillar.get('firewall:custom_rules', []) %}
-A INPUT -p {{ rule.proto }} --dport {{ rule.dport }} -j ACCEPT -m comment --comment "{{ rule.comment }}"
{% endfor %}

COMMIT

 

6.4.4 灰度

防火墙改完绝对不能覆盖。我们生产做法:

第一次下发:往 chain 末尾追加新规则,保留原规则。

test=True 验证配置文件合法。

真实应用,验证从堡垒机能连 SSH、ping 通。

第二天再看没报警,再让 playbook 跑全量。

6.4.5 回滚

 

# /etc/sysconfig/iptables.rpmsave 是改之前备份的
iptables-restore < /etc/sysconfig/iptables.rpmsave
systemctl restart iptables

 

或者手动用 iptables -F 临时清空(生产环境慎用)。

6.4.6 风险提醒

改防火墙前必须确认从堡垒机或带外管理能连到机器。 一次我们改了 SSH 端口 + 防火墙,碰巧那天堡垒机 IP 段调整,运维全部被锁在外面。

默认 policy 一定是 ACCEPT 或 DROP,不要 DROP 后没加 ESTABLISHED,RELATED, 否则 sshd 已经建立的连接也会被丢。

用 firewalld 还是 iptables 取决于 OS,不要在 Rocky 8 上写 iptables-only SLS, firewalld 没 disable 的话会冲突。

6.5 MySQL 主从配置管理

6.5.1 背景

我们有 50 多套 MySQL 主从,全部 5.7 / 8.0 混跑。 手写 my.cnf 容易写错 server_id 重号。 用 SLS 把 my.cnf 模板化,server_id 从 minion_id 派生。

6.5.2 Pillar

 

# /srv/pillar/mysql/init.sls
mysql:
version:8.0.36
port:3306
data_dir:/data/mysql
log_dir:/var/log/mysql
binlog_format:ROW
innodb_buffer_pool_size:"{{ (grains['mem_total'] * 0.6) | int }}M"
max_connections:1000
replication_user:repl
# /srv/pillar/mysql/master.sls
mysql:
role:master
server_id:"10{{ grains['id'][-3:]|int }}"
read_only:0
log_bin:mysql-bin
log_slave_updates:1
gtid_mode:1
enforce_gtid_consistency:1
# /srv/pillar/mysql/slave.sls
mysql:
role:slave
server_id:"10{{ grains['id'][-3:]|int }}"
read_only:1
log_bin:mysql-bin
log_slave_updates:0
gtid_mode:1
enforce_gtid_consistency:1

 

6.5.3 State

 

# /srv/salt/mysql/init.sls
mysql-pkg:
pkg.installed:
    -name:mysql-community-server
    -version:{{pillar['mysql']['version']}}

mysql-conf-dir:
file.directory:
    -name:/etc/my.cnf.d
    -makedirs:True

mysql-config:
file.managed:
    -name:/etc/my.cnf
    -source:salt://mysql/files/my.cnf
    -template:jinja
    -user:root
    -group:root
    -mode:'0644'
    -require:
      -pkg:mysql-pkg
    -watch_in:
      -service:mysql

mysql-data-dir:
file.directory:
    -name:{{pillar['mysql']['data_dir']}}
    -user:mysql
    -group:mysql
    -mode:'0750'
    -makedirs:True

mysql-service:
service.running:
    -name:mysqld
    -enable:True
    -watch:
      -file:mysql-config

 

/srv/salt/mysql/files/my.cnf:

 

[mysqld]
user = mysql
port = {{ pillar['mysql']['port'] }}
datadir = {{ pillar['mysql']['data_dir'] }}
socket = /var/lib/mysql/mysql.sock
log-error = {{ pillar['mysql']['log_dir'] }}/mysqld.log
pid-file = /var/run/mysqld/mysqld.pid
server_id = {{ pillar['mysql']['server_id'] }}
log_bin = {{ pillar['mysql']['log_bin'] }}
binlog_format = {{ pillar['mysql']['binlog_format'] }}
log_slave_updates = {{ pillar['mysql']['log_slave_updates'] }}
read_only = {{ pillar['mysql']['read_only'] }}
gtid_mode = {{ pillar['mysql']['gtid_mode'] }}
enforce_gtid_consistency = {{ pillar['mysql']['enforce_gtid_consistency'] }}
innodb_buffer_pool_size = {{ pillar['mysql']['innodb_buffer_pool_size'] }}
max_connections = {{ pillar['mysql']['max_connections'] }}

 

6.5.4 灰度

 

# 配置更新是平滑的(reload),但改 server_id 需要重启
salt -G 'role:mysql' state.sls mysql test=True
salt -G 'env:canary and role:mysql' state.sls mysql
salt -G 'env:prod and role:mysql' state.sls mysql --batch-size=20%

 

注意:server_id 改动必须重启 MySQL。 线上不能全量同时重启,要用 orchestrate 编排滚动重启。 这部分逻辑一般写在 orchestrate/mysql_rolling_restart.sls 里。

6.5.5 回滚

 

# 还原 my.cnf
cp /etc/my.cnf.rpmsave /etc/my.cnf
systemctl reload mysqld
# 如果 server_id 变了导致主从关系混乱,需要重新 change master

 

6.5.6 风险提醒

改 read_only 在 slave 上误改成 0,会导致从库接收写请求, 复制中断。需要用 unless 守卫。

改 innodb_buffer_pool_size 不重启不会生效。

改 binlog_format 在线不生效,要等新连接才生效。

永远不要在 SLS 里写 MySQL 不支持的参数,比如 CREATE INDEX ... INCLUDE (...)、 8.0 不支持 query_cache_type(5.7 有,8.0 已删除)。

6.6 批量用户与 sudo 管理

6.6.1 背景

运维、研发、dba 各自有自己的系统账号,权限和 sudo 配置要统一管控。

6.6.2 State

 

# /srv/salt/users/init.sls
{%foruserinpillar.get('users',[])%}
{{user.name}}:
user.present:
    -name:{{user.name}}
    -uid:{{user.uid}}
    -gid:2000
    -home:/home/{{user.name}}
    -shell:/bin/bash
    -groups:
      -{{user.name}}
      {%ifuser.sudo|default(False)%}
      -wheel
      {%endif%}
    -require:
      -group:{{user.name}}

group-{{user.name}}:
group.present:
    -name:{{user.name}}
    -gid:2000

{%ifuser.sudo|default(False)%}
/etc/sudoers.d/{{user.name}}:
file.managed:
    -name:/etc/sudoers.d/{{user.name}}
    -source:salt://users/files/sudoers.tmpl
    -template:jinja
    -user:root
    -group:root
    -mode:'0440'
    -defaults:
        user:{{user}}
{%endif%}
{%endfor%}

 

6.6.3 灰度

新增用户不涉及存量用户,可以一次性下发。 删除用户必须先确认 user.present 转 user.absent,并保留一段时间的"禁用"状态。

6.6.4 回滚

 

# 删除用户
salt '*' user.absent name=opsuser1 remove=True force=True
# 还原 sudoers
salt '*' file.managed /etc/sudoers.d/opsuser1 source=salt://users/files/opsuser1.sudoers

 

6.6.5 风险提醒

user.absent 加 remove=True 会删 home 目录,生产里不要用。 改成 user.absent name=opsuser1,保留 home 目录作为历史。

改 /etc/sudoers 写错语法会导致所有用户无法 sudo。 Salt 提供了 visudo 验证钩子,写在 file.managed 之前:

 

/etc/sudoers.d/opsuser1:
  file.managed:
    - ...
    - check_cmd: /usr/sbin/visudo -c -f

 

永远不要把用户密码写在 pillar 里。用 user.present 配合hash_password: True + password: '...' 但 hash 由 ssh key 体系替代。

6.7 定时任务分发

6.7.1 背景

每个业务都有自己的清理脚本、监控脚本,定期跑。 这些 cron 任务集中管理,不再允许开发直接登机器写 crontab。

6.7.2 State

 

# /srv/salt/crons/init.sls
{%forjobinpillar.get('crons',[])%}
cron-{{job.name}}:
cron.present:
    -name:{{job.name}}
    -user:{{job.user|default('root')}}
    -minute:{{job.minute|default('*')}}
    -hour:{{job.hour|default('*')}}
    -daymonth:{{job.daymonth|default('*')}}
    -dayweek:{{job.dayweek|default('*')}}
    -month:{{job.month|default('*')}}
    -command:{{job.command}}
{%endfor%}

 

pillar 示例:

 

crons:
  -name:clean-old-logs
    user:root
    minute:0
    hour:3
    command:/usr/local/bin/clean_logs.sh>/var/log/clean.log2>&1
-name:report-disk
    user:deployer
    minute:'*/30'
    command:/home/deployer/bin/disk_report.sh

 

6.7.3 灰度

cron 任务不会立即执行(等下一个时间点),但配置错会持续失败。 我们要求 test=True 看 diff,全量发。

6.7.4 回滚

 

salt '*' cron.absent name=clean-old-logs user=root

 

6.7.5 风险提醒

cron 任务如果执行时间长且频率高,可能出现"上次还没跑完下次又启动"的并发问题。 脚本里加 flock:

 

/usr/local/bin/clean_logs.sh:
* * * * * /usr/bin/flock -n /tmp/clean.lock /usr/local/bin/clean_logs.sh

 

cron.present 默认会用 MAILTO=root,要发不了邮件会撑爆 spool。 配 email: false 关闭。

6.8 日志切割与归档

6.8.1 背景

业务日志散在 /var/log/,磁盘总被撑爆,logrotate 配置文件散落各处。 统一用 SLS 收敛。

6.8.2 State

 

# /srv/salt/logrotate/init.sls
logrotate:
pkg.installed:
    -name:logrotate

{%forappinpillar.get('logrotate_apps',[])%}
/etc/logrotate.d/{{app.name}}:
file.managed:
    -name:/etc/logrotate.d/{{app.name}}
    -source:salt://logrotate/files/app.tmpl
    -template:jinja
    -user:root
    -group:root
    -mode:'0644'
    -defaults:
        app:{{app}}
{%endfor%}

 

pillar 示例:

 

logrotate_apps:
  -name:nginx
    paths:
      -/var/log/nginx/*.log
    rotate:14
    size:100M
    compress:True
    create:'0640 nginx nginx'
    postrotate:'nginx -s reload > /dev/null 2>&1 || true'
-name:my-app
    paths:
      -/var/log/my-app/*.log
    rotate:7
    daily:True
    missingok:True
    sharedscripts:True
    postrotate:'systemctl reload my-app'

 

6.8.3 灰度

test=True 验证模板渲染没问题即可全量发。 logrotate 自己的执行是 logrotate -d 干跑,不会真的转。

6.8.4 回滚

file.absent 删除不需要的配置,重新跑 SLS 恢复。

6.8.5 风险提醒

nginx 的 postrotate 必须用 nginx -s reload,不要 kill -HUP(PID 文件可能错)。

多个 file 共享 postrotate 要 sharedscripts: True,否则每个 file 都跑一次 reload。

size 和 daily 同时存在时,logrotate 会按"哪个先到"判断。

6.9 监控客户端批量接入

6.9.1 背景

Prometheus 体系下,node_exporter 装在每台机器上,端口、用户、版本必须统一。

6.9.2 Pillar

 

# /srv/pillar/node_exporter/init.sls
node_exporter:
version:1.7.0
port:9100
user:node_exporter
textfile_dir:/var/lib/node_exporter
enabled_collectors:
    -cpu
    -meminfo
    -diskstats
    -netdev
    -filesystem
    -loadavg
    -systemd
    -process
    -ntp
bind:'0.0.0.0'
allow_cidrs:
    -10.20.0.0/16

 

6.9.3 State

 

# /srv/salt/node_exporter/init.sls
node-exporter-user:
user.present:
    -name:{{pillar['node_exporter']['user']}}
    -shell:/sbin/nologin
    -system:True

node-exporter-dirs:
file.directory:
    -name:/opt/node_exporter
    -user:{{pillar['node_exporter']['user']}}
    -group:{{pillar['node_exporter']['user']}}
    -mode:'0755'
file.directory:
    -name:/var/lib/node_exporter
    -user:{{pillar['node_exporter']['user']}}
    -group:{{pillar['node_exporter']['user']}}
    -mode:'0755'
    -makedirs:True

node-exporter-bin:
file.managed:
    -name:/opt/node_exporter/node_exporter
    -source:https://github.com/prometheus/node_exporter/releases/download/v{{pillar['node_exporter']['version']}}/node_exporter-{{pillar['node_exporter']['version']}}.linux-amd64.tar.gz
    -source_hash:https://github.com/prometheus/node_exporter/releases/download/v{{pillar['node_exporter']['version']}}/sha256sums.txt
    -skip_verify:True
    -mode:'0755'
    -user:root
    -group:root

# 注意:上面这种方式在生产里并不好——Salt 下载外网受网络限制
# 实际生产里我们会把 node_exporter 二进制放到内部 mirror(比如自建 minio)
# 然后用 salt://node_exporter/files/node_exporter 走 file.managed

node-exporter-systemd:
file.managed:
    -name:/etc/systemd/system/node_exporter.service
    -source:salt://node_exporter/files/node_exporter.service
    -template:jinja
    -user:root
    -group:root
    -mode:'0644'
    -watch_in:
      -service:node_exporter

node-exporter-firewall:
iptables.insert:
    -position:1
    -table:filter
    -chain:INPUT
    -jump:ACCEPT
    -source:{{pillar['node_exporter']['allow_cidrs'][0]}}
    -dport:{{pillar['node_exporter']['port']}}
    -proto:tcp
    -save:True
service.running:
    -name:node_exporter
    -enable:True
    -watch:
      -file:node-exporter-systemd

 

/srv/salt/node_exporter/files/node_exporter.service:

 

[Unit]
Description=Node Exporter
After=network.target

[Service]
User={{ pillar['node_exporter']['user'] }}
ExecStart=/opt/node_exporter/node_exporter 
  --web.listen-address={{ pillar['node_exporter']['bind'] }}:{{ pillar['node_exporter']['port'] }} 
  --collector.textfile.directory={{ pillar['node_exporter']['textfile_dir'] }} 
  --collectors.enabled={{ pillar['node_exporter']['enabled_collectors'] | join(',') }}
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

 

6.9.4 灰度

 

salt -G 'env:canary' state.sls node_exporter
salt -G 'env:prod' state.sls node_exporter --batch-size=10%

 

6.9.5 回滚

 

salt -G 'role:web' state.single service.dead name=node_exporter
salt -G 'role:web' state.single file.absent name=/etc/systemd/system/node_exporter.service

 

6.9.6 风险提醒

node_exporter 默认监听 9100,只对内网开放。 错误地监听 0.0.0.0 + 没防火墙,可能被外网收集所有 metrics。

启用的 collector 不要全开。systemd、process 之类的开销大, 按需开启。

version 不要用 latest,写死版本号,文件下载走内部 mirror。

6.10 salt-api 与 Web 平台对接

6.10.1 背景

SLS 文件用 Git 管理,CI 平台推完代码后要触发 Salt 执行; 同时运营 / DBA 同学偶尔需要重启某个服务,希望走 Web 平台而不是 SSH。

6.10.2 安装

 

yum install -y salt-api

 

6.10.3 配置

/etc/salt/master 加段:

 

external_auth:
  pam:
    opsuser1:
      -.*
      -'@runner'
      -'@wheel'
      -'@jobs'
    dbauser1:
      -'web*':
        -state.sls
        -state.highstate
        -cmd.run
        -service.*

rest_cherrypy:
port:8000
host:0.0.0.0
ssl_crt:/etc/pki/tls/certs/salt-api.crt
ssl_key:/etc/pki/tls/private/salt-api.key
disable_ssl:False

 

@jobs 允许列历史作业,@runner 允许 salt-run,@wheel 允许 master 端 key 管理。

6.10.4 启动

 

systemctl enable salt-api
systemctl start salt-api

 

6.10.5 调用示例

登录拿 token:

 

curl -k https://salt-api.example.com:8000/login 
  -H "Accept: application/json" 
  -d username=opsuser1 
  -d password=opsuser1_pwd 
  -d eauth=pam

 

返回:

 

{
  "return": [
    {
      "token": "a1b2c3d4...",
      "start": 1640995200.0,
      "expire": 1641038400.0,
      "user": "opsuser1",
      "perms": [".*", "@runner", "@wheel", "@jobs"],
      "eauth": "pam"
    }
  ]
}

 

带 token 跑命令:

 

curl -k https://salt-api.example.com:8000 
  -H "Accept: application/json" 
  -H "X-Auth-Token: a1b2c3d4..." 
  -d client=local 
  -d tgt='role:nginx and env:prod' 
  -d fun=state.sls 
  -d arg='nginx' 
  -d kwarg='{"test": true}'

 

6.10.6 灰度

API 调用本身就是异步的。 CI 平台推 SLS 时,先发 canary 组,再发 prod 组,通过编排:

 

# /srv/salt/orchestrate/release_v1.sls
canary:
salt.state:
    -tgt:'G@env:canary and G@role:web'
    -sls:
      -web.deploy
    -failhard:True
    -batch:'10%'

prod:
salt.state:
    -tgt:'G@env:prod and G@role:web'
    -sls:
      -web.deploy
    -require:
      -salt:canary
    -failhard:True
    -batch:'20%'

 

6.10.7 风险提醒

external_auth 一定要按"用户 + 目标 + 模块"最小化授权。 一次我们给新来的实习生配 .* + *,导致能跑任意 SLS,把生产 nginx 配置全删了。

rest_cherrypy 一定要开 SSL,否则 token 会以明文走。

API 操作的 audit 要开 audit returner,所有调用的 fun、tgt、arg 入库。

密码不要写死在调用里,CI 用 vault 注入。

七、高可用与扩展

SaltStack 的 HA 是个大坑。社区对"多 master 怎么做"有过很多讨论,但生产里要看你 到底要解决什么问题。我们这里讲两种我们实际跑过的方案。

7.1 单 master 的单点风险

默认部署下,master 是一个进程:salt-master。 它的关键工作:

监听 4505 / 4506。

维护 Pillar / file_roots。

维护 minion 密钥列表。

维护 jobs cache。

维护 event bus。

任一项挂掉,集群就开始"看起来还活着但啥都干不了"。 常见的故障:

master OOM。 minion 数量超过 1 万后,几千并发作业时 ZeroMQ 消息全堆在内存。

master 磁盘满。 returner 写 MySQL 失败,event 继续堆在本地。

master 主机故障。 物理机宕机、虚拟化热迁移失败。

网络分区。 机房之间断网,syndic 链路失效。

7.2 多 master failover 方案

我们生产用的是双 master 方案,minion 端配置:

 

# /etc/salt/minion
master:
-10.20.0.10
-10.20.0.11
master_type:failover
master_shuffle:True
master_alive_interval:30
master_tries:3

 

master 写成列表,minion 按顺序尝试。

master_type: failover 表示主备,主挂了就切备。

master_shuffle: True 启动时随机选一个 master,可以分散 master 压力。

master_alive_interval: 30 每 30 秒检测一次主 master 是否活着。

master_tries: 3 失败重试次数。

关键:两个 master 的 file_roots / pillar_roots / ext_pillar 必须保持一致。 我们用 Git 仓库 + rsync:

 

# /usr/local/bin/salt-sync.sh
#!/bin/bash
# master-A 上
rsync -az --delete /srv/salt/ master-B:/srv/salt/
rsync -az --delete /srv/pillar/ master-B:/srv/pillar/
rsync -az /etc/salt/master master-B:/etc/salt/master
ssh master-B 'systemctl reload salt-master'

 

每 5 分钟同步一次。生产里可以走 keepalived + DRBD,但更复杂的方案稳定性也未必好。

7.3 master 端调优

master 端几个关键参数。生产里 5000 minion 规模我们调过一轮:

 

# /etc/salt/master
worker_threads:20
pub_hwm:1000
zmq_backlog:10000
tcp_keepalive:True
tcp_keepalive_idle:300
tcp_keepalive_cnt:3
tcp_keepalive_intvl:60
event_match_type:startswith

 

worker_threads:处理作业的工作线程。默认 5,几千并发要调到 20–30。 上限取决于 CPU。

pub_hwm:ZeroMQ 发送队列高水位线。超过会丢弃。 调大让突发流量有缓冲。

zmq_backlog:监听 socket 的 backlog,调大避免握手 RST。

tcp_keepalive:开 TCP keepalive,minion 死链能被快速发现。

event_match_type:事件匹配模式。startswith 比 pcre 快很多。 reactor 里如果用 pcre,master 端 CPU 会涨。

7.4 jobs 持久化

默认 job cache 在 master 的 /var/cache/salt/master/jobs 目录的 local 库里。 master 重启后,老的 jid 还能查到,但新的作业可能因为 return 没到就丢。

我们用 MySQL returner 持久化:

 

master_job_cache: mysql
mysql.host: 'salt-meta.example.com'
mysql.user: 'salt'
mysql.pass: '{{ salt_pillar['mysql_pass'] }}'
mysql.db: 'salt'

 

jids 表存 jid + 完整请求载荷,salt_returns 表存每次执行结果。 作业完成后,事件流也写到 salt_events 表。

通过 salt-run jobs.list_jobs 查历史:

 

salt-run jobs.list_jobs
salt-run jobs.list_jobs search_function='state.sls'
salt-run jobs.list_jobs search_target='role:nginx'

 

风险提醒:MySQL returner 写放大明显。 5000 minion 一次并发作业,salt_returns 表可能一次写 5000 行。 要给表加合适的索引,定期归档(按月)。

7.5 状态爆炸

master 跑一年,/srv/salt 下的 SLS 越来越多,几十 MB 是常事。 编译一次 highstate 越来越慢,minion 端要 5–10 秒才返回。

解决办法:

按业务拆分 pillar。不要把所有数据写在一个 init.sls。

用 ext_pillar 从 CMDB 取数据。ext_pillar: [my_cmbd],从内部配置中心拉。

用 master_tops 替代 top.sls。基于业务标签动态生成匹配。

拆分 master。按业务线分多套 master,独立 SLS 仓库。

我们最终选的是 1 + 2:pillar 拆细,用 ext_pillar 从 CMDB 拉机器标签。 SLS 文件不再做"角色 × 环境"的全量组合,而是按角色写一套,环境变量从 pillar 注入。

7.6 大规模场景的拆分策略

5000 minion 以下,单 master 双机 failover 撑得住。 5000–20000 建议拆业务线:

master-A:基础平台(DB、MQ、缓存)。

master-B:业务应用(Web、API、Worker)。

master-C:大数据(Hadoop、Flink、ES)。

minion 端 master 字段配对应的 master IP,或者用 DNS round-robin。

20000+ 建议引入 syndic:

 

顶级 master
  ├── syndic-A(区域 1)
  │     ├── minion × 5000
  └── syndic-B(区域 2)
        ├── minion × 5000

 

syndic 自己既是 master 又是 minion,会做协议转换。 调试难度大,我们在 8000 minion 时没用 syndic,靠多 master failover 顶住了。

八、与 CI/CD、配置中心、监控联动

SaltStack 单独用只能解决"配置管理",要真正"自动化运维",必须接 CI / 监控 / 审计。

8.1 SLS 文件进 Git

我们用 GitLab 管理所有 SLS、Pillar、master 配置:

 

salt-config/
  master/
  minion/
  salt/
    common/
    nginx/
    mysql/
    redis/
    k8s/
    ...
  pillar/
    base/
    nginx/
    mysql/
    ...
  top.sls
  README.md

 

master 的 /etc/salt/master 不直接进仓库(包含敏感信息), 但所有 master 配置相关的"软链接"指向仓库:

 

ln -sf /opt/salt-config/master/master.conf /etc/salt/master
ln -sf /opt/salt-config/master/file_roots /srv/salt
ln -sf /opt/salt-config/master/pillar_roots /srv/pillar

 

这样 master 重启后所有配置从仓库恢复。

8.2 CI/CD 触发

GitLab CI 流水线:

 

# .gitlab-ci.yml
stages:
-test
-deploy

lint:
stage:test
image:salt-lint:latest
script:
    -findsaltpillar-name'*.sls'-execsalt-lint{};
    -yamllint-d'{extends: default, rules: {line-length: disable}}'salt/pillar/

dry-run:
stage:test
script:
    -salt-call--localstate.slsnginxtest=True
only:
    -merge_requests

deploy-canary:
stage:deploy
script:
    -salt-api-callweb-clusterstate.slsnginx
environment:
    name:canary
only:
    -main
when:manual

 

deploy-canary 阶段是手动确认,运营同学在 GitLab UI 点头才执行。 执行走 salt-api 的 RESTful 接口,不直接 SSH 到 master。

8.3 接入 ELK

salt returner 接外部系统的标准做法在 Salt 3000+ 以后变窄了: 官方维护的常用 returner 是 mysql、postgres、local、redis、sentry、slack、mattermost,kafka returner 在 Salt 3000+ 不再随主仓库发布。 我们生产里的做法是:先用 mysql returner 落库, 再通过 Canal / Debezium 把 MySQL 的 salt_returns 表 binlog 订阅到 Kafka, 最终走 ELK 链路。

Kibana 上查"过去 24 小时所有失败的 state":

 

tag: "salt/job/*/ret/*" AND data.success: false

 

8.4 监控 master 自身

Prometheus 抓取 master 自身指标有几种方式:

salt-master 自带 salt-master 的内嵌 prometheus exporter(3004+ 部分支持)。

写自定义 salt-run 脚本,输出 Prometheus 文本格式,挂 sidecar。

直接用 node_exporter 抓 salt-master 进程的 CPU / 内存 / FD。

我们生产用的是方案 3 + 方案 2 结合:

 

#!/usr/bin/env python3
# /usr/local/bin/salt_exporter.py
from prometheus_client import start_http_server, Gauge
import subprocess, time, json

MINION_UP = Gauge('salt_minion_up', '1 if minion is up', ['minion_id'])
JOBS_ACTIVE = Gauge('salt_jobs_active', 'Active jobs count')
JOBS_FAILED = Gauge('salt_jobs_failed', 'Failed jobs in last 1h')

def collect():
    out = subprocess.run(['salt-run', 'manage.status', '--out=json'],
                         capture_output=True, text=True)
    status = json.loads(out.stdout)
    up = status.get('return', {}).get('up', [])
    down = status.get('return', {}).get('down', [])
    for m in up:
        MINION_UP.labels(minion_id=m).set(1)
    for m in down:
        MINION_UP.labels(minion_id=m).set(0)

if __name__ == '__main__':
    start_http_server(9101)
    whileTrue:
        collect()
        time.sleep(30)

 

Grafana 告警规则:

 

groups:
  -name:salt
    rules:
      -alert:SaltMinionDown
        expr:salt_minion_up==0
        for:5m
        labels:
          severity:warning
        annotations:
          summary:"Minion {{ $labels.minion_id }} down"
      -alert:SaltMinionDown
        expr:salt_minion_up==0
        for:30m
        labels:
          severity:critical

 

8.5 审计

所有执行走 salt-api 留痕,MySQL audit returner 记录:

 

# /etc/salt/master
# audit 通过 audit returner 实现,下面是一种典型实现
# 推荐做法:自定义 returner 写到 ES / MySQL,把每次 fun、tgt、arg、metadata 入库

 

8.5.1 自定义 audit returner 思路

在 master 端的 /srv/salt/_returners/ 目录放一个自定义 returner。 Salt 会在 salt-master 启动时自动 sync 这些 returner。 最简示例,把每次调用写到 MySQL audit_log 表:

 

# /srv/salt/_returners/audit_mysql.py
import json
import time
import logging
import salt.returners

log = logging.getLogger(__name__)

def __virtual__():
    return'audit_mysql'

def returner(ret):
    '''
    ret 是 dict,包含 fun、jid、id、return、success、fun_args 等字段
    '''
    try:
        import MySQLdb
        conn = MySQLdb.connect(
            host='salt-meta.example.com',
            user='salt',
            passwd='{{ pillar['mysql_pass'] }}',
            db='salt',
            port=3306
        )
        cur = conn.cursor()
        cur.execute("""INSERT INTO audit_log
            (fun, jid, minion_id, success, fun_args, return_data, alter_time)
            VALUES (%s, %s, %s, %s, %s, %s, NOW())""",
            (
                ret.get('fun'),
                ret.get('jid'),
                ret.get('id'),
                ret.get('success'),
                json.dumps(ret.get('fun_args', [])),
                json.dumps(ret.get('return', ''))
            ))
        conn.commit()
        cur.close()
        conn.close()
    except Exception as e:
        log.error('audit_mysql returner failed: %s', e)

 

注意几点:

__virtual__ 返回名字是 returner 的标识符,调用时用 --return audit_mysql。

不要把 MySQL 密码硬编码在 returner 里,从 __opts__['pillar'] 拿。

真正生产里这个 returner 还应该带 fun_args、user、external_auth 来源信息, 方便事后追溯。

更进一步可以加一个异步队列(Redis List),让 returner 立刻 enqueue 之后返回, 避免主作业流程被审计写阻塞。

 

CREATE TABLE audit_log (
idBIGINTNOTNULL AUTO_INCREMENT PRIMARY KEY,
  fun VARCHAR(64) NOTNULL,
  jid VARCHAR(20) NOTNULL,
  minion_id VARCHAR(255) NOTNULL,
successVARCHAR(20) NOTNULL,
  fun_args MEDIUMTEXT,
  return_data LONGTEXT,
  alter_time TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMP,
KEY fun (fun),
KEY jid (jid),
KEY minion_id (minion_id)
) ENGINE=InnoDB;

 

使用时:

 

salt -G 'role:nginx' cmd.run 'uptime' --return audit_mysql

 

salt-api 的请求会带上 metadata,returner 把 metadata 也写到 audit_log, 事后追溯非常方便。

8.5.2 二次审批

仅审计还不够,关键操作要二次审批。 CI 平台调 salt-api 跑生产 state 之前,要走工单系统确认。 审核人通过后 CI 才会带上"已审批"的 metadata 调 salt-api。 未带 metadata 或 metadata 不全的请求,salt-api 端拦下不执行。

 

curl -k https://salt-api:8000 
  -H "X-Auth-Token: $TOKEN" 
  -d client=local 
  -d tgt='role:nginx' 
  -d fun=state.sls 
  -d arg='nginx' 
  -d metadata='{"ticket": "INC-20260506-001", "operator": "opsuser1"}'

 

这些 metadata 会被 returner 一起落到审计库,事后追溯非常方便。

九、故障复盘案例

下面四个案例都是我们生产里真实遇到过的(去敏感化),按"现象→判断→检查→定位→修复→验证→复盘"展开。

9.1 案例一:minion 集体断连

9.1.1 现象

监控告警:salt_minion_up == 0 的机器数从 5 台突然涨到 80 台。 告警系统本身用 salt '*' test.ping 做心跳,发现大量 false。 Prometheus 平台 UI 上看 30 分钟内 minion 离线率从 0.5% 涨到 13%。

9.1.2 初步判断

不是单台机器问题,是 master 端的问题。 可能性:master 进程 OOM、被 K8s 调度、磁盘满、网络抖动。

9.1.3 命令检查

 

# master 端
ps aux | grep salt-master
free -h
df -h /var/log /var/cache
# 看 master 日志
tail -f /var/log/salt/master
# 看连接
ss -tan | grep -E '4505|4506' | wc -l
# 看 minion 在线
salt-run manage.status
salt-run manage.up
salt-run manage.down

 

我们这次看到的:

salt-master 进程还在。

free -h 显示 available 内存 0。

/var/log/salt/master 最后几行是 Out of memory: Killed process 12345 (salt-master)。

salt-run manage.status 输出 minion 都 down。

9.1.4 关键指标

master 内存:32GB,常驻 28GB。

minion 数量:~4000。

作业并发:最近 1 小时有一个"全网 highstate"的 batch,触发了几千并发作业。

returner:MySQL 在另一个机房,跨机房延迟 30ms,每次写放大。

9.1.5 根因定位

worker_threads=5 配的太低,几千并发作业排队; returner 跨机房写 MySQL,每次作业要等 30ms 往返; master 端作业结果在内存里堆积,直到 OOM 被 kill。 minion 端的作业已经下发,但 return 回来时 master 进程没了, 所以 minion 端表现是"作业没回执"。

9.1.6 修复方案

短期:

临时把 master 切到备机 B(failover 自动完成)。

备机 B 的 worker_threads 调到 20。

临时关闭高并发批量任务。

长期:

worker_threads: 20。

returner 改成本机 SQLite,等异步转发到 MySQL(写个 reactor 把结果转发)。

pub_hwm: 1000、zmq_backlog: 10000。

加 master 自身监控:内存使用率 > 80% 告警。

9.1.7 验证方式

 

# 模拟小批量
salt -G 'env:canary' state.highstate
# 看 master 内存
free -h
# 看连接
ss -tan | grep -E '4505|4506' | wc -l

 

观测 30 分钟,内存稳定在 16GB 左右,连接数稳定。

9.1.8 复盘总结

单 master 撑大集群必须配 worker_threads 和 returner,不能让作业堆在内存。

master 自身监控不能只监控进程存活,要看内存、连接数、worker queue。

failhard 加上能让大批量任务在出错时快速失败而不是把资源耗光。

9.2 案例二:state 执行半成功

9.2.1 现象

 

salt '*' state.highstate

 

输出形如:

 

Summary for web-prod-01
------------
Succeeded: 14
Failed:     2
------------
Total states run:    16
Total run time:     23.421 s

 

每次都恰好 2 个 state 失败,同一台机器上固定是 pkg.installed 的 nginx 和 php-fpm。 Succeeded 数量在不同机器上有差异,但失败的 ID 一致。

9.2.2 初步判断

不是环境差异(不是 OS、不是机器型号),是 pkg.installed 找不到包。 可能:包被删了、repo 错了、pkg cache 过期。

9.2.3 命令检查

 

# 单台机器 debug
salt 'web-prod-01' state.sls nginx -l debug --state-output=changes

 

输出关键行:

 

[ERROR ] Failed to install package nginx. Error: Error downloading packages:
  Cannot find a valid baseurl for repo: extra
[DEBUG ] Repository 'extra' is missing name in configuration

 

/etc/yum.repos.d/extra.repo 不存在,但 state 里写了要装这个 repo, 显然是某次手改 yum.repos.d 后这个文件被删了。

9.2.4 关键指标

看 state 文件 nginx/init.sls:

 

nginx-repo:
  pkgrepo.managed:
    - name: extra
    - baseurl: file:///opt/repo/extra
    - enabled: True
    - gpgcheck: False

 

看 minion 端 /etc/yum.repos.d/:

 

ls -la /etc/yum.repos.d/

 

实际只有 CentOS-Base.repo 和 epel.repo,没有 extra.repo。 state 里写了 pkgrepo.managed 想创建 extra.repo,但 baseurl 是 file:///opt/repo/extra, 本地根本没有这个目录(/opt/repo/extra 不存在),于是 pkgrepo.managed 失败, nginx 包就装不了。

9.2.5 根因定位

之前有人改了 nginx/init.sls,加了 pkgrepo.managed 块。

file:///opt/repo/extra 这个本地仓库路径在某些机器上有、在其他机器上没有。

state 加了 unless 守卫,但守卫写错(用 test -f /etc/yum.repos.d/extra.repo, 但 pkgrepo 创建前文件本来就不在,守卫永远 true,导致 pkgrepo 实际不被管理)。

9.2.6 修复方案

改 state 文件,正确使用 unless / onlyif。

在 pkgrepo.managed 块加 require 提前建目录。

灰度:

 

# 先在 1 台
salt -L 'web-canary-01' state.sls nginx test=True
# 再在 canary 组
salt -G 'env:canary' state.sls nginx
# 全量
salt -G 'role:nginx' state.sls nginx --batch-size=20%

 

9.2.7 验证方式

 

salt -G 'role:nginx' state.sls nginx test=True
# 看到所有 "Succeeded",没有 Failed

 

9.2.8 复盘总结

unless / onlyif 写错了不会报错,是 salt 执行成功的,但实际逻辑反了。

pkgrepo.managed 的 baseurl 必须是有效的,能用 curl/wget 验证。

SLS 改完先 lint(salt-lint),再 dry-run,再灰度,最后全量。

9.3 案例三:top.sls 拼写错误导致 prod 配置部分丢失

9.3.1 现象

某业务上新机器一批 50 台,Nginx 装上后能起,systemctl status nginx 显示 active (running)。 从浏览器访问业务域名,返回 502。 部分页面(首页、登录页)能访问,但业务页面统一 502。 已经跑过 state.highstate 一次,看起来是绿的。

9.3.2 初步判断

Nginx 进程在跑、配置语法过、监听正常,但 server_name 匹配不上或者 root 路径错。 看具体 nginx.conf 内容。

9.3.3 命令检查

 

salt 'web-prod-NN' cmd.run 'nginx -t'
# 输出:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
# (但访问业务域名 502)

salt 'web-prod-NN' cmd.run 'cat /etc/nginx/conf.d/example.com.conf'
# 输出:server { listen 80; server_name example.com; root ???; }
# root 字段渲染成空字符串

 

9.3.4 关键指标

模板文件用 jinja 渲染,jinja 拿 pillar nginx:port 替换 listen 端口。

看 pillar:

 

salt 'web-prod-NN' pillar.items nginx
# 输出(实际生产场景下):
# ----------
#     nginx:
#         ----------
#         port:
#             80
#         workers:
#             auto
#         vhosts:
#             None

 

nginx:port 和 nginx:workers 拿到了,vhosts 却是 None。 说明 base 命中的 nginx/init.sls 渲染成功,但 prod 环境预期合并的 vhosts 字段没进来。

9.3.5 根因定位

看 /srv/pillar/top.sls:

 

base:
  '*':
    -base
'role:nginx':
    -match:grain
    -nginx
'role:nginx and env:prod':
    -match:grain
    -nginx
    -nginx-prod

 

真正的根因:top.sls 把 prod 环境的 SLS 引用写成 nginx-prod(带连字符), 但仓库里实际文件叫 nginx.prod.sls(带点号)。 Salt 在解析 top.sls 时,找不到 nginx-prod 这个 SLS,只 log warning 不报错, prod 环境原本应该合并的 vhosts 字段就丢了。 结果是 nginx.init.sls 里 base 的 port/workers 仍然生效,但 vhosts 是 None。

top.sls 拼写错误是 Salt pillar 调试里最痛的问题: 解析不报错,运行时只丢一部分配置。

/srv/pillar/nginx/init.sls:

 

nginx:
  port:80
workers:auto
vhosts:
    -name:example.com
      root:/var/www/example.com
      php:True
    -name:static.example.com
      root:/var/www/static
      php:False

 

/srv/pillar/nginx.prod.sls:

 

nginx:
  vhosts:
    -name:example.com
      root:/var/www/example.com-prod
      php:True
    -name:static.example.com
      root:/var/www/static-prod
      php:False

 

prod 文件意图是覆盖 vhosts 字段(指到 prod 路径)。 但 prod 文件没有重写 port/workers,所以 Salt 的 smart merge 仍然拿到 base 的值。 这个合并行为本身是对的。

问题在于 top.sls 的 - nginx-prod 拼写错误,导致 prod 整份 SLS 没被加载。

9.3.6 修复方案

改 top.sls,把 nginx-prod 改回 nginx.prod。

用 pillar.show_top 验证匹配的 SLS 列表:

 

salt 'web-prod-NN' pillar.show_top
# 输出应包含 nginx.prod

 

改完后用 pillar.items 完整对账:

 

salt 'web-prod-NN' pillar.get nginx:vhosts
# 应该返回 prod 的 vhosts 列表

 

模板用 pillar.get 给默认值,避免 None 被 jinja 渲染成空串:

 

root {{ vhost.root|default('/var/www/' ~ vhost.name) }};

 

灰度重启 nginx:

 

salt -G 'env:canary' cmd.run 'systemctl reload nginx'

 

上线后加一个 CI 校验:解析 top.sls 后对每台 minion dump 应命中的 SLS 列表, prod 环境的 web 机器 pillar 里 nginx.vhosts 必须非空。

9.3.7 验证方式

 

# 1. 验证 top.sls 命中
salt 'web-prod-NN' pillar.show_top
# 输出应该包含 nginx.prod

# 2. 验证 pillar 数据
salt 'web-prod-NN' pillar.get nginx:port
salt 'web-prod-NN' pillar.get nginx:workers
salt 'web-prod-NN' pillar.get nginx:vhosts
# 全部非空

# 3. 验证渲染后配置
salt 'web-prod-NN' cmd.run 'cat /etc/nginx/conf.d/example.com.conf'
# root 字段有值,且是 prod 路径

# 4. 业务验证
curl -H "Host: example.com" http://web-prod-NN/
# 应该返回业务页面,不再是 502

 

9.3.8 复盘总结

top.sls 的 SLS 引用写错是 Salt pillar 调试最痛的坑。 解析不报错,只 log warning,运行时部分配置悄悄丢失。

pillar.items 输出"key 在但 value 是 None"是 Salt 的一个特点, 用 pillar.get 看具体值更准;pillar.show_top 看实际命中的 SLS。

state 模板里要 pillar.get('key', default),不要直接 pillar['key'], 避免 KeyError 之外还有"空字符串被填进去"的问题。

top.sls / pillar 改动必须走 PR review,不能直接 master 改。

加 CI 校验:所有 prod 环境的 pillar dump 后做 schema 校验,缺字段直接拦下。

pillar 数据"被合并"的逻辑不直观,最好用 pillar.show_top 验证。

9.4 案例四:salt-api 接口被误用为远程命令执行

9.4.1 现象

某天审计发现:salt-api 的 audit log 里有大量 cmd.run 调用, 执行用户是 intern1(实习生),执行机器是 prod-db-*。

数据库上有些 SHOW MASTER STATUS 之类的查询被跑过, 幸运的是没有改数据,但这是重大安全隐患。

9.4.2 初步判断

实习生账号权限过大。salt-api 的 external_auth 配置有问题。

9.4.3 命令检查

 

# 看 access log
grep 'intern1' /var/log/salt/api.log | tail -50
# 输出形如:
# [2024-09-12 1045] intern1 GET / - tgt='prod-db-*' fun=cmd.run arg=['ls /data']
# [2024-09-12 1001] intern1 GET / - tgt='prod-db-*' fun=cmd.run arg=['mysql -e "show processlist"']

# 看 external_auth 配置
cat /etc/salt/master | grep -A 30 'external_auth'

 

external_auth 配置(问题版本):

 

external_auth:
  pam:
    intern1:
      - .*
      - '@runner'
      - '@wheel'
      - '@jobs'

 

.* 匹配所有模块,@runner 允许跑 salt-run(可以删 minion key),@wheel 允许管理 master 配置。

9.4.4 关键指标

实习生账号创建是为了"查询监控数据"。

创建时运维同学图省事,给了 .*。

没有 target 限制,意味着 intern1 可以对所有 minion 跑任意命令。

9.4.5 根因定位

external_auth 写错了,不是"按用户最小化授权",而是"一把钥匙开所有门"。

实习生拿到 token 后能做的事超过范围。

审计发现得早,没造成实际数据损失。

9.4.6 修复方案

收回 intern1 的 token,重新签发。

改 external_auth:

 

external_auth:
  pam:
    intern1:
      -'role:web and env:canary':
        -cmd.run
        -test.ping
        -test.version
        -grains.items
    dbauser1:
      -'role:mysql':
        -state.sls
        -state.highstate
        -cmd.run
        -service.*
    opsuser1:
      -'*':
        -.*
        -'@runner'
        -'@wheel'
        -'@jobs'

 

强制 salt-api 走 SSL:

 

rest_cherrypy:
  port: 8000
  disable_ssl: False
  ssl_crt: /etc/pki/tls/certs/salt-api.crt
  ssl_key: /etc/pki/tls/private/salt-api.key

 

启用 audit returner,把所有调用入库:

 

# /etc/salt/master
audit_log_file: /var/log/salt/audit.log

 

加审计告警:调用 cmd.run、@wheel 立即通知 ops 群。

9.4.7 验证方式

 

# 实习生用 intern1 token 跑生产 DB 机器
curl -k https://salt-api:8000 
  -H "X-Auth-Token: $INTERN_TOKEN" 
  -d client=local 
  -d tgt='role:mysql and env:prod' 
  -d fun=cmd.run 
  -d arg='whoami'
# 应该返回 401

 

9.4.8 复盘总结

salt-api 的 external_auth 是生产里最敏感的配置。 一定按"用户 × target × module"三维最小化授权。

实习生、临时外包账号默认 test.ping + test.version + grains.items。

强审计:所有 cmd.run、@wheel 操作的 metadata 一定要有 operator、reason。

关闭非 SSL 访问,强制走 HTTPS。

十、风险提醒与回滚清单

下面这张表是我们把 SaltStack 用在生产三年沉淀的"事故预防清单"。

10.1 高危命令清单

命令 风险 防控
salt-key -D 删所有 minion 接受记录 加 audit,禁止无审计执行
salt-key -d 删单个 minion 强提示 + 二次确认
salt '*' cmd.run 'rm -rf ...' 误删文件 必须 pillar 化、dry-run 验证
salt '*' file.remove /etc/... 改 / 删关键文件 走 state + unless 守卫
salt '*' iptables.flush 清空防火墙 配 iptables.services + dry-run
salt '*' system.reboot 强制重启 加 test=True、at 调度
salt '*' mysql.query 'DROP DATABASE ...' 删库 完全禁止;备份恢复走专用工具
salt '*' pkg.remove 'kernel*' 卸载 kernel 加 -match 限定包名

10.2 灰度标准步骤

我们生产里所有 SLS 变更走统一灰度:

test=True 看 diff。

canary 1–2 台机器,5 分钟观察。

canary 完整组(约 5%),5 分钟观察。

10% 抽样(--batch-size=10%)。

30% 抽样。

50% 抽样。

100%。

每一步间隔 10 分钟,监控告警和业务指标正常才能继续。 出问题立刻停止,回到上一步。

10.3 回滚三板斧

备份原文件:Salt 自动备份 .rpmsave,确认存在。

保留旧 SLS:Git 仓库保留,必要时 git revert。

编排 onfail:

 

deploy_app:
  salt.state:
    -tgt:'role:web'
    -sls:
      -web.deploy
    -onfail:
      -salt:deploy_db

rollback_app:
salt.state:
    -tgt:'role:web'
    -sls:
      -web.rollback
    -require:
      -salt:deploy_app

 

10.4 变更窗口

工作日上午 10:00–12:00、下午 14:00–17:00。

业务高峰避开:电商类业务避开晚 8 点到 10 点。

大版本 Salt 升级、master HA 切换放在 0:00–5:00。

10.5 操作审计

所有通过 salt-api 的操作必须带 metadata:

 

curl -k https://salt-api:8000 
  -H "X-Auth-Token: $TOKEN" 
  -d client=local 
  -d tgt='role:nginx' 
  -d fun=state.sls 
  -d arg='nginx' 
  -d metadata='{"ticket": "INC-20260506-001", "operator": "opsuser1", "reason": "fix vhost port"}'

 

数据库 audit log 至少保留 90 天,半年导出冷归档。

10.6 灾备

master 配置、pillar、state 全部进 Git。

每台 master 上跑 salt-master 之前先 git pull 拉最新配置。

灾备演练:把 master A 关掉,确认 master B 接住,minion 端无感切换。

十一、总结

11.1 一句话

SaltStack 不是银弹,但在大规模同质机 + 配置中心化场景下, 仍然是"最稳的方案之一",比 Puppet 易学,比 Ansible 撑得住规模,比 Chef 接地气。 用好 SaltStack 的关键就三件事:

把 SLS 写得像代码——版本控制、PR review、CI 校验。

把 pillar 写得像数据——拆细、加密、按业务隔离。

把执行写得像发布——灰度、灰度、灰度。

11.2 演进方向

我们接下来想做的事:

Salt 3006+ 的 RAET 替代 ZeroMQ。 性能更好,但生产案例少,先观察。

salt-master 跑在 K8s 上。 master 是有状态服务,StatefulSet + PVC 跑起来,灾备变简单。

自研 Salt 调度平台。 现在的编排走 orchestrate + salt-api, 想做一个前端,把"提交 SLS → canary → 灰度 → 监控 → 回滚"全流程包起来。

接 GitOps。 SLS 仓库 push 触发 reconcile,ArgoCD / Flux 风格的声明式执行。

11.3 写给读者

如果你是第一次接触 SaltStack,建议按这个顺序读:

salt '*' test.ping 跑通,理解 minion-master 通信。

写一个最简单的 SLS:pkg.installed nginx。

加 file.managed 把 nginx.conf 模板化。

加 pillar,理解数据怎么从 master 推到 minion。

加 Grains,理解怎么按机器特征分类。

写 orchestrate,理解跨机器编排。

接 salt-api,理解怎么被外部系统调用。

接 MySQL returner,理解作业结果怎么持久化。

接 reactor,理解事件怎么触发自动化。

最后才是 HA、监控、灾备。

SaltStack 的概念多,但都是工程上绕不开的。 理解透了之后你再回头看 Ansible / Puppet / Chef,会发现他们做的事情本质上一样, 只是工程上的取舍不同。

祝你们的集群稳定,少 OOM,多下线。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分