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:
# 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.
#!/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:andstate:, when given. execresources: the guard decides. Anexecwhose guard says it must run counts as drift, and anexecwithout any guard always does.- Anything not in the recipe is invisible: files nobody declared, extra packages, extra users.
Common mistakes¶
- Unguarded
execresources. They makedriftexit1forever. Give everyexeca guard, oraction: :nothingif it only runs when notified. - Running drift without root. Files only root can read cannot be
compared; those resources fail and
driftexits2, not1. - Treating
plan's exit status as drift.planexits0even when there are changes. Scripts that need a yes/no answer usedrift. - Expecting drift to repair.
driftnever changes anything. Runapplyto converge, after reviewing the diff.
See also¶
- Drift in CI: run
drifton a schedule. - JSON output: the report format.
- Notifications: what
applyruns after a change.