SaltStack States: Event-Driven Configuration Management at Scale
Salt's reputation is speed, but its real edge is the event bus and reactor. Here's how to write maintainable states and automate responses across thousands of nodes.
- #iac
- #saltstack
- #configuration-management
- #event-driven
- #automation
- #scale
SaltStack gets pigeonholed as “the fast one” — the config management tool you reach for when Ansible’s SSH-per-host model gets slow at thousands of nodes. The speed is real (Salt’s ZeroMQ pub/sub talks to a whole fleet in parallel), but it undersells the actual differentiator: Salt has an event bus, and on top of it a reactor that turns infrastructure into something event-driven rather than cron-driven. That’s the part worth learning.
Let me cover states done well, then the event system that makes Salt genuinely different.
States: the declarative core
A Salt state declares desired configuration in YAML (rendered through Jinja). The structure mirrors other config tools but the composition model is cleaner once you internalize it.
# /srv/salt/nginx/init.sls
nginx_installed:
pkg.installed:
- name: nginx
nginx_config:
file.managed:
- name: /etc/nginx/nginx.conf
- source: salt://nginx/files/nginx.conf.jinja
- template: jinja
- require:
- pkg: nginx_installed
nginx_running:
service.running:
- name: nginx
- enable: True
- watch:
- file: nginx_config # reload when config changes
Two relationships do most of the work: require enforces ordering (config only after the package), and watch triggers a reaction (reload the service when the file changes). Master those two and your states stay correct without manual ordering hacks.
Pillar: separating data from logic
The discipline that keeps Salt maintainable is pillar — secure, per-minion data kept out of your state logic. States describe how; pillar describes what for this host.
# /srv/pillar/web/init.sls
nginx:
worker_processes: 4
server_name: example.com
upstream_port: 8080
# referenced in a state's Jinja template
worker_processes {{ pillar['nginx']['worker_processes'] }};
server_name {{ pillar['nginx']['server_name'] }};
Pillar data is targeted to minions and never rendered onto hosts that shouldn’t see it — which is also why it’s the right home for secrets (especially with a GPG or Vault pillar backend). Keep states generic and reusable; push all the host-specific and environment-specific values into pillar.
The targeting system
Salt’s targeting is more expressive than most. You can act on minions by glob, grain (static facts like OS or role), pillar value, or compound expressions:
# Apply the nginx state to all web-role minions in prod
salt -C 'G@role:web and G@env:prod' state.apply nginx
# Run a command across every Ubuntu minion, in parallel
salt -G 'os:Ubuntu' cmd.run 'apt list --upgradable'
Grains-based targeting means you stop maintaining host lists — you target by what a machine is, and new machines that match are included automatically.
The event bus: where Salt pulls ahead
Every Salt minion and the master publish events onto a shared bus — job returns, minion start/stop, custom events your code fires. You can watch it live:
salt-run state.event pretty=True
This is infrastructure as a stream of events rather than a thing you poll. A minion came online, a state failed, a deploy finished — all are events you can react to. No other mainstream config tool ships this out of the box.
The reactor: closing the loop
The reactor subscribes to events and fires Salt actions in response — turning “something happened” into “do this automatically.”
# /etc/salt/master.d/reactor.conf
reactor:
- 'salt/minion/*/start':
- /srv/reactor/onboard.sls
# /srv/reactor/onboard.sls
apply_baseline:
local.state.apply:
- tgt: {{ data['id'] }}
- arg:
- baseline
Now when any new minion connects, the reactor automatically applies the baseline state — no human, no cron, no orchestration tool. This is the pattern that makes Salt feel like a control system: events drive configuration. Combine it with beacons (minion-side watchers that fire events on file changes, service failures, or load thresholds) and you get self-healing: a beacon detects nginx died, fires an event, the reactor restarts it.
Orchestration for multi-node sequences
For cross-host workflows (provision the DB, then the app, then the LB), use the orchestrate runner from the master:
# /srv/salt/orch/deploy.sls
deploy_database:
salt.state:
- tgt: 'role:db'
- tgt_type: grain
- sls: postgres
deploy_app:
salt.state:
- tgt: 'role:app'
- tgt_type: grain
- sls: myapp
- require:
- salt: deploy_database
This sequences states across machines with dependency ordering — the multi-node equivalent of require.
Where AI fits
Salt’s renderer stack (YAML + Jinja, with Python and other renderers available) is powerful but fiddly, and reactor/beacon configs are sparsely documented. I use an assistant to scaffold states with correct require/watch graphs, to write the Jinja that pulls from pillar, and to draft reactor SLS for a given event. Keep a few SaltStack prompts for generating states and reactor configs, then test with state.apply test=True (dry run) before applying for real.
Operating Salt sanely
- Always dry-run.
salt '*' state.apply mystate test=Trueshows what would change without changing it. - Keep states idempotent. Re-applying should be a no-op when already converged — that’s the whole point.
- Put secrets in encrypted pillar, never in states.
- Be careful with reactor loops. A reactor that triggers an event that re-triggers the reactor is a fun way to melt your master. Add guards.
- Version
/srv/saltand/srv/pillarin Git and deploy via GitFS or CI, not by editing on the master.
Salt’s parallelism gets it in the door at scale, but the event bus and reactor are why it stays. Once your infrastructure responds to events instead of waiting for the next cron tick, you’ve moved from configuration management to a genuine control loop. For more cross-tool IaC, see our Infrastructure as Code guides.
Generated Salt states and reactor configs are assistive, not authoritative. Always run with test=True against non-production minions before applying.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.