1. 这不是又一个“配置管理工具入门”,而是你真正搞懂SaltStack的起点
如果你刚点开这篇内容,大概率正站在SaltStack的门口反复徘徊:文档里满屏的salt-master、salt-minion、pillar、grains、states、execution modules……像一堵密不透风的术语墙。你试过照着官方Quickstart跑通第一条salt '*' test.ping,但当需要把一台新服务器自动装好Nginx、配好SSL证书、再拉起一个Docker容器时,就卡在了“接下来该写什么?怎么组织?为什么这个state不生效?”——这根本不是操作问题,是底层概念没对齐。我带过十几支运维和SRE团队落地SaltStack,90%的新手掉坑,不是因为不会写YAML,而是从第一天起,就把salt-master当成“服务器”,把salt-minion当成“客户端”,把state当成“脚本”,这种类比在初期能跑通,但到中后期必然崩盘。SaltStack的本质是一套基于事件驱动的分布式远程执行与状态编排框架,它的所有术语都服务于这个核心定位。比如grains不是“主机信息快照”,而是Salt在minion启动时采集的、不可变的硬件/OS指纹,它决定了“这台机器能做什么”;而pillar也不是“配置中心”,它是master端定义的、按目标精准推送的、可加密的业务密钥与策略数据,它回答“这台机器该做什么”。理解这些差异,不是抠字眼,而是决定你写的state是能稳定运行三年,还是三个月后就变成没人敢动的“祖传代码”。这篇文章不教你怎么安装,不列命令大全,只做一件事:把SaltStack的术语体系彻底掰开、揉碎、还原成真实场景里的动作逻辑。无论你是刚接触自动化运维的初级工程师,还是已用Ansible或Puppet多年、想评估SaltStack是否值得切换的技术负责人,只要你想让自动化真正“可维护、可审计、可扩展”,这篇就是你绕不开的第一课。
2. 核心设计哲学与术语体系全景图:为什么SaltStack的每个词都不可替代
2.1 SaltStack不是“另一个Ansible”,它的基因决定了一切
很多人初学SaltStack,下意识拿它和Ansible比:都是YAML写配置、都能批量执行命令、都支持playbook/state。但这种对比就像拿汽车和飞机比“都有轮子”——忽略了最根本的架构差异。Ansible是SSH驱动的推式(push)模型:控制节点临时连上目标机,执行完就断开,状态靠本地文件记录。SaltStack则是ZeroMQ/RAET驱动的拉式(pull)+事件总线混合模型:minion常驻后台,主动连接master建立长连接,所有指令通过消息队列异步下发,执行结果实时回传。这个底层差异直接催生了SaltStack独有的术语生态。举个最典型的例子:Ansible的inventory是静态主机列表,而SaltStack的targeting(目标匹配)是动态的、多维度的、可编程的。你可以用-G 'os:Ubuntu'按grains匹配,用-I 'environment:prod'按pillar匹配,甚至用-C 'G@os:Ubuntu and not I@role:db'组合逻辑——这不是语法糖,而是架构赋予的能力。因为minion持续在线,master能随时获取其最新grains、pillar状态,并基于此做毫秒级决策。所以当你看到salt -G 'os:CentOS' pkg.install httpd时,背后不是简单的“找CentOS机器装Apache”,而是master向所有已注册minion广播一条“匹配grains.os==CentOS”的指令,每个minion收到后自行判断是否响应,再执行pkg模块。这种设计让SaltStack在万级节点规模下依然保持亚秒级响应,但也意味着:你必须理解grains和pillar的生命周期,否则targeting会失效。我见过太多团队把grains当普通变量改,结果发现修改不生效——因为grains是minion启动时采集的只读数据,改了得重启minion。这就是术语背后的硬约束,不是文档没写清楚,是你没意识到这个词绑定着底层机制。
2.2 术语全景图:一张表理清所有核心概念的定位与关系
| 术语 | 定义本质 | 生命周期 | 关键特性 | 常见误用 | 实际作用 |
|---|---|---|---|---|---|
| salt-master | SaltStack的中央协调器与策略分发中心 | 长期运行服务进程 | 单点(可集群)、管理minion密钥、编译state、分发pillar、接收事件 | 当作“跳板机”或“配置数据库”使用 | 接收minion连接、验证身份、分发指令、聚合结果、触发事件 |
| salt-minion | SaltStack的执行代理与状态报告者 | 长期运行服务进程 | 每台受管主机一个、主动连接master、执行模块、上报grains/pillar | 当作“监控Agent”或“日志收集器”部署 | 建立安全连接、执行master指令、采集并上报grains、拉取并应用pillar、上报执行结果 |
| grains | minion启动时采集的、只读的硬件/OS基础属性 | minion启动时采集,重启后更新 | 静态、不可变、全局可见(所有minion都能查其他minion的grains) | 在state中动态修改grains值 | 提供精准targeting依据(如-G 'os:Ubuntu')、作为state条件判断输入(如{% if grains['os'] == 'RedHat' %}) |
| pillar | master端定义的、按target精准推送的、可加密的业务配置数据 | master重启或salt-run pillar.show_pillar后刷新 | 动态、可加密、target-specific、仅对匹配minion可见 | 把敏感信息(密码、密钥)硬编码在state中 | 分离环境配置(dev/prod)、注入密钥、定义角色(role:web/db)、控制state开关(如enable_nginx: true) |
| state | 描述系统“期望状态”的YAML/Python文件,声明式定义资源 | 手动触发(salt '*' state.apply)或通过schedule自动执行 | 声明式、幂等、依赖管理、支持Jinja2模板 | 当作“Shell脚本”写,忽略idempotency | 告诉minion“最终要变成什么样”,如“确保Nginx包已安装、配置文件存在且内容正确、服务正在运行” |
| execution module | minion端执行的原子操作函数,如pkg.install、cmd.run | 内置模块随minion安装,自定义模块需同步 | 过程式、非幂等、即时返回结果 | 在state中滥用cmd.run替代pkg.installed | 提供即时操作能力(调试、临时任务),是state底层执行单元 |
| renderer | 将state文件(如.sls)转换为Python数据结构的引擎 | 文件加载时调用 | 默认Jinja-YAML,支持纯YAML、Mako等 | 忽略renderer链导致Jinja语法报错 | 解析state文件,处理模板逻辑,生成最终执行数据 |
这张表不是为了背诵,而是帮你建立“术语-机制-行为”的映射。比如看到pillar,立刻想到三点:1)它在master上定义;2)它只推送给匹配的minion;3)它能加密。这就解释了为什么生产环境的数据库密码绝不能写在state里,而必须放在pillar中,并用gpgrenderer加密。再比如grains,它的“只读性”直接决定了你在自动化流程中,如果需要根据CPU核心数动态调整服务进程数,不能在state里去改grains,而应该用grains.item模块在minion启动时采集,或在pillar中预定义不同规格机器的配置。术语不是孤立的单词,它们是SaltStack这台精密仪器上的一个个齿轮,咬合错了,整个系统就卡顿。
2.3 为什么“Master-Minion”模型是双刃剑?必须直面的现实约束
很多教程把salt-master和salt-minion描绘成“老板-员工”的理想关系,但真实世界里,这对组合有它固有的张力。先说salt-master:它不是无状态的。它的核心数据包括minion的公钥(存于/etc/salt/pki/master/minions/)、pillar缓存、job缓存(默认内存,可配Redis)。这意味着master重启后,minion需要重新认证(虽然通常自动完成),而长时间运行的master可能因job积压导致内存飙升——我们曾遇到过master因未清理旧job,内存占用超80%,响应延迟从50ms涨到3s。解决方案?定期salt-run jobs.active看活跃任务,用salt-run jobs.list_jobs查历史,再用salt-run jobs.clear_cache清理。这不是高级技巧,是日常运维的必修课。再说salt-minion:它常驻后台,但并非完全“透明”。一个被遗忘的minion,可能因网络波动断连后无法自动重连(尤其在某些防火墙策略下),或者因磁盘满导致/var/cache/salt/minion/写失败而静默退出。这时候salt '*' test.ping会显示超时,但你得先登录机器查systemctl status salt-minion,再看journalctl -u salt-minion -n 50。更隐蔽的是minion的grains刷新:默认只在启动时采集,但有些场景需要实时更新,比如云主机IP变更。这时不能等重启,得手动salt '*' saltutil.refresh_grains。这些都不是bug,而是架构选择带来的必然运维负担。理解这一点,才能避免把SaltStack当成“设好就忘”的黑盒。它强大,但要求你对它的“呼吸节奏”有感知——master的job队列长度、minion的连接状态、grains的时效性,都是你需要监控的指标。我建议在Prometheus里加几个关键exporter:salt-exporter抓master指标,node_exporter加自定义脚本查minion进程存活,这才是真正的生产就绪。
3. 核心术语深度拆解:从原理到实操的每一处细节
3.1 salt-master:不只是“服务器”,它是策略中枢与信任锚点
salt-master远不止是一个监听4505/4506端口的服务。它的核心职责有三层:身份认证中心、策略分发引擎、事件总线枢纽。先看身份认证:当minion首次启动,它会生成一对RSA密钥,将公钥发给master。master收到后,存入/etc/salt/pki/master/minions/目录,文件名即minion ID(默认为主机名)。此时minion处于“pending”状态,master管理员需手动salt-key -a minion-id接受,或配置auto_accept: True自动接受(仅限测试环境!)。这个过程建立了双向信任:minion信任master的公钥(用于加密通信),master信任minion的公钥(用于身份识别)。一旦接受,master会将自身的公钥发给minion,双方用此建立TLS加密通道。这就是为什么/etc/salt/pki/master/master.pem和/etc/salt/pki/master/master.pub如此关键——它们是整个集群的信任根。如果master密钥泄露,攻击者可伪造master下发恶意指令;如果minion密钥泄露,攻击者可伪装成合法节点窃取pillar数据。所以生产环境必须严格管控/etc/salt/pki/目录权限(700),并定期轮换密钥(salt-key -d old-minion-id && salt-key -a new-minion-id)。再看策略分发:master不直接执行任何操作,它只编译state、解析pillar、生成执行计划,然后通过ZeroMQ消息队列推送给minion。这个“编译”过程很关键——当你运行salt '*' state.apply nginx,master会:1)根据target找到匹配minion;2)为每个minion拉取其专属pillar;3)合并base环境下的nginx.sls和top.sls指定的state;4)用Jinja渲染器处理模板;5)将最终的Python数据结构序列化后发送。整个过程master不碰目标机器的文件系统,它只是“导演”。最后是事件总线:SaltStack的event系统是其灵魂。每次minion执行完一个state,都会向master发布一个salt/job/<jid>/ret/<minion-id>事件,包含返回码、输出、耗时等。master可以监听这些事件,触发后续动作,比如state.apply成功后自动发Slack通知,或失败时触发告警。这需要配置/etc/salt/master.d/reactor.conf,定义事件匹配规则和响应runner。例如:
# /etc/salt/master.d/reactor.conf reactor: - 'salt/job/*/ret/*': - /srv/reactor/notify.sls然后在/srv/reactor/notify.sls里写:
# /srv/reactor/notify.sls notify_slack: runner.slack.post_message: - channel: '#alerts' - message: 'Job {{ data["jid"] }} on {{ data["id"] }} returned {{ data["return"]["retcode"] }}' - from_name: 'SaltStack'这体现了master的“中枢”属性:它不干活,但指挥一切。理解这点,你就明白为什么优化master性能的关键是减少不必要的事件发布(如关闭state_events: False)和合理配置worker_threads(默认为cpu核心数,万级节点建议调至32-64)。
3.2 salt-minion:不只是“客户端”,它是自治执行体与状态哨兵
如果说master是大脑,minion就是遍布全身的神经末梢与肌肉。它的设计哲学是自治、轻量、可靠。一个minion进程包含三个核心线程:PubChannel(处理master指令)、EventPublisher(上报事件)、Maintenance(维护连接与缓存)。当master下发指令,PubChannel线程接收并解析,然后交由Execution Module执行;执行完毕,Maintenance线程将结果打包,通过EventPublisher线程发回master。这种分离设计保证了即使某个模块执行卡死,也不会阻塞指令接收。minion的配置文件/etc/salt/minion是它的“基因图谱”。关键参数如master: salt-master.example.com定义连接目标;id: web01-prod强制设置minion ID(避免DNS问题);grains:区块可手动添加自定义grains(如{"region": "us-west-2", "env": "prod"}),这些会与自动采集的grains合并,成为targeting的依据。但要注意:手动grains优先级低于自动grains,且同样只在启动时加载。所以grains.append这样的动态操作无效。minion的缓存机制是其可靠性的基石。它会在/var/cache/salt/minion/下缓存:1)master的公钥(pki/master.pub);2)最近执行的state结果(acc/目录);3)pillar数据(ext_pillar/目录)。这意味着即使master短暂宕机,minion仍能基于缓存的pillar继续执行state.apply(只要缓存未过期)。但这也带来风险:如果pillar更新了,minion不会自动拉取,除非你显式运行salt '*' saltutil.refresh_pillar。我们曾因此踩坑:一次数据库密码轮换后,忘了刷新pillar,导致所有应用服务因认证失败而雪崩。解决方案是将refresh_pillar加入CI/CD流水线,在pillar更新后自动触发。另外,minion的心跳机制(ping_interval)和重连策略(recon_default,recon_max,recon_randomize)决定了它的韧性。默认ping_interval: 0(禁用心跳),但生产环境建议设为60秒,并配置recon_max: 600(最大重连间隔10分钟),避免网络抖动时频繁重连冲击master。最后,minion的模块加载是动态的。内置模块(如pkg,file,service)随minion安装,但自定义模块(如/srv/salt/_modules/my_custom.py)需通过salt '*' saltutil.sync_modules同步到所有minion,再用salt '*' sys.reload_modules重载。这解释了为什么你写了新模块却提示Function my_custom.do_something is not available——缺了同步和重载两步。记住:minion不是被动接收者,它是带着缓存、带着心跳、带着模块生态的自治体。
3.3 grains与pillar:一对互补的“数据双生子”,分工明确不容混淆
这是SaltStack最易混淆,也最关键的术语对。用一句话概括:grains是“我是谁”,pillar是“我该做什么”。grains是minion启动时,通过调用/usr/lib/python3/dist-packages/salt/grains/core.py等模块,扫描/proc/cpuinfo、/etc/os-release、dmidecode等系统信息,生成的只读字典。它反映的是物理/OS层面的客观事实:CPU型号、内存大小、操作系统版本、虚拟化类型(KVM/VMware/AWS)、网络接口列表。正因为它是客观的、只读的,所以grains是targeting的黄金标准。比如salt -G 'virtual:kvm' cmd.run 'uptime'能精准找到所有KVM虚拟机,而-G 'os:Ubuntu'则找到所有Ubuntu系统。但grains也有局限:它不包含业务信息。你无法用grains区分“这台Ubuntu是Web服务器还是数据库服务器”,因为它不关心你的业务逻辑。这时pillar就登场了。pillar是master端定义的、JSON/YAML格式的配置数据,存储在/srv/pillar/目录。它的核心价值在于按target精准推送、可加密、可继承。一个典型的/srv/pillar/top.sls:
base: 'web*': - webserver 'db*': - database '*': - common对应/srv/pillar/webserver.sls:
nginx: version: 1.20.1 enable_ssl: true ssl_cert: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- ssl_key: | -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----当salt 'web01' pillar.items执行时,master会:1)匹配web01到'web*';2)加载webserver.sls和common.sls;3)合并数据(common中的同名key会被webserver覆盖);4)将最终字典推送给web01。注意:db01永远看不到webserver.sls的内容,这是pillar的隔离性。而ssl_key这种敏感数据,可通过gpgrenderer加密:先用gpg --gen-key生成密钥对,导出公钥到/etc/salt/gpgkeys/,然后在pillar文件开头加#!jinja|yaml|gpg,再将私钥部分用gpg --encrypt --armor --recipient 'salt-pillar'加密。这样,只有master能解密,minion收到的是明文,但传输过程全程加密。grains和pillar的协同使用才是威力所在。比如一个通用state:
# /srv/salt/nginx/init.sls {% set nginx_conf = salt['pillar.get']('nginx:config', default={}) %} {% if grains['os_family'] == 'RedHat' %} nginx-package: pkg.installed: - name: nginx {% elif grains['os_family'] == 'Debian' %} nginx-package: pkg.installed: - name: nginx-full {% endif %} nginx-config: file.managed: - name: /etc/nginx/nginx.conf - source: salt://nginx/files/nginx.conf - template: jinja - context: enable_ssl: {{ pillar.get('nginx:enable_ssl', false) }} cert_content: {{ pillar.get('nginx:ssl_cert', '') }}这里,grains['os_family']决定装哪个包(适配OS差异),pillar.get('nginx:enable_ssl')决定是否启用SSL(业务策略)。两者结合,一份state就能覆盖多环境、多OS。混淆它们的后果很严重:把业务配置(如数据库URL)写进grains,会导致所有minion都能看到,且无法按环境区分;把OS信息(如osrelease)写进pillar,则失去了grains的自动采集优势,还得手动维护。记住:grains是上帝视角的硬件快照,pillar是上帝视角的业务蓝图,它们共同构成SaltStack的“数据地基”。
3.4 state与execution module:声明式与过程式的共生关系
state和execution module是SaltStack的“左右手”,一个负责“我要什么”,一个负责“我现在做什么”。execution module(简称module)是minion端执行的原子函数,如pkg.install、file.touch、cmd.run。它们是过程式、非幂等、即时返回的。salt '*' pkg.install vim执行后,立刻返回安装结果;salt '*' cmd.run 'df -h'立刻返回磁盘信息。Module是SaltStack的“肌肉”,提供底层能力。而state是声明式、幂等、描述期望状态的YAML/Python文件。state.apply不是执行命令,而是告诉minion:“请确保以下状态成立:Nginx包已安装、配置文件存在且内容正确、服务正在运行”。minion会检查当前状态,只对不一致的部分执行操作。比如pkg.installedstate,它会先调用pkg.versionmodule查当前版本,如果不存在或版本不符,才调用pkg.install。这就是幂等性:多次执行,结果不变。理解这个区别,是写出健壮state的前提。常见错误是滥用cmd.run。比如有人写:
# 错误:用cmd.run代替pkg.installed install-nginx: cmd.run: - name: apt-get update && apt-get install -y nginx这违反了幂等性:每次执行都会apt-get update,浪费带宽;且无法感知Nginx是否已安装,导致重复执行。正确写法是:
# 正确:用pkg.installed声明状态 nginx-package: pkg.installed: - name: nginx - refresh: true # 等价于apt-get updatepkg.installed内部会智能判断:如果包已安装且版本满足要求,什么也不做;否则才执行安装。State还支持依赖管理(require,watch)和条件判断(onlyif,unless)。比如:
nginx-service: service.running: - name: nginx - enable: true - require: - pkg: nginx-package - file: nginx-config - watch: - file: nginx-config # 如果config文件变化,自动reload nginx这里require确保包和配置先于服务启动,watch实现配置热更新。而execution module的价值在于调试与临时任务。当你state执行失败,第一反应不是改state,而是用module排查:
# 查看当前Nginx状态 salt 'web01' service.status nginx # 查看配置文件语法 salt 'web01' cmd.run 'nginx -t' # 手动重载 salt 'web01' service.reload nginx这些即时反馈,是state无法提供的。Module和state的关系,就像建筑师(state)和工人(module):建筑师画图纸(声明状态),工人按图纸施工(执行module),但工人也能随时汇报现场情况(module调试)。一个成熟的SaltStack工程师,必须熟练切换这两种思维模式。
4. 实操全流程:从零构建一个可落地的Web服务自动化项目
4.1 环境准备与最小化验证:确保master-minion心跳正常
在开始写任何state前,必须建立一个健康的通信基线。假设你有一台CentOS 7 master和一台Ubuntu 20.04 minion。第一步,安装master:
# CentOS 7 master sudo yum install -y https://repo.saltproject.io/py3/redhat/7/x86_64/latest/SaltProject.repo sudo yum install -y salt-master sudo systemctl enable salt-master sudo systemctl start salt-master第二步,安装minion:
# Ubuntu 20.04 minion curl -fsSL https://repo.saltproject.io/py3/ubuntu/20.04/amd64/latest/SaltProject.repo | sudo tee /etc/apt/sources.list.d/salt.list sudo apt-get update sudo apt-get install -y salt-minion第三步,配置minion指向master。编辑/etc/salt/minion:
master: 192.168.1.100 # master的IP id: web01-prod # 强制设置minion ID grains: role: web env: prod第四步,启动minion并接受密钥:
sudo systemctl enable salt-minion sudo systemctl start salt-minion # 在master上执行 sudo salt-key -L # 查看pending keys sudo salt-key -a web01-prod # 接受第五步,验证通信。在master上运行:
sudo salt 'web01-prod' test.ping # 应返回 True sudo salt 'web01-prod' test.version # 应返回minion的Salt版本,如 '3004' sudo salt 'web01-prod' grains.items | grep -E "(os|os_family|cpuarch)" # 应返回类似 os: Ubuntu, os_family: Debian, cpuarch: x86_64这五步看似简单,却是90%故障的源头。常见问题:1)防火墙阻塞4505/4506端口(sudo firewall-cmd --permanent --add-port=4505-4506/tcp);2)minion配置的master地址无法解析(用IP而非hostname);3)id设置与DNS主机名不一致导致targeting失败。我习惯在/etc/hosts里加一行192.168.1.100 salt-master,并在minion配置中用master: salt-master,这样既可读又可靠。验证通过后,你才拥有了一个“活着”的SaltStack集群,这是所有后续工作的基石。
4.2 pillar配置:为Web服务定义安全、可变的业务参数
现在,我们为Web服务定义pillar。创建目录结构:
sudo mkdir -p /srv/pillar/{webserver,database,common} sudo touch /srv/pillar/top.sls编辑/srv/pillar/top.sls:
base: 'web*': - webserver 'db*': - database '*': - common编辑/srv/pillar/common.sls(通用配置):
# /srv/pillar/common.sls global: timezone: Asia/Shanghai locale: en_US.UTF-8 ntp_server: pool.ntp.org编辑/srv/pillar/webserver.sls(Web服务特有配置):
# /srv/pillar/webserver.sls nginx: version: 1.20.1 enable_ssl: true ssl_cert: | -----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJAN... -----END CERTIFICATE----- ssl_key: | -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAu... -----END RSA PRIVATE KEY----- sites: - name: default port: 80 root: /var/www/html - name: api port: 8000 root: /opt/api注意:ssl_key是敏感信息,生产环境必须用gpg加密。这里为演示简化。配置完成后,刷新pillar:
sudo salt 'web01-prod' saltutil.refresh_pillar # 验证 sudo salt 'web01-prod' pillar.items # 应看到nginx相关的所有配置关键点:pillar的top.sls是入口,它像路由器一样,根据minion ID(web*)将webserver.sls推送给匹配的minion。common.sls被所有minion加载,提供全局基础配置。这种分层设计,让你能轻松扩展:新增web02-prod,只需在/etc/salt/minion里设id: web02-prod,它自动获得相同pillar。而database.sls则完全隔离,web01-prod永远看不到数据库密码。这就是pillar的威力:用最少的配置,实现最大的灵活性与安全性。
4.3 state编写:从零构建Nginx Web服务的完整生命周期
现在,我们编写state来部署Nginx。创建目录:
sudo mkdir -p /srv/salt/{nginx,nginx/files}首先,/srv/salt/nginx/init.sls是主state文件:
# /srv/salt/nginx/init.sls # 1. 确保系统基础配置 system-config: timezone.system: - name: {{ pillar['global']['timezone'] }} locale.system: - name: {{ pillar['global']['locale'] }} ntp.config: - servers: - {{ pillar['global']['ntp_server'] }} # 2. 安装Nginx包 nginx-package: pkg.installed: - name: nginx - version: {{ pillar['nginx']['version'] }} - refresh: true {% if grains['os_family'] == 'RedHat' %} - pkgs: - nginx {% elif grains['os_family'] == 'Debian' %} - pkgs: - nginx-full {% endif %} # 3. 管理Nginx配置文件 nginx-config: file.managed: - name: /etc/nginx/nginx.conf - source: salt://nginx/files/nginx.conf - template: jinja - context: enable_ssl: {{ pillar['nginx']['enable_ssl'] }} sites: {{ pillar['nginx']['sites'] }} - user: root - group: root - mode: '644' # 4. 管理站点配置文件 {% for site in pillar['nginx']['sites'] %} nginx-site-{{ site['name'] }}: file.managed: - name: /etc/nginx/sites-available/{{ site['name'] }} - source: salt://nginx/files/site.jinja - template: jinja - context: site: {{ site }} enable_ssl: {{ pillar['nginx']['enable_ssl'] }} - user: root - group: root - mode: '644' file.symlink: - name: /etc/nginx/sites-enabled/{{ site['name'] }} - target: /etc/nginx/sites-available/{{ site['name'] }} - force: true {% endfor %} # 5. 确保SSL证书存在 {% if pillar['nginx']['enable_ssl'] %} nginx-ssl-cert: file.managed: - name: /etc/nginx/ssl/nginx.crt - contents: {{ pillar['nginx']['ssl_cert'] }} - user: root - group: root - mode: '600' nginx-ssl-key: file.managed: - name: /etc/nginx/ssl/nginx.key - contents: {{ pillar['nginx']['ssl_key'] }} - user: root - group: root - mode: '600' {% endif %} # 6. 启动并启用Nginx服务 nginx-service: service.running: - name: nginx - enable: true - reload: true - require: - pkg: nginx-package - file: nginx-config - file: nginx-ssl-cert - file: nginx-ssl-key - watch: - file: nginx-config - file: nginx-ssl-cert - file: nginx-ssl-key这个state体现了SaltStack的最佳实践:1)用grains适配不同OS家族;2)用pillar注入业务参数;3)用Jinja2循环生成多站点;4)用require和watch管理依赖与热更新。接着,创建配置模板/srv/salt/nginx/files/nginx.conf:
# /srv/salt/nginx/files/nginx.conf user www-data; worker_processes auto; pid /run/nginx.pid; events { worker_connections 768; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; }以及站点模板/srv/salt/nginx/files/site.jinja:
# /srv/salt/nginx/files/site.jinja server { listen {{ site.port }}; server_name _; root {{ site.root }}; index index.html; location / { try_files $uri $uri/ =404; } {% if enable_ssl and site.port == 443 %} listen 443 ssl; ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; {% endif %} }最后,创建/srv/salt/top.sls关联state:
# /srv/salt/top.sls base: 'web*': - nginx现在,执行部署:
sudo salt 'web01-prod' state.apply nginx test=True # 先用test=True预览,确认无误后执行 sudo salt 'web01-prod' state.apply nginxtest=True会模拟执行,告诉你哪些状态会改变,这是避免线上事故的黄金习惯。执行后,检查/etc/nginx/目录,应看到nginx.conf、sites-available/default、sites-enabled/default、ssl/nginx.crt等文件,且nginx服务正在运行。整个过程,你没有写一行Shell脚本,没有手动SSH,所有操作都通过声明式state