☰
SaltStack任务管理实战:从cron定时到多机编排
2026/10/12 12:53:53 网站建设 项目流程

接手一批新机器的时候,我最怕的就是"夜里有变更"。白天业务跑着,谁也不敢动;凌晨两点没人盯着,要么靠人肉定闹钟爬起来敲命令,要么靠脑子硬记"明天要记得把那批调度任务清掉"。用了Saltstack之后,这个痛点算是彻底解决了。任务管理这一块,本质上就是把"什么时候执行、在哪执行、执行什么、执行完怎么办"这套逻辑,全部交给Saltstack去编排,我只需要把规则写清楚。

这篇教程是Saltstack入门到精通系列的第八篇,也是我认为从"会用Saltstack"到"用好Saltstack"最关键的一步。前面几篇讲了配置管理、pillar、grains、state这些东西,但它们更像是"存量资源的描述"。任务管理解决的是"增量动作的执行"——什么时候触发、怎么排队、怎么做多机协同。这篇文章我会从最常用的cron定时任务讲起,再到一次性延迟任务、多机编排,最后聊几个我实际踩过的坑,尽量把思路捋清楚。

1. 从"被闹钟支配"到"被任务表支配":Saltstack任务管理到底管什么

先说个概念上的梳理,不然很容易把几个东西混在一起。Saltstack里跟"任务"相关的组件实际上有四个层面:

第一个是执行模块(Execution Modules),这就是你平时在命令行里敲的salt '*' cmd.run 'uptime'、salt '*' pkg.install vim这类即时命令。它解决的是"我现在就要做一件事",没有计划性,跑完就结束。

第二个是状态模块(State Modules),也就是state.sls文件里描述的东西,解决的是"我希望目标机器最终处于什么状态"。它本身不是任务,但它可以被任务系统调用。比如你写了一个ntp.sls,描述"必须安装ntp、必须启动ntpd",那这个sls就可以作为一个"任务包"被定时调度。

第三个是调度器(Scheduler),也就是schedule配置,可以配置在master上,也可以配置在minion上。它解决的是"到了什么时间点,自动去跑某个任务"。这是任务管理的核心执行引擎。

第四个是编排(Orchestrate),用salt-run state.orchestrate来跑,解决的是"多个minion之间、多个状态之间,谁先谁后、失败怎么办"这种多主机协同问题。

我用一个类比给你理清楚:执行模块是"手动挡",你想动就动一下;状态模块是"自动驾驶",它只管最终到达什么状态;调度器是"闹钟",到点就叫你去干某件事;编排是"总指挥",它告诉所有车按什么顺序走、哪个路口先放行。

这一篇的重点会放在调度器和编排上,因为这两个才是真正的"任务管理"。状态模块和执行模块只是被任务系统拿来用的工具,前面几篇已经讲了不少。

为什么任务管理这么重要?因为运维工作里大部分事故,都出在"人忘了执行"或者"执行顺序错了"。比如你改了防火墙规则,但你忘了30分钟后有个批量任务要跑;再比如你给一批机器升级了内核,但你忘了另一个编排任务还在等着重启服务。靠脑子记这些,迟早出事。把任务交给系统去按计划执行,出问题才有据可查。

2. 定时任务怎么配:Saltstack的cron状态模块实战

2.1 最简单的cron.sls长什么样

大多数人的定时任务需求,用Saltstack的cron状态就能搞定。先看一个最基础的例子。假设我要在所有的web服务器上,每天凌晨3点清理一次临时目录,我会这么写:

# /srv/salt/clean-tmp.sls clean-tmp-job: cron.present: - name: find /tmp -type f -mtime +3 -exec rm -f {} \; - hour: 3 - minute: 0

然后执行salt '*' state.apply clean-tmp,Saltstack会在每台minion的crontab里自动加一行。

这里有几个细节我要特别强调:

name字段是完整命令,不是注释。很多人第一次写会以为name只是给任务起个名字,实际上它是真正要执行的命令行。如果你写过crontab,就知道crontab里只能写一条命令,这里同理。

hour和minute是必填的。不写的话,Saltstack会使用默认值*,也就是每分钟跑一次。我第一次用的时候就没注意,结果一个清理脚本每分钟跑一次,跑了半小时,把临时目录删了个干净,顺带把一个服务运行需要的临时socket文件也删了,线上告警一片。

更稳妥的写法是明确指定day、month这些参数,不要嫌啰嗦:

clean-tmp-job: cron.present: - name: find /tmp -type f -mtime +3 -exec rm -f {} \; - minute: 0 - hour: 3 - day: '*' - month: '*' - weekday: '*'

2.2 环境变量是隐形杀手

有个问题很坑:通过Saltstack配置的cron任务,执行时的PATH跟你在终端里手动执行命令时的PATH完全不一样。crontab默认的PATH极其精简,通常只有/usr/bin:/bin。

所以你在命令行里能跑通的命令,比如python3、/usr/local/bin/python3,到了cron里可能直接报command not found。

解决办法有两个,二选一:

方案A:在命令里写绝对路径。比如:

clean-tmp-job: cron.present: - name: /usr/local/bin/find /tmp -type f -mtime +3 -exec /usr/bin/rm -f {} \;

方案B:在cron条目里显式定义PATH。Saltstack的cron状态支持env参数:

clean-tmp-job: cron.present: - name: find /tmp -type f -mtime +3 -exec rm -f {} \; - hour: 3 - minute: 0 - env: - PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

我个人的习惯是优先用方案B,因为一个任务可能要调用好几个命令,每个都写绝对路径太啰嗦,而且容易漏。在env里把PATH设置好,下面随便写命令名就行。

2.3 删除定时任务比你想的要小心

删cron的写法是这样的:

clean-tmp-job: cron.absent: - name: find /tmp -type f -mtime +3 -exec rm -f {} \;

注意,cron.absent匹配的是"命令内容",不是"你用cron.present时写的那个名字"。所以你要删除某个定时任务,必须把之前cron.present里的完整命令原封不动抄过来。用Jinja渲染的时候要特别小心,别让变量在渲染前后产生变化,否则absent会匹配不上。

还有个更隐蔽的坑:如果你在一个sls里同时写了cron.present和cron.absent,比如一个sls文件里包含多个机器的任务配置,Saltstack会按顺序执行。如果后面某个逻辑被触发去absent一个不存在的任务,它不会报错,只是返回unchanged。这个特性可以用来做"覆盖式清理",但要注意别把本该存在的任务误删了。

2.4 通过Jinja管理多环境的cron差异

到了多环境场景,cron的写法就很有讲究了。比如生产环境的清理任务是凌晨3点,测试环境的清理任务是凌晨5点。写死一个时间是不行的,你也不想复制粘贴两个sls文件。更好的做法是结合pillar:

# /srv/salt/clean-tmp.sls {% set clean_hour = pillar.get('clean_tmp_hour', 3) %} {% set clean_minute = pillar.get('clean_tmp_minute', 0) %} clean-tmp-job: cron.present: - name: find /tmp -type f -mtime +3 -exec rm -f {} \; - hour: {{ clean_hour }} - minute: {{ clean_minute }}

然后在pillar里为不同环境设置不同的值。这样一套sls走天下,不同环境只有pillar的差异。任务管理最怕的就是"每个环境的配置散落在不同地方",用pillar统一管理是根治之道。

3. 一次性任务和延迟任务:不用盯着时钟等执行

3.1 场景:5分钟后我要跑一个脚本,但不想手动等

有时候你不需要一个循环往复的cron任务,你只是想"5分钟后执行一次这个脚本"。Saltstack的schedule可以做到,而且可以做到只在内存里生效,不写进crontab。

配置方式很直接,写在minion配置文件里,或者写在state里下发。我还是推荐写在state里下发,这样方便版本管理。比如:

# /srv/salt/delayed-clean.sls schedule-delayed-clean: schedule.present: - name: delayed_clean - function: cmd.run - args: 'find /tmp -type f -mtime +3 -exec rm -f {} \;' - when: 03:30 - once: True

这里有几个参数必须讲清楚:

function是执行模块的函数名,比如cmd.run、test.ping、state.apply。args是传给这个函数的参数。如果你想调用state.apply,还要用kwarg传参,比如:

schedule-run-clean-tmp: schedule.present: - name: clean_tmp_state - function: state.apply - args: clean-tmp - when: 04:00 - once: True

when是指定当天的某个时间点执行,格式是HH:MM。once: True是关键,表示这个任务只执行一次,执行完之后,Saltstack会把它标记为完成。不过注意,这个"一次"是相对于minion进程生命周期而言的。如果minion重启了,这个schedule任务还留在配置里,可能再次触发。所以真正的"一次性任务",我建议跑完之后用schedule.disable或者schedule.delete主动清掉,别只依赖once。

3.2 schedule任务的闭环清理

做一个真正的一次性任务,我习惯这么写,两步:

第一步,配置里先声明:

# /srv/salt/oneoff-check.sls schedule-oneoff: schedule.present: - name: oneoff_check - function: cmd.run - args: /usr/local/bin/check_disk.sh - when: 23:00 - once: True

第二步,在脚本或者另一个state里,跑完之后调用清理:

salt-call schedule.delete oneoff_check

为什么非要加这一步?因为我遇到过minion意外重启,然后这个"一次性"任务在新进程起来之后又跑了一遍,导致重复执行的问题。加了schedule.delete之后,管你进程怎么重启,任务表里压根就没有这条,自然也就不会触发。

3.3 schedule放在master还是minion?

这个选择很多人纠结。我的结论是:

  • 放在minion:每个minion自己管自己的schedule,适合"跟业务相关的任务",比如每台机器上的清理任务、日志轮转。即使master挂了,minion上的任务照样跑,故障域隔离得比较好。

  • 放在master:master统一管理所有minion的任务,适合"跟全局相关的任务",比如批量升级、批量重启。master感知的是全局状态,可以统一调控。

个别场景需要在master上配置schedule,然后通过state.apply让所有minion去执行。但说实话,我更多是在/etc/salt/master.d/schedule.conf里配置那种"master自己发起的runner任务",而不是minion任务。绝大多数minion任务,我都放在minion端或者通过state下发。

4. 任务编排(Orchestrate):让一批机器按你的剧本行动

4.1 什么是Orchestrate,它解决了什么

假设我要做一次全量更新:先关停应用层服务,再更新依赖包,然后重启服务,最后验证健康检查。这是有严格顺序的。

如果我用salt '*' state.apply一把梭,所有机器同时执行,那应用层服务可能还没停止,依赖包就被替换了,跑出来的结果完全不可控。这时候就需要编排。

Saltstack的编排是通过master上的state.orchestraterunner来执行的。它和普通state的区别在于:普通state是minion自己执行自己那份sls;编排是在master上,由master统一下发指令,控制一批minion执行不同的sls。

看一个最简单的编排例子:

# /srv/salt/orch/rolling-update.sls stop-app: salt.state: - tgt: 'web-app' - tgt_type: grain - sls: app.service.stop update-packages: salt.state: - tgt: 'web-app' - tgt_type: grain - sls: app.packages.update - require: - salt: stop-app start-app: salt.state: - tgt: 'web-app' - tgt_type: grain - sls: app.service.start - require: - salt: update-packages health-check: salt.function: - tgt: 'monitor-server' - tgt_type: grain - name: cmd.run - arg: - curl -f http://web-app/healthz - require: - salt: start-app

执行方式:

salt-run state.orchestrate orch.rolling-update

这个编排文件里,salt.state表示"调用某个minion组去执行某个state",salt.function表示"调用某个minion组去执行某个执行模块"。require的语义和state里一模一样,就是"前置依赖"。

4.2 编排的执行顺序和失败控制

编排执行时,同级的salt.state默认是并行还是串行的?答案是:在同一个编排sls里,同级的state默认并行执行,只有用require、watch这些requisite建立依赖关系,才会等待前一个完成再继续。

所以你在写编排的时候,一定要显式写清楚依赖。我见过不少新手写编排,每个salt.state都没有requisite,结果一跑,所有机器同时乱套,然后还回头怀疑Saltstack编排是不是不生效。不是不生效,是你没告诉它按什么顺序走。

失败处理是另一个大坑。state.orchestrate默认在某个state失败后,不会停止整个编排,而是继续跑后面的。这在依赖关系清晰的时候问题不大,因为前一个失败了,后一个require前一个,所以后一个也会失败退出。但如果有些步骤是独立执行的,它失败了你可能没注意到。

想要"一个任务失败就完全终止"的效果,有两个选择:

  1. 在state.orchestrate里加-o参数,比如salt-run state.orchestrate orch.rolling-update -o failhard,遇到第一个失败就直接停止。
  2. 在编排sls里,把所有后续步骤都用require链挂在第一个任务下面。这是更细的控制,但写起来稍麻烦。

我推荐的做法是:核心的关键步骤用failhard;非关键的可选步骤,比如"更新监控探针"这种即使失败也不影响主干的任务,就不挂require,让它们失败也能过去。

4.3 把前一个任务的结果传给下一个任务

编排里经常要用到"上一个任务的返回值"。比如你先获取了某台机器的IP列表,然后要把它传给后面的部署任务。Saltstack的state.orchestrate里可以用conf.get这种runner来读取前一个的任务结果。

实际开发中,我更习惯用salt-run的另一个利器——runner模块手动编排,而不只是依赖state写死。比如:

salt-run state.orchestrate orch.get_ips salt-run state.orchestrate orch.deploy_app

前半段先跑完拿IP,后半段直接用之前写好的state文件,结合pillar把IP列表传进去。这种"分段编排"的做法虽然多了一步手动触发,但排查问题的时候特别清晰,每个人都能看明白到底卡在哪一步。

4.4 编排里的目标定位:tgt和tgt_type

编排里的tgt和tgt_type用来指定目标机器,tgt_type支持glob、grain、pillar等。我的建议是:尽量用grain或者pillar作为目标匹配维度,少用主机名glob。因为主机名容易随扩容变化,而"这台机器是不是web-app角色"这种信息,通过grain或pillar表达更稳定。

举个例子:

deploy-code: salt.state: - tgt: 'role:web' - tgt_type: grain - sls: app.deploy

这样不管web节点怎么扩容缩容,只要新节点打上了role: web这个grain,编排就能自动覆盖到它。任务管理如果只能管理存量机器,价值就少了一大半,能自动适应增量机器才是真本事。

5. 状态模块里的任务管理细节:触发器与条件控制

5.1 watch和onchanges:你改配置,我才执行动作

任务管理不只是"定时跑",还包括"被特定事件触发后去跑特定动作"。Saltstack的state里有几个触发机制,我实际用得最多的是watch和onchanges。

watch和require最大的区别是:require只看"前一个任务是否成功执行",而watch还多看一层"前一个任务是否发生了变化"。如果前一个任务的执行结果是unchanged(没有实际改动),那么watch后面的任务不会执行。

这个语义非常有用。举一个典型场景:

# /srv/salt/nginx.sls nginx-install: pkg.installed: - name: nginx nginx-config: file.managed: - name: /etc/nginx/nginx.conf - source: salt://nginx/files/nginx.conf - require: - pkg: nginx-install nginx-reload: service.running: - name: nginx - watch: - file: nginx-config

这里nginx-reload这个service任务,只有nginx-config这个文件发生了变化(比如内容更新了),才会重启nginx。如果配置文件没变,watch就不触发reload。这就是"配置驱动重载"的核心机制。

5.2 如果你想"无论变没变,都要跑一次"怎么办

有一种情况很尴尬:你确实希望每次执行都触发一个动作,哪怕文件内容没变。比如你写了一个脚本,希望每次salt '*' state.apply都跑一次。那你不要用watch,直接用cmd.run的默认行为即可。

我遇到过一个需求:"每次apply这个state,都要把当前机器的磁盘信息上报一次"。这种可以直接写:

report-disk: cmd.run: - name: /usr/local/bin/report_disk_usage.sh

没有任何requisite,所以每次apply都会执行。这时候反而要注意一点:cmd.run默认只有在命令返回值变化时才认为状态发生了变化。如果你在脚本里没有输出,cmd.run可能会认为没有变化而跳过。解决办法是加一句输出:

#!/bin/bash echo "report disk usage at $(date)" df -h

只要命令有stdout输出,Saltstack就会认为状态发生了改变。这是个很实用的小技巧,不然你会发现"任务根本没执行啊",但实际上是执行了,只是结果判定为unchanged被忽略掉了。

5.3 test=True:任务管理最重要的安全阀

不管你的任务是定时任务还是编排任务,在下发之前,一定要先跑一遍test模式。Saltstack的state支持salt '*' state.apply clean-tmp test=True,这个东西会模拟整个执行过程,显示每个步骤"如果真正执行,会做什么",但不会真正改动系统。

在任务管理里,test=True还有一个额外好处:它可以校验你的cron配置、schedule配置、watch依赖关系是否解析正常。如果Jinja渲染有错误、requisite引用了不存在的state ID,test=True都会直接报错,不会等到真正执行时才炸出来。

我自己写任何新的sls,铁打不动的流程是:

  1. 先state.apply xxx test=True,看返回的预执行结果。
  2. 再state.apply xxx,正式执行。
  3. 再看一遍返回的最终结果,确认有没有意外。

6. 自定义执行模块与任务结果处理

6.1 写一个自己的执行模块

任务管理到后面,必然会出现一些"官网模块满足不了"的需求。比如你要封装一个复杂的部署脚本,不想把它写成一长串命令行。这时候就需要自定义执行模块。

在Saltstack里,自定义执行模块放在salt://_modules/目录下。你写一个Python文件,比如deploy_tools.py:

# /srv/salt/_modules/deploy_tools.py def ping_services(service_list): ret = {} for svc in service_list: ret[svc] = __salt__['service.status'](svc) return ret def backup_mysql(db_name): ret = __salt__['cmd.run_all']( 'mysqldump --single-transaction -u root {} > /backup/{}.sql'.format(db_name, db_name) ) if ret['retcode'] != 0: return {'success': False, 'message': ret['stderr']} return {'success': True, 'message': 'backup done'}

写完存好之后,还要做一步"同步"操作:

salt '*' saltutil.sync_modules

同步之后,就可以直接当普通执行模块用了:

salt '*' deploy_tools.backup_mysql mydb

如果这个模块要在state里被引用,也是直接写deploy_tools.backup_mysql即可。模块里的__salt__字典是Saltstack给每个执行模块内置的"胶水引用",可以直接调用其他模块的函数,比如cmd.run_all、service.status。这个机制非常方便,相当于你是站在Saltstack所有官方模块的肩膀上写自己的业务逻辑。

6.2 任务执行后的结果收集

任务跑完了,结果怎么看?命令行直接返回是一回事,但如果任务是在凌晨3点由schedule自动触发的,你总不可能凌晨3点爬起来盯着终端看。

三个思路,可以组合使用:

第一个思路:用returner把结果送回数据库或消息队列。Saltstack的returner模块支持把执行结果发到MySQL、Redis、MongoDB等。比如我可以写一个简单的returner,把结果写到某个数据库表:

# /srv/salt/_returners/db_returner.py def returner(ret): # 假设有配置好的数据库连接方式 conn = __salt__['config.get']('db_conn') # 把ret['return']的结果写入数据库 # ret 里面包含 id, fun, return, success, jid 等字段 pass

配置好并sync_returners后,执行任务时加--return db_returner,结果就会自动入库。

第二个思路:用salt-run的runner。在master上执行salt-run jobs.lookup_jid 201612312359000000,可以查询某个历史任务的详细返回。

第三个思路:也是最简单的,就是让任务结果输出到一个文件。我经常在cron/schedule的命令里追加一句>> /var/log/mytask.log 2>&1,然后在master上用salt '*' cmd.run 'tail -n 100 /var/log/mytask.log'去批量查看。

这个做法虽然土,但排查效率极高——所有机器的结果都在一个固定的地方,直接grep就行。很多时候运维不必非要搞得多花哨,能最快定位问题的方式就是好方式。

6.3 任务失败的通知机制

任务跑失败了,得让人知道。我的做法是通过cmd.run返回码判断,配合一个"失败才发消息"的执行模块。比如写一个自定义模块:

def notify_slack(message): # 调用企业微信/钉钉/邮件 webhook __salt__['http.query']( os.environ.get('ALERT_URL'), method='POST', data=json.dumps({'text': message}) ) return True

然后每次schedule任务,我在命令后面加上"如果非0退出码,就调通知"。但更推荐的做法是把通知逻辑也写进自定义执行模块里,把"业务逻辑"和"告警逻辑"放在一起:

def run_and_alert(cmd, alert_group): ret = __salt__['cmd.run_all'](cmd, python_shell=True) if ret['retcode'] != 0: __salt__['alert.notify']('task failed on {}: {}'.format(__grains__['id'], ret['stderr'])) return ret

这样不管你是手动执行、cron调度、schedule调度,还是编排调用,失败之后都会自动走告警通道,不用额外维护一套告警规则。

7. 几个高频坑:我踩过之后总结的排错经验

7.1 坑一:cron任务执行了,但命令本身报错,crontab里看不到日志

先说现象:你配置了cron任务,crontab -l 能看到,时间到了它也确实跑了,但你在系统日志里看不到任何输出。

原因是:cron默认把输出通过邮件发走,如果机器没装邮件系统,输出就直接丢了。尤其是cron.present默认不会给你加日志重定向。

解决办法很简单,在cron.present的命令后面显式加重定向:

clean-tmp-job: cron.present: - name: echo "clean-tmp start $(date)" >> /var/log/clean-tmp.log 2>&1 && find /tmp -type f -mtime +3 -exec rm -f {} \; >> /var/log/clean-tmp.log 2>&1

日志是一个运维人员的命根子。任务跑了没、跑成功没、输出是什么,全得靠日志,别指望系统帮你保留。

7.2 坑二:重名任务导致Jinja渲染后的cron出现重复

这是一个我印象很深的坑。我有两个sls,因为命名不规范,都用sync-cert-job这个名字。结果两个sls同时apply到一台机器上,Saltstack在渲染crontab时发现两条完全不同的命令,但名字一样。它不会报警,而是把后渲染的覆盖掉先渲染的。最后我的证书同步任务莫名消失,排查半天才发现是两个sls里的ID冲突了。

在Saltstack里,state的ID是全局标识,一个ID只能对应一个state声明。尤其是cron这种写外部的任务,ID冲突导致的结果就是"先建的任务被后建的悄悄替代"。所以命名一定要加上区分业务含义的前缀,比如:

sync-cert-from-vault-job: cron.present: ...

不要用sync-cert-job这种到处都可能重复的通用名字。

7.3 坑三:schedule任务在minion重启后没了

如果schedule配置不是写在minion配置文件里,而是通过salt '*' state.apply schedule_config这种方式下发,那minion重启之后,这个schedule任务会不会丢?

答案是:要看配置来源是否持久化。如果schedule写在/etc/salt/minion.d/*.conf里,minion启动时会重新加载;如果只是通过salt-call schedule.present设置的内存态,minion重启后就不在了。

为了稳妥,我给所有schedule任务都配套了一个state文件,作为持久化配置源:

# /srv/salt/schedule/custom.sls schedule-custom-jobs: schedule.present: - name: disk_clean - function: state.apply - args: disk_clean - when: 02:30

这样即使minion重启,只要执行过一次state.apply schedule/custom,任务就在;如果新装了一台minion,初始部署时也会把这个state带上,schedule就自动建立。运维的可靠性从来不是靠"运行时手动敲一下"来保证的,而是靠"声明式配置+重复执行不会错"来保证的。

7.4 坑四:编排跑了半天,发现第一步就错了还继续往下跑

这个问题我在前面提过一嘴,但因为它太重要,值得单独拿出来讲。

我在早期做批量升级的时候,编排文件里第一步是"停止应用",第二步是"备份数据",第三步才是"升级包"。当时第一步stop-app由于目标匹配错误,实际上根本没有匹配到任何机器,但它返回的是unchanged这种中性状态,不是失败。结果后续的备份、升级全部在应用还在运行的情况下执行,备份出来的数据完全不可用。我盯着编排结果看了半天,还觉得每一步都"成功"了,直到第二天业务方反馈数据有问题才反应过来。

这里的根因有两个:一是目标匹配写错导致空跑,二是编排状态里unchanged不等于正常,需要人工确认。我现在会在编排第一步后面加一个检查步骤,专门确认目标机器数量符合预期:

check-target-count: salt.function: - tgt: 'role:web' - tgt_type: grain - name: test.ping - failhard: True

test.ping如果匹配不到机器,会返回失败。再用failhard: True把这个检查设成关键节点,一旦没有目标机器,整个编排立刻终止,避免后面白白执行一堆无效操作。这个习惯帮我挡住过好几次事故。

8. 我总结的一套任务管理写法和流程

最后梳理一下我目前最常用、也最稳定的一套任务管理实践,给你做一个参考。

任务分三类管理:

  1. 周期性任务(比如清理临时文件、日志轮转、证书检查)——用cron.present写进sls,配合pillar管理时间和开关。
  2. 一次性延迟任务(比如"半小时后重启服务")——用schedule.present显式配once: True,并在任务里加清理逻辑,避免重启后重复触发。
  3. 多机协同任务(比如滚动更新、批量重启、跨机器依赖操作)——用state.orchestrate,写清楚requisite依赖,关键步骤加failhard。

每个任务在写完之后,强制走四步检查:

  • 第一步:state.apply <task> test=True,确认渲染结果和执行计划正确。
  • 第二步:检查目标匹配是否正确,比如state.apply <task> test=True返回的机器列表是不是你预期的那几台。
  • 第三步:正式执行,观察返回结果里的success和comment字段。
  • 第四步:确认日志文件是否产生了预期内容,没有就去看系统日志和盐的日志。

任务命名上,我用的是三段式:业务名-动作名-类型。比如nginx-config-reload-job、mysql-backup-schedule、app-rolling-update-orch。命名清晰的好处是,当你需要cron.absent或者排查任务冲突时,一眼就能从ID里分辨出职责。

环境变量和安全,永远不要依赖任务运行环境自带的东西,能写绝对路径就写绝对路径,需要在任务里用的密钥通过pillar传,别硬编码在sls里。

备份先行,凡是涉及数据迁移、服务重启、包升级这类有破坏性的任务,我会在编排文件里强制加一个备份步骤,并且用require把后面所有任务都挂在备份步骤之下。宁可多花几分钟备份,也不要让整批机器陷入不可回滚的境地。

Saltstack的任务管理,归根到底做的是一件"从人到机器"的信任转移:把执行计划写清楚、验证充分、交给系统去跑。定期检查任务是必要的,但这份工作留给人的应该是指挥和决策,而不是时刻盯着屏幕等结果。有了这套体系,我能把精力放在更重要的业务架构上,也建议你从手边最简单的定时清理任务开始,先跑通一个完整的闭环,慢慢再把编排、告警、结果收集都加进来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询