ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SaltStack应用部署实战:从state编写到灰度发布与回滚

SaltStack应用部署实战:从state编写到灰度发布与回滚 SaltStack部署应用作业指导书1. 先说说我为什么还在用SaltStack做应用部署先说一个我自己的场景。我这边管理着几十台生产服务器业务形态以Java Web应用为主外围还有一些Nginx静态资源服务。最早部署全靠人肉登录服务器、备份老包、上传war包、重启Tomcat偶尔写个Shell脚本统一处理。服务器少的时候还能对付等到机器数量一多、应用发布频率一上来手工操作就明显顶不住了。后来我引入了SaltStack把它作为配置管理和应用发布的主通道一直用到现在。今天这篇就是我把这套流程整理成的一份可以照着做的作业指导书。SaltStack说白了就是一套基于master-minion架构的自动化运维工具。master下发指令minion在每台被管机器上执行两者之间走ZeroMQ消息队列。它跟Ansible最大的区别在于Ansible默认走SSH推命令SaltStack则是在每台机器上常驻一个agent执行效率更高也适合做比较精细的批量编排。我用它来做应用部署最核心的一个理念就是“一切皆状态”你要部署什么、配置文件长什么样、依赖哪些目录和用户、服务最终应该处于什么状态都写进state文件里执行一次就是一次声明式的收敛。跑完之后系统状态和文件里描述的状态一致这就是一次成功的部署。这篇内容适合谁看我觉得主要是这几类人刚接手运维工作、想摆脱频繁登录服务器手工操作的人已经用了一段时间SaltStack但只会执行salt * cmd.run还没把state管理真正用起来的同学正在设计应用发布流程想把配置管理、发布、回滚串成一个标准化通道的团队。我自己踩过不少坑有些坑在网上查了很多资料才弄明白。所以这篇不会只讲“怎么装”还会讲“为什么这么设计”、遇到问题怎么排查以及一些只有在生产环境里才能体会到的细节。1.1 部署应用这件事核心难点到底在哪很多人一开始觉得部署应用不就是把包拷上去、解压、重启服务嘛。可真到了生产环境你会发现每个环节都有坑配置文件每个环境不一样不能直接覆盖应用依赖的目录、用户、权限必须先准备好服务重启不能简单kill再start要有优雅停机万一新版本有问题回滚要又快又准再往细了说一次发布涉及几十台机器哪台成功哪台失败必须一目了然。所以用SaltStack部署应用本质上不是在解决“怎么拷贝文件”的问题而是在解决“如何把一次发布变成可重复、可审计、可回滚的标准动作”的问题。这是我在设计整个方案时最重要的一个出发点。比如同一个Tomcat应用测试环境和生产环境的数据库地址不一样SaltStack里的pillar就是干这个的——把差异化的参数从state模板里抽出来按环境单独维护。应用目录、启动用户、JVM参数这些也都应该通过变量定义而不是硬编码在sls文件里。这样一套state走天下差别全部由数据层去控制。1.2 这套方案的整体形态下面这个轮廓就是我目前在用的这套方案的简化版一台Salt master负责下发指令、存state文件、做目标匹配每台业务服务器安装salt-minion注册到master统一纳入管理使用git管理/srv/salt下面的所有state文件和/srv/pillar下面的pillar文件应用发布走state.apply配置变更走state.apply服务状态检查也走state.applyJenkins流水线在构建完成后通过调用salt-api远程触发发布任务实现“代码提交到自动部署”的闭环链路。听起来不复杂但要把这套跑顺有很多细节需要抠。下面我会按从零开始搭建的顺序把每一步讲清楚有些参数为什么会这么配、有些命令为什么必须这么写我都会尽量解释到位。2. 环境规划与前置准备先把地基打好我在动手搭SaltStack之前会先花半小时把环境规划好而不是直接一路yum install。这套架构虽然不复杂但master、minion、文件服务器、pillar这些角色的职责一定要提前理清后期才不会乱。2.1 master与minion的部署与基础配置先给出一份我常用的环境参考Salt masterCentOS 7.94C8G磁盘100G内网IP 10.0.0.10Salt minionCentOS 7.92C4G业务服务器若干台Python版本master和minion统一使用Python 3.6及以上新版本SaltStack要求Python 3旧项目如果还有Python 2建议尽快升级安装SaltStack官方源我这边用的是阿里云镜像加速如果网络不便可以直接用官方源。以下是master上的安装方式yum install -y https://repo.saltproject.io/py3/redhat/7/x86_64/latest/salt-repo-latest.el7.repo yum clean expire-cache yum install -y salt-master salt-minion注意master上也建议装一个salt-minion为什么后面你调试state.apply、用salt-call在master本机验证状态时都方便。这是很多人忽略的一个小技巧。配置/etc/salt/master我一般会做这几项关键配置interface: 10.0.0.10 publish_port: 4505 ret_port: 4506 file_roots: base: - /srv/salt/base pillar_roots: base: - /srv/pillar/base auto_accept: False这里有几个点需要单独说明publish_port4505是master用来向minion发布命令的端口ret_port4506是minion返回结果给master的端口。这两个端口很重要防火墙和安全组必须放通否则minion根本无法收到指令。我遇到过几次“配置没问题但minion掉线”的情况最后查下来都是安全组没放行4506端口。auto_accept不建议设为True。虽然自动接受密钥省事但生产环境里最好还是手动salt-key -a确认防止别人随手加一台机器就混进集群。file_roots和pillar_roots我单独建了base目录这样后续如果要分环境可以按目录去扩展不用重构。接下来配置minion通常在每台业务服务器上执行yum install -y salt-minion然后编辑/etc/salt/minionmaster: 10.0.0.10 id: web-01这里有个关键点id必须唯一而且建议在安装时就定义好。我之前踩过一个坑两台机器id相同结果第二台机器注册后第一台机器的minion密钥被覆盖导致第一台机器掉线排查了很久。后来我在所有机器的minion配置里统一用“角色-编号”的命名方式比如web-01、redis-01这样既好识别又不会冲突。启动并注册systemctl enable --now salt-master systemctl enable --now salt-minion # 在master上查看和接受密钥 salt-key -L salt-key -a web-01 salt * test.ping2.2 grains、pillar与目录结构的设计等minion全部连上来之后下一步不是着急部署应用而是把grains和pillar这套数据体系建好。grains是minion上报给master的静态信息比如操作系统、CPU核数、内网IP等pillar是master下发给minion的配置数据用来存放密码、端口、环境标签这类差异化的参数。我先在每台minion上自定义一些grains比如角色和可用区。在/etc/salt/grains里写roles: - web env: prod zone: az1写完重启salt-minion或执行salt web-01 saltutil.refresh_grains即可生效。有了grains之后我就可以在state文件里用grains做目标匹配了。比如salt -G roles:web test.ping salt -C roles:web and env:prod test.ping-G是按grain匹配-C是复合匹配支持and/or/not逻辑。这个功能在灰度发布时特别有用我后面会专门讲到。pillar的目录我这样规划/srv/pillar/base/ ├── top.sls ├── common.sls └── app-web.slstop.sls按机器ID或grain来匹配变量base: roles:web: - match: grain - common env:prod: - match: grain - app-webapp-web.sls里放应用相关的差异化参数app-web: jvm_opts: -Xms512m -Xmx1024m db_host: 10.0.0.20 db_port: 3306state文件里就可以通过pillar[app-web][db_host]去引用这也是我坚持把参数从state里抽出来的原因改配置不用改逻辑改逻辑不用动配置。state目录我建议这样组织/srv/salt/base/ ├── top.sls ├── common/ │ ├── init.sls │ └── files/ ├── tomcat/ │ ├── init.sls │ ├── app.sls │ └── files/ └── app-web/ ├── init.sls └── files/top.sls是把state文件映射到目标机器的入口。一套好的目录结构应该能让你一眼看出“这台机器最终会执行哪些状态”这是可维护性的基础。3. 核心实操从零编写state文件并完成首次应用部署地基建好之后就可以开始写state了。这一章我以部署一个Java Web应用到Tomcat为例把整个流程掰开揉碎讲清楚。state文件是SaltStack的核心很多人觉得SaltStack难其实难的不是语法而是“状态思维”的转变你要告诉系统“最终状态是什么样”而不是“接下来做什么”。3.1 用state实现Tomcat的安装与配置管理我在common/init.sls里放一些所有机器都通用的基础配置比如系统参数和文件描述符限制。这里的关键套路是用file.managed托管配置文件用cmd.run执行系统命令然后用onchanges只在文件真正变化时才触发后续动作。/etc/sysctl.conf: file.managed: - source: salt://common/files/sysctl.conf - user: root - group: root - mode: 644 sysctl-reload: cmd.run: - name: sysctl -p - onchanges: - file: /etc/sysctl.conf为什么这里要用onchanges而不是直接cmd.run因为sysctl -p这个操作不需要每次执行state都跑一遍只有在sysctl.conf真正发生变化时才应该执行。onchanges会拿当前执行结果和上一次做对比状态没有变化就跳过这样既保证幂等性又不会产生无意义的操作。生产环境里这类细节决定了你的发布脚本能不能反复执行而不出乱子。接下来安装Tomcat。安装Tomcat的方式有两种用系统包管理器直接安装或者用二进制包解压。我这里推荐用二进制包的方式原因很简单版本和JVM参数完全可控不依赖操作系统的软件仓库节奏。state可以这么写create-tomcat-user: user.present: - name: tomcat - system: True - shell: /sbin/nologin tomcat-dir: file.directory: - name: /opt/tomcat - makedirs: True - user: tomcat - group: tomcat tomcat-archive: archive.extracted: - name: /opt/tomcat - source: salt://tomcat/files/apache-tomcat-8.5.98.tar.gz - archive_format: tar - user: tomcat - group: tomcat - unless: test -f /opt/tomcat/bin/startup.sh这里unless是幂等性的关键。如果没有unless判断每次执行state都会重新解压一次虽然tar会覆盖文件但万一解压过程中把正在运行的Tomcat文件替换了就意味着一次潜在的故障。加上unless之后只有当/opt/tomcat/bin/startup.sh不存在时才执行解压第二次执行state时直接跳过稳定多了。接着托管Tomcat的server.xml。我的原则是所有需要统一管控的配置文件都由SaltStack统一分发不让人手工登录修改避免“配置漂移”。server.xml: file.managed: - name: /opt/tomcat/conf/server.xml - source: salt://tomcat/files/server.xml - user: tomcat - group: tomcat - mode: 640最后是服务管理。用systemd管理Tomcat然后通过service.running声明“它必须处于运行状态”tomcat: service.running: - name: tomcat - enable: True - watch: - file: /opt/tomcat/conf/server.xmlwatch的意思是被监控的文件发生变化时自动重启服务。这一点很实用以后你只要改server.xml然后执行stateTomcat会自动平滑重启省去手工登录操作。不过要小心watch一定要配合合理的配置变更频率使用如果在某台机器上频繁触发重启要考虑是不是配置文件模板里带了时间戳之类的动态内容导致文件内容每次都在变。这个坑后面会专门讲。3.2 应用代码包的分发、解压与滚动重启Tomcat装好之后重点来了应用war包怎么分发和更新。这是“SaltStack部署应用”里最核心的环节。我推荐的目录方案是“版本目录软链”/apps/web/releases/20250105120000/存放带时间戳的版本包/apps/web/current软链指向当前运行的版本目录Tomcat的docBase配置指向/apps/web/current/ROOT这样做的最大好处是回滚极快只需要切换软链再重启不需要重新上传老包。那么state怎么写我先创建基础目录和用户ensure-web-user: user.present: - name: app - system: True - shell: /sbin/nologin ensure-app-dirs: file.directory: - name: /apps/web/releases - makedirs: True - user: app - group: app接着是上传和校验war包。我一般在发布时用file.managed把构建好的war包分发到版本目录然后用cmd.run执行校验和比较。比如deploy-war: file.managed: - name: /apps/web/releases/20250105120000/ROOT.war - source: salt://app-web/files/ROOT.war - user: app - group: app - mode: 644如果希望校验包完整性可以用source_hash参数在分发前先校验哈希防止文件在网络传输或拷贝过程中损坏。解压和更新软链我放在同一个sls文件里通过cmd.run串起来。注意这里要重点讲一下为什么用软链方式而不是直接在Tomcat的webapps目录里替换war包。因为war包直接替换到webapps目录Tomcat如果热部署没开很可能需要重启如果开了热部署又可能出现“新包正在解压、老包还在运行”的中间态日志里全是各种奇怪的类加载错误。软链切换则完全是原子操作先把新版本解压到新的版本目录再把软链指向新目录最后重启Tomcat全程没有文件覆盖的中间状态。extract-war: cmd.run: - name: cd /apps/web/releases/20250105120000 unzip -qo ROOT.war -d ROOT chown -R app:app ROOT - unless: test -d /apps/web/releases/20250105120000/ROOT这里的unless同样是幂等控制防止重复解压。解压完成后更新软链switch-current: cmd.run: - name: ln -sfn /apps/web/releases/20250105120000 /apps/web/current - require: - cmd: extract-war最后重启Tomcat并检查健康状态restart-tomcat: cmd.run: - name: systemctl restart tomcat - onchanges: - file: /apps/web/releases/20250105120000/ROOT.war check-tomcat: cmd.run: - name: curl -fsS http://127.0.0.1:8080/health - failhard: True - require: - cmd: restart-tomcatfailhard: True是一个容易被人忽略但很有用的参数。它表示当前状态执行失败时立即中止后续所有状态不让后面的命令继续跑。配合健康检查能有效避免“应用已经起不来了脚本还继续往下执行”的情况。3.3 执行apply时的关键操作与日志解读state文件写完后在master上执行salt web-01 state.apply tomcat输出结果里每个state下面都会有Succeeded、Failed、Changed、Unchanged这几个计数器。第一次执行时大部分操作应该是Changed第二次再执行应该变成Unchanged这就是幂等性的体现。如果你发现某个状态每次执行都是Changed那大概率是写得不幂等要查一查原因。还有一个小技巧加--outjson看详细输出salt web-01 state.apply tomcat --outjson生产环境执行时我习惯先对一台测试机执行确认无误后用grain匹配批量跑。批量执行时控制好并发度可以加--batch-size20%或者分批次执行避免一次性把几十台机器全部重启造成服务整体不可用。这些细节虽然不起眼但在生产环境里都是保命用的。4. 把自动部署玩明白结合Jenkins和高级匹配实现灰度发布基础部署能跑通之后就开始要往自动化方向走了。这一章我会讲两件事怎么把SaltStack接到Jenkins流水线里以及在多机环境下怎么做滚动和灰度发布。这两个能力是生产环境发布提效的关键。4.1 Jenkins触发SaltStack执行发布任务我的做法是Jenkins负责代码编译、打包war、推送产物到Salt master的/srv/salt/base/app-web/files/目录然后调用salt命令触发state。这个过程你可以用SSH插件直接执行命令也可以用salt-api我推荐后一种因为更安全、可控且方便审计。先开启salt-api并配置权限。安装yum install -y salt-api在master配置里加入external_auth: pam: saltapi: - .* rest_cherrypy: port: 8000 ssl_crt: /etc/pki/tls/certs/localhost.crt ssl_key: /etc/pki/tls/private/localhost.key重启后用curl测试接口curl -sk https://10.0.0.10:8000/login \ -H Accept: application/x-yaml \ -d usernamesaltapi \ -d password你的密码 \ -d eauthpam拿到token之后调state.apply接口curl -sk https://10.0.0.10:8000/ \ -H Accept: application/x-yaml \ -H X-Auth-Token: TOKEN \ -d clientlocal \ -d tgtweb-01 \ -d funstate.apply \ -d argtomcat这段命令看着不多但背后解决的问题是Jenkins不需要保存每台服务器的root密码它在发布机上执行一次API调用由salt-api完成认证和授权。权限收敛之后整个发布链路就不会因为一台机器密码泄露导致全网沦陷。如果你不想部署salt-api也可以在Jenkins服务器上装salt-master或salt-minion然后用CLI方式调用。不过既然要打通流水线我建议一步到位直接上salt-api这样后续要对接审批、审计、消息通知都方便。4.2 用target匹配和分批策略实现滚动发布多机部署时“一次性全部重启”是不可接受的。一定要分批每批执行完做健康检查确认没问题再放下一批。SaltStack的target匹配在这里用处非常大。假设我有12台web服务器分布在az1和az2两个可用区。灰度发布时我先发布az1的6台salt -C roles:web and env:prod and zone:az1 state.apply app-web --batch-size2--batch-size2表示同时只执行2台一批完成后再下一批。执行完所有az1机器后手动或脚本检查健康状态确认没问题再发az2salt -C roles:web and env:prod and zone:az2 state.apply app-web --batch-size3如果你用的是nodegroups还可以把机器组预设在master配置里这样命令更短nodegroups: web-az1: Lweb-01,web-02,web-03 web-az2: Lweb-04,web-05,web-06然后直接用-N web-az1调用salt -N web-az1 state.apply app-web --batch-size2滚动发布时我建议每次只操作总机器数的20%到30%批次之间留出足够的时间窗口观察监控和日志不要贪多。发布中最怕的不是发布失败而是失败了你不知道最后把故障面扩大到所有机器。4.3 快速回滚软链切换的正确打开方式有了软链目录方案回滚就变成了一个非常简单的事情。如果新版本有问题不需要重新解压老war包只需要执行一条命令把/apps/web/current指回上一个版本目录再重启Tomcat。我在每个版本目录下还会保留一个REVISION文件记录这个版本对应的构建号、时间、git commit号。回滚时先看一下历史记录ls -lt /apps/web/releases/ cat /apps/web/releases/20250104120000/REVISION然后切换软链salt web-01 cmd.run ln -sfn /apps/web/releases/20250104120000 /apps/web/current systemctl restart tomcat如果是批量回滚就带target执行salt -C roles:web and env:prod cmd.run ln -sfn /apps/web/releases/20250104120000 /apps/web/current systemctl restart tomcat --batch-size4回滚的关键是“决策要快、动作要稳”。决策快靠监控和告警动作稳靠的是这套标准化脚本。如果回滚动作本身还需要临时去服务器上敲一堆命令那这个回滚方案基本等于没有。5. 常见问题与排障技巧实录用SaltStack做应用部署最让人头疼的往往不是state语法本身而是各种环境问题。这里我把实践中遇到的高频问题整理成一份排障清单每一条都是真金白银踩出来的经验。5.1 密钥与连接问题排查现象minion在salt-key里能看到但salt * test.ping返回False。这个问题的原因通常有三个一是master和minion版本不兼容master是较新版本minion还停在老版本ZeroMQ协议对不上二是minion端的cachedir里有旧的密钥需要清掉重新认证三是4505或4506端口被安全组拦截minion收不到master的发布指令。排查时先看minion日志tail -f /var/log/salt/minion如果日志里反复出现Failed to connect to the master基本就是网络端口问题。用telnet master_ip 4506测一下通不通如果不通去查防火墙和安全组。如果日志里有Authentication error清掉minion上的缓存重新认证systemctl stop salt-minion rm -rf /etc/salt/pki/minion systemctl start salt-minion salt-key -D web-01 salt-key -a web-01这条命令我在生产环境执行过很多次每次都能解决密钥错乱问题操作起来也快不影响业务。现象minion id重复导致一台minion掉线。如果两台机器在/etc/salt/minion里配置了相同的id后注册的minion会覆盖先注册的密钥导致先注册的那台下线。注意这里的“覆盖”不是显式的而是密钥不匹配造成的隐性问题。解决办法就是严格执行“一机一id”而且要建立注册台账每次新装机器都检查一遍再注册。5.2 state执行异常与幂等性问题现象某个state每次执行都显示Changed服务不停重启。我遇到过最常见的原因有两个一是模板文件里包含了动态内容比如每次render都会生成不同的时间戳导致文件内容每次都在变化onchanges一直被触发二是cmd.run没有做幂等控制比如直接执行了systemctl restart tomcat而没有加unless或onlyif判断。解决办法先看具体是哪个state在变salt web-01 state.apply app-web --outjson从JSON输出里找到Changes字段看文件内容到底变化了什么。如果是时间戳问题在模板里把动态内容改成静态内容如果是cmd.run的问题加上unless条件让命令只在目标状态不满足时才执行。比如我之前写过一个初始化缓存目录的state每次都执行mkdir -p /data/cache。后来改成init-cache-dir: cmd.run: - name: mkdir -p /data/cache - unless: test -d /data/cache这样第二次执行时目录已经存在命令被跳过状态变成Unchanged。现象分发文件时源文件变化了但minion上还是旧文件。SaltStack的file.managed默认会检查源文件的hash如果源文件变化minion应该会更新。但有时候master的文件服务器缓存没有刷新导致minion拿到的是旧文件。这时候在master上执行salt * saltutil.clear_cache salt * saltutil.refresh_modules如果是因为你直接修改了/srv/salt/base里的文件通常还需要等待几秒让master更新file roots的缓存也可以主动执行salt-run fileserver.update。5.3 pillar和grains数据不生效现象修改了pillar文件但minion上salt-call pillar.items还是旧数据。这是因为pillar数据不是实时下发的需要在minion上主动刷新。执行salt * saltutil.refresh_pillar刷新后如果还不行检查top.sls里的匹配规则。pillar的匹配规则和state的target匹配是独立的容易混淆。比如我在pillar/top.sls里用了roles:web的grain匹配就一定要确认minion上确实有这个grain否则匹配不上pillar就是空的。遇到匹配不上的情况可以先用一个临时命令验证salt -G roles:web pillar.items看看到底哪些机器匹配到了哪些没有。这样能快速定位是匹配规则的问题还是pillar文件本身的问题。5.4 生产环境执行策略与安全注意事项最后说几个生产级的小注意事项每一条都是别人踩过的坑不要把生产机器的root密码写在Jenkins配置里。走salt-api的eauth或PAM认证权限在master上一处收紧。不要在业务高峰期执行大规模state.apply特别是带watch重启的操作。即使你的state写得没问题也要留出观察窗口。批量操作务必加--batch-size不要一条命令打全量。不管是发布还是重启分批执行都是底线。state文件要进git禁止直接在服务器上改。就算只是改一个端口也要走版本库方便回溯和审计。敏感信息不要直接写在sls文件里。用pillar保存密码和密钥配合pillar_roots的权限管理来控制访问必要的时候对接外部密钥管理服务。6. 运维纪律与这套方案后续还能怎么长最后再分享一些我在实践中的体会不算总结只是聊聊那些“做好了能少熬很多夜”的细节。SaltStack这套体系真正跑顺之后你会发现它带来最大的改变不是“命令变短了”而是发布这件事从“操作”变成了“机制”。以前发布要人盯着现在只需要构建、审核、触发剩下的都是状态收敛。但我必须提醒一句工具只是手段运维纪律才是核心。state文件必须有版本管理配置变更必须有评审发布执行必须有灰度回滚动作必须有预案。这些做不到再好的工具也会被用坏。我个人的一个习惯是每次有新应用上线先在测试环境的master上把state完整跑通一遍然后用--batch-size1去生产环境的第一台机器试点确认健康检查通过后再逐步放量。遇到过一次因为Tomcat的server.xml里一个连接池参数写错导致应用起来后几分钟才报错的情况。从那以后我就把健康检查做成了发布流程里的强制环节并且在发布后至少观察两三分钟看监控曲线确认没有异常再到下一批。这套方案后续还可以继续扩展方向也很多可以用SaltStack的reactor配合事件总线在minion上报故障时自动触发修复动作可以用salt-ssh接管那些暂时不方便装agent的机器也可以把state的渲染和后端CMDB打通实现自动注册、自动初始化。核心思想还是那句环境状态代码化发布流程标准化变更过程可视化。如果你也在批量管理服务器、被应用发布折磨得够呛希望这篇指导书能帮你少走点弯路。把基础做好后面的路会轻松很多。
返回列表