Skip to content

Plan, apply, drift

Sadame has three commands that work on the same comparison: for every resource in the recipe, read the current state from the machine, compare it with the desired state, and report the difference. plan shows the difference, apply removes it, and drift tells you, through its exit status, whether there is any difference left. Used together they form a loop: plan a change, apply it, and later check that the machine has not wandered away from the recipe.

flowchart LR
    recipe["recipe + vars"] --> plan["sadame plan<br/>review the diff"]
    plan --> apply["sadame apply<br/>change what differs"]
    apply --> converged(("machine<br/>matches recipe"))
    converged -->|"later: hand edits,<br/>upgrades, accidents"| drift["sadame drift<br/>in cron or CI"]
    drift -->|"exit 0"| converged
    drift -->|"exit 1: drift found"| plan

When to use which

Command Changes the machine Use it to Exit status
plan no review what a recipe or var change would do before applying it 0 whether or not there are changes
apply yes converge the machine: change only what differs, then run notifications 0 when everything succeeded, 1 when something failed
drift no check, from a script, cron job or CI pipeline, that the machine still matches 0 no difference, 1 difference found

All three exit with 2 when the command line or the recipe is wrong. plan and drift also exit with 2 when they could not read the state of some resource, because they then cannot tell whether it matches:

Status plan apply drift
0 plan computed every resource and notification succeeded machine matches the recipe
1 not used a resource or notification failed machine differs from the recipe
2 usage or recipe error, or a state could not be read usage or recipe error usage or recipe error, or a state could not be read

plan and drift produce the same report; only the summary line and the exit status differ. Both accept --json (see the JSON output reference).

The loop on a real machine

The example recipe installs nginx, adds a system user, writes a configuration file and keeps nginx running:

examples/10-drift-demo/recipe.rb
# A small server to watch drift on: a package, a system user, a config file
# and a service. Paths are Debian and Ubuntu's.
package "nginx"

user "demo-app", system: true, home: "/var/lib/demo-app", shell: "/usr/sbin/nologin"

directory "/etc/demo-app", mode: "0755"

file "/etc/demo-app/app.conf",
     template: "app.conf.erb",
     owner: "root",
     group: "demo-app",
     mode: "0640"

service "nginx", enable: true, state: :running

Apply

$ sudo sadame apply examples/10-drift-demo
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

changed    package[nginx]
           state: absent -> installed
changed    user[demo-app]
           state: absent -> present
           home: /var/lib/demo-app
           shell: /usr/sbin/nologin
changed    directory[/etc/demo-app]
           state: absent -> present
           mode: 0755
changed    file[/etc/demo-app/app.conf]
           state: absent -> present
           mode: 0640
           owner: root
           group: demo-app
           --- /dev/null
           +++ /etc/demo-app/app.conf (desired)
           @@ -0,0 +1,4 @@
           +# Managed by sadame. Local changes will be overwritten.
           +listen    = 127.0.0.1:9000
           +workers   = 4
           +log_level = info
changed    service[nginx]
           state: stopped -> running

Summary
  changed      5  package[nginx]
                  user[demo-app]
                  directory[/etc/demo-app]
                  file[/etc/demo-app/app.conf]
                  service[nginx]
  unchanged    0
  failed       0
  skipped      0
  notified     0

Finished in 3.94s.

Drift right after apply

$ sudo sadame drift examples/10-drift-demo
sadame 0.5.1 drift on local (Debian GNU/Linux 12 (bookworm))

No drift: 5 resources match the recipe.

$ echo $?
0

Something changes the machine

break-things.sh does what happens to servers over time: someone edits a setting by hand, loosens a file's permissions while debugging, and stops a service.

examples/10-drift-demo/break-things.sh
#!/bin/sh
# Simulates what happens to servers over time: a hand edit, a permissions
# "fix" and a stopped service. Run it as root after `sadame apply`, then run
# `sadame drift` to see each change reported.
set -e

# Someone tunes a setting by hand.
sed -i 's/^workers   = .*/workers   = 16/' /etc/demo-app/app.conf

# Someone makes the file world-readable while debugging.
chmod 0644 /etc/demo-app/app.conf

# Someone stops nginx and forgets to start it again.
systemctl stop nginx

echo "break-things: edited app.conf, changed its mode, stopped nginx"
$ sudo sh examples/10-drift-demo/break-things.sh
break-things: edited app.conf, changed its mode, stopped nginx

Drift catches it

Three changes on two resources: the file's content and mode, and the service state. The exit status is 1.

$ sudo sadame drift examples/10-drift-demo
sadame 0.5.1 drift on local (Debian GNU/Linux 12 (bookworm))

~ file[/etc/demo-app/app.conf]
    mode: 0644 -> 0640
    --- /etc/demo-app/app.conf
    +++ /etc/demo-app/app.conf (desired)
    @@ -1,4 +1,4 @@
     # Managed by sadame. Local changes will be overwritten.
     listen    = 127.0.0.1:9000
    -workers   = 16
    +workers   = 4
     log_level = info

~ service[nginx]
    state: stopped -> running

Drift: 2 of 5 resources differ from the recipe.

$ echo $?
1

Apply puts it back

$ sudo sadame apply examples/10-drift-demo
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

unchanged  package[nginx]
unchanged  user[demo-app]
unchanged  directory[/etc/demo-app]
changed    file[/etc/demo-app/app.conf]
           mode: 0644 -> 0640
           --- /etc/demo-app/app.conf
           +++ /etc/demo-app/app.conf (desired)
           @@ -1,4 +1,4 @@
            # Managed by sadame. Local changes will be overwritten.
            listen    = 127.0.0.1:9000
           -workers   = 16
           +workers   = 4
            log_level = info
changed    service[nginx]
           state: stopped -> running

Summary
  changed      2  file[/etc/demo-app/app.conf]
                  service[nginx]
  unchanged    3
  failed       0
  skipped      0
  notified     0

Finished in 0.02s.

$ sudo sadame drift examples/10-drift-demo
sadame 0.5.1 drift on local (Debian GNU/Linux 12 (bookworm))

No drift: 5 resources match the recipe.

$ echo $?
0

What drift sees, and what it does not

Drift compares only what the recipe declares, attribute by attribute:

  • A file's content, mode, owner and group, but only those you set. A file declared without mode: can change mode without drift.
  • Packages, by name: an installed package of any version matches state: :installed.
  • Services: only enable: and state:, when given.
  • exec resources: the guard decides. An exec whose guard says it must run counts as drift, and an exec without any guard always does.
  • Anything not in the recipe is invisible: files nobody declared, extra packages, extra users.

Common mistakes

  • Unguarded exec resources. They make drift exit 1 forever. Give every exec a guard, or action: :nothing if it only runs when notified.
  • Running drift without root. Files only root can read cannot be compared; those resources fail and drift exits 2, not 1.
  • Treating plan's exit status as drift. plan exits 0 even when there are changes. Scripts that need a yes/no answer use drift.
  • Expecting drift to repair. drift never changes anything. Run apply to converge, after reviewing the diff.

See also