Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Infrastructure as Code By James Joyner IV · · 9 min read

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=True shows 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/salt and /srv/pillar in 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.

Free download · 368-page PDF

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.