
1. 这不是又一个“配置管理工具入门”而是你真正搞懂SaltStack的起点如果你刚点开这篇内容大概率正站在SaltStack的门口反复徘徊文档里满屏的salt-master、salt-minion、pillar、grains、states、execution modules……像一堵密不透风的术语墙。你试过照着官方Quickstart跑通第一条salt * test.ping但当需要把一台新服务器自动装好Nginx、配好SSL证书、再拉起一个Docker容器时就卡在了“接下来该写什么怎么组织为什么这个state不生效”——这根本不是操作问题是底层概念没对齐。我带过十几支运维和SRE团队落地SaltStack90%的新手掉坑不是因为不会写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 Gos:Ubuntu and not Irole:db组合逻辑——这不是语法糖而是架构赋予的能力。因为minion持续在线master能随时获取其最新grains、pillar状态并基于此做毫秒级决策。所以当你看到salt -G os:CentOS pkg.install httpd时背后不是简单的“找CentOS机器装Apache”而是master向所有已注册minion广播一条“匹配grains.osCentOS”的指令每个minion收到后自行判断是否响应再执行pkg模块。这种设计让SaltStack在万级节点规模下依然保持亚秒级响应但也意味着你必须理解grains和pillar的生命周期否则targeting会失效。我见过太多团队把grains当普通变量改结果发现修改不生效——因为grains是minion启动时采集的只读数据改了得重启minion。这就是术语背后的硬约束不是文档没写清楚是你没意识到这个词绑定着底层机制。2.2 术语全景图一张表理清所有核心概念的定位与关系术语定义本质生命周期关键特性常见误用实际作用salt-masterSaltStack的中央协调器与策略分发中心长期运行服务进程单点可集群、管理minion密钥、编译state、分发pillar、接收事件当作“跳板机”或“配置数据库”使用接收minion连接、验证身份、分发指令、聚合结果、触发事件salt-minionSaltStack的执行代理与状态报告者长期运行服务进程每台受管主机一个、主动连接master、执行模块、上报grains/pillar当作“监控Agent”或“日志收集器”部署建立安全连接、执行master指令、采集并上报grains、拉取并应用pillar、上报执行结果grainsminion启动时采集的、只读的硬件/OS基础属性minion启动时采集重启后更新静态、不可变、全局可见所有minion都能查其他minion的grains在state中动态修改grains值提供精准targeting依据如-G os:Ubuntu、作为state条件判断输入如{% if grains[os] RedHat %}pillarmaster端定义的、按target精准推送的、可加密的业务配置数据master重启或salt-run pillar.show_pillar后刷新动态、可加密、target-specific、仅对匹配minion可见把敏感信息密码、密钥硬编码在state中分离环境配置dev/prod、注入密钥、定义角色role:web/db、控制state开关如enable_nginx: truestate描述系统“期望状态”的YAML/Python文件声明式定义资源手动触发salt * state.apply或通过schedule自动执行声明式、幂等、依赖管理、支持Jinja2模板当作“Shell脚本”写忽略idempotency告诉minion“最终要变成什么样”如“确保Nginx包已安装、配置文件存在且内容正确、服务正在运行”execution moduleminion端执行的原子操作函数如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它只推送给匹配的minion3它能加密。这就解释了为什么生产环境的数据库密码绝不能写在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里加几个关键exportersalt-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 nginxmaster会1根据target找到匹配minion2为每个minion拉取其专属pillar3合并base环境下的nginx.sls和top.sls指定的state4用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/下缓存1master的公钥pki/master.pub2最近执行的state结果acc/目录3pillar数据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.slsbase: web*: - webserver db*: - database *: - common对应/srv/pillar/webserver.slsnginx: 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.sls3合并数据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/minionmaster: 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-port4505-4506/tcp2minion配置的master地址无法解析用IP而非hostname3id设置与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.slsbase: 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.slsWeb服务特有配置# /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加密。这里为演示简化。配置完成后刷新pillarsudo salt web01-prod saltutil.refresh_pillar # 验证 sudo salt web01-prod pillar.items # 应看到nginx相关的所有配置关键点pillar的top.sls是入口它像路由器一样根据minion IDweb*将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 testTrue # 先用testTrue预览确认无误后执行 sudo salt web01-prod state.apply nginxtestTrue会模拟执行告诉你哪些状态会改变这是避免线上事故的黄金习惯。执行后检查/etc/nginx/目录应看到nginx.conf、sites-available/default、sites-enabled/default、ssl/nginx.crt等文件且nginx服务正在运行。整个过程你没有写一行Shell脚本没有手动SSH所有操作都通过声明式state