§ B9 · Track B · pravo

Drift: find and reconcile changes

Change a resource outside pravo, find the change with pravo drift, and reconcile it in both directions.

Time
12 min
Level
intermediate
Needs
B8
You will learn
What is drift? How do you find it, and how do you reconcile it?
Done when
pravo drift exits 2 after a file is deleted and exits 0 after the specification and the world agree again.

View as Markdown

On this page

A colleague deletes a file, a bucket or a rule from the console. Which command tells you, and how soon?

Drift is a difference between the state and the real world that no pravo command caused. OpenTofu reports it with a refresh-only plan. pravo has a command for it. A plan reads the resources it reaches and the resources they read. It reads a resource only when its record is older than the refresh age. This lesson shows how that can hide a deletion.

OpenTofu
tofu plan -refresh-only -detailed-exitcode
pravo
pravo drift

pravo drift reads the real state of every resource, whatever its refresh age, and reports the ones that changed. This lesson causes drift with a shell and reads it. No cloud account is involved: the resources are files.

specification state world pravo plan the specification against the state pravo drift the state against the world, exit 2
fig. 1 Plan compares the specification with the state. Drift compares the state with the world.

Set a refresh age

Work in an empty directory. A plan reads a resource from its provider only when it looks at the resource, which means that it reaches it or reaches a resource that reads it, and its record is older than the refresh age. The default is zero, so a plan reads every resource it looks at. This root sets one hour. A longer refresh age makes a plan read fewer records and trust older ones.

main.cue
package main

import (
	"pravo.dev/pravo"
	loc "pravo.dev/providers/hashicorp/local"
)

$settings: pravo.#Settings & {refresh: "1h"}

$local: loc.#Provider

motd: $local.#Resource.local_file & {
	filename: "motd.txt"
	content:  "welcome\n"
}

banner: $local.#Resource.local_file & {
	filename: "banner.txt"
	content:  "release 1\n"
}
terminal
$pravo init
$pravo apply --auto-approve
$pravo drift
Configuration does not load yet; generating schemas for the declared providers first.
Created cue.mod/module.cue
Providers:
  local = hashicorp/local v2.9.1
...
pravo has been initialised. Run "pravo plan" to see what it would do.
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <txn>]
No drift. 2 resources refreshed; 0 pending changes from the specification.

No drift and exit code 0. The state and the files agree.

Break the world

Delete motd.txt, as someone with a shell does.

terminal
$rm motd.txt

The records are a few seconds old and the refresh age is one hour.

Predict Does pravo plan notice that the file is gone?

No. The plan trusts a record that is within the refresh age and does not ask the provider.

terminal
$pravo plan
No changes. Your infrastructure matches the configuration.
Graph: planned 0 of 2, 2 unchanged and skipped, 2 within refresh age.

The Graph: line says 2 within refresh age. Neither record was read.

--refresh=all reads every resource, whatever its age. The other values are age, the default, expired and false. expired reads every record that is past its age, and false reads nothing.

terminal
$pravo plan --refresh=all
...
motd: no longer exists; it will be created again
...
  # motd (local_file) will be created
  + motd: {
  +     content:              "welcome\n"
...
    }

Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 2, 1 unchanged and skipped, 2 refreshed, 1 drifted.

The plan creates motd, and the Graph: line counts one drifted resource. pravo drift always reads everything and answers with an exit code.

terminalexit 2
$pravo drift
Drift. 1 of 2 resources changed outside pravo:
  motd: create
    - refresh found the real state differs from the state (drift)

Exit code 2 means drift was found. Exit code 0 means none. A CI job reads the code and the JSON.

terminalexit 2
$pravo drift --json
{
  "time": "<time>",
  "root": ".",
  "drifted": [
    {
      "addr": "motd",
      "action": "create",
      "reasons": [
        "refresh found the real state differs from the state (drift)"
      ],
      "gone": true
    }
  ],
  "pending_changes": 1,
  "summary": {
    "add": 1,
    "change": 0,
    "destroy": 0,
    "read": 0,
    "blast_radius": 1
  }
}

gone means the provider found no object. pending_changes is the number of changes in the plan.

Reconcile: restore

The specification is right and the world is wrong. pravo drift left a mark on motd: the record says that the file is gone, and the plan acts on that without reading the file again.

terminal
$pravo plan | grep -E '^(Plan|Graph)'
$pravo apply --auto-approve | tail -n 1
Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 2, 1 unchanged and skipped, 2 within refresh age.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]
terminal
$pravo drift
No drift. 2 resources refreshed; 0 pending changes from the specification.

Before pravo drift read the world, the same apply would have printed No changes: the records said that the file exists, and they were young. The mark is what makes the plan act. It stays until an apply writes the resource.

Not every change is drift

Drift is what the provider reports when it reads the resource. The local provider reads whether the file exists and what it contains. It does not read the file mode.

terminal
$chmod 600 banner.txt
$pravo drift
No drift. 2 resources refreshed; 0 pending changes from the specification.

A change that the provider cannot see is not drift. That includes this one.

Exercise

Exercise

Reconcile by accepting

Someone rewrote banner.txt. Make the specification say what the file says, and bring the state in line without losing the edit.

the edit
echo "release 2" > banner.txt
Hint 1

Run pravo drift first. The provider treats a file whose content differs as gone, so the report says create.

Hint 2

Change content of banner to the new text. Then apply with --refresh=all, and run pravo drift again.

Solution
terminal
$echo "release 2" > banner.txt
main.cue
package main

import (
	"pravo.dev/pravo"
	loc "pravo.dev/providers/hashicorp/local"
)

$settings: pravo.#Settings & {refresh: "1h"}

$local: loc.#Provider

motd: $local.#Resource.local_file & {
	filename: "motd.txt"
	content:  "welcome\n"
}

banner: $local.#Resource.local_file & {
	filename: "banner.txt"
	content:  "release 2\n"
}
terminal
$pravo apply --refresh=all --auto-approve
$cat banner.txt
...
banner: no longer exists; it will be created again
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]
release 2

Both reconciliations end the same way: the specification, the state and the world agree, and pravo drift exits 0.

Clean up

terminal
$pravo destroy --auto-approve | tail -n 1
Destroy complete! Resources: 0 added, 0 changed, 2 destroyed. [txn <txn>]

Recap

  • Drift is a change to a resource that no pravo command made. pravo drift reads every resource and exits 2 when it finds one.
  • A plan reads the resources it reaches, and the resources they read, when their records are older than the refresh age. --refresh=expired reads every record that is past its age, and --refresh=all reads everything.
  • A read that finds drift leaves a mark on the record. The next plan, saved plan and apply act on it until an apply writes the resource.
  • pravo drift --due --limit N reads the oldest records that are past their refresh age, so a scheduled job covers a large estate in turns.
  • Reconcile by applying the specification, or by changing the specification to match the world.

Check yourself

What exit codes does pravo drift return?

0 when it finds no drift and 2 when at least one resource changed outside pravo.

Why does pravo plan print No changes after a file is deleted?

The plan does not reach the resource, and the record is within the refresh age, so the plan does not read the file.

The guide CI shows how a pipeline reads these exit codes. B10 turns to rules about what a plan may do.