§ B7 · Track B · pravo

Changing live infrastructure

Save a plan, apply exactly that plan or nothing, and apply it in stages. Read the refusal when the plan is stale.

Time
15 min
Level
intermediate
Needs
B6
You will learn
How do you apply exactly the plan that was reviewed? What happens when the world moves before you apply it?
Done when
A saved plan applies in two stages, and a plan whose files have changed is refused with exit code 3.

View as Markdown

On this page

Two questions about a change to live infrastructure. Does the apply run exactly the plan that was reviewed? Can part of the plan go out today and the rest tomorrow?

OpenTofu answers the second with -target, which narrows the plan when it is made. pravo has no -target. It saves the whole plan and applies a selection of that file.

OpenTofu
tofu plan -target=aws_db_instance.main -out=tfplan
tofu apply tfplan
pravo
pravo plan --out change.plan
pravo apply change.plan --only db

What runs is always part of a plan that was reviewed as a whole. This lesson saves a plan, applies it in stages and reads the refusals.

Save a plan

Work in an empty directory. This root has a release number and a manifest file that reads it. The env tag from B4 names the file.

main.cue
package main

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

$local:  loc.#Provider
$random: rnd.#Provider

env: *"dev" | "prod" @tag(env)

release: $random.#Resource.random_integer & {min: 1, max: 99, seed: "v1"}
manifest: $local.#Resource.local_file & {
	filename: "manifest-\(env).txt"
	content:  "release \(release.result)\n"
}
terminal
$pravo init
$pravo apply --auto-approve
Configuration does not load yet; generating schemas for the declared providers first.
Created cue.mod/module.cue
Providers:
  local = hashicorp/local v2.9.1
  random = hashicorp/random v3.9.1
...
pravo has been initialised. Run "pravo plan" to see what it would do.
...
Plan: 2 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 2.
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <txn>]

Change the seed. The release number changes, and the manifest that reads it changes with it.

main.cue
package main

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

$local:  loc.#Provider
$random: rnd.#Provider

env: *"dev" | "prod" @tag(env)

release: $random.#Resource.random_integer & {min: 1, max: 99, seed: "v2"}
manifest: $local.#Resource.local_file & {
	filename: "manifest-\(env).txt"
	content:  "release \(release.result)\n"
}

--out writes the plan to a file instead of only printing it.

terminal
$pravo plan --out change.plan
...
pravo will perform the following actions:

  # release (random_integer) must be replaced
-/+ release: {
  ~     id:      "15" -> (known after apply)
        max:     99
        min:     1
  ~     result:  15 -> (known after apply)
  ~     seed:    "v1" -> "v2" # forces replacement
    }

  # manifest (local_file) must be replaced
-/+ manifest: {
  ~     content:              "release 15\n" -> (known after apply) # forces replacement
...
    }

Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 2, 2 refreshed.

Saved the plan to change.plan. To apply exactly these actions, run:
  pravo apply change.plan

The file is the plan. pravo show prints it again with the time and the command that made it.

terminal
$pravo show change.plan
Saved plan created <time> by "pravo plan --out change.plan"

pravo will perform the following actions:

  # release (random_integer) must be replaced
-/+ release: {
  ~     id:      "15" -> (known after apply)
        max:     99
        min:     1
  ~     result:  15 -> (known after apply)
  ~     seed:    "v1" -> "v2" # forces replacement
    }
...
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 2, 2 refreshed.

The plan carries the tags it was made with, so a tag cannot change at apply time.

terminalexit 1
$pravo apply change.plan -t env=prod
Error: -t and -T cannot be combined with a saved plan; the plan carries its own tags

A plan applies only to what it was made for

The plan records a fingerprint of the files it was made from. Decide whether a comment counts.

Predict You add a comment to main.cue after saving the plan. Does pravo apply change.plan still run?

No. The fingerprint covers every file that was loaded for the root, so a comment is a change. The run is refused with exit code 3 and nothing is applied.

terminal
$echo "// a comment" >> main.cue
terminalexit 3
$pravo apply change.plan
Error: saved plan is stale; run "pravo plan" again:
  - the configuration changed since the plan was made

The reason is listed under the error. pravo show prints the same reason as a warning above the plan. Use it to check a plan before you apply it.

terminal
$pravo show change.plan | head -n 2
Warning: this plan can no longer be applied: the configuration changed since the plan was made
Saved plan created <time> by "pravo plan --out change.plan"

Take the comment out again. The plan is valid when the files are the files it was made from.

terminal
$grep -v '^// a comment$' main.cue > main.next && mv main.next main.cue
$pravo show change.plan | head -n 2
Saved plan created <time> by "pravo plan --out change.plan"

The fingerprint also covers packages elsewhere in the module that the root imports. A package the root does not import has no effect on it.

Apply in stages

You can apply a part of the plan. The part must be closed: it must include everything it depends on that is still pending. The manifest reads the release, so the manifest alone is refused.

terminalexit 1
$pravo apply change.plan --only manifest
Error: the selection is not closed under dependencies; it also needs release
  (the smallest closed selection is -only manifest,release)

The message gives the smallest selection that works. Nothing ran. Select the release.

terminal
$pravo apply change.plan --only release
Applying 1 of 2 remaining changes from change.plan.
release: Destroying... [id=15]
release: Creating replacement...
release: Replacement complete after 0s [id=24]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <txn>]
1 change remains in change.plan; apply with "pravo apply change.plan".

The release is replaced and the manifest is not. The plan file keeps the rest, and pravo show marks the part that is applied.

terminal
$pravo show change.plan
Saved plan created <time> by "pravo plan --out change.plan"

pravo will perform the following actions:

  # release (random_integer) must be replaced (already applied in txn <txn>)
-/+ release: {
...
    }

  # manifest (local_file) must be replaced
...
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 2, 2 refreshed.

Check the new release. The manifest still holds the old number.

terminal
$cat manifest-dev.txt
$pravo state show release | grep -o 'result:.*'
release 15
result:  24

Review whatever you need. Then apply what remains.

terminal
$pravo apply change.plan
$cat manifest-dev.txt
manifest: Destroying... [id=5fc55c580ff743f43f93b9139b115b7bbfe8610c]
manifest: Creating replacement...
manifest: Replacement complete after 0s [id=9a8ecf4b7af5fb7483b46efde91bb596bfaa94a6]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed. [txn <txn>]
release 24

Each resource is planned again by its provider immediately before it is applied. A provider that plans something different at that point stops the apply.

Exercise

Exercise

Let the world move

Add a second file to the root and save a plan for it. Then apply the specification without the plan, the way a colleague applies it. Apply your saved plan and read the refusal.

audit
audit: $local.#Resource.local_file & {
	filename: "audit-\(env).txt"
	content:  "audit of release \(release.result)\n"
}
Hint 1

Save the plan with pravo plan --out audit.plan. Apply the specification with pravo apply --auto-approve. The plan file is not an argument of that command.

Hint 2

The refusal names a record, the revision it has now, and the revision the plan expected. A record that did not exist when the plan was made has revision 0.

Solution
main.cue
package main

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

$local:  loc.#Provider
$random: rnd.#Provider

env: *"dev" | "prod" @tag(env)

release: $random.#Resource.random_integer & {min: 1, max: 99, seed: "v2"}
manifest: $local.#Resource.local_file & {
	filename: "manifest-\(env).txt"
	content:  "release \(release.result)\n"
}

audit: $local.#Resource.local_file & {
	filename: "audit-\(env).txt"
	content:  "audit of release \(release.result)\n"
}
terminal
$pravo plan --out audit.plan
...
  # audit (local_file) will be created
  + audit: {
  +     content:              "audit of release 24\n"
...
    }

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

Saved the plan to audit.plan. To apply exactly these actions, run:
  pravo apply audit.plan
terminal
$pravo apply --auto-approve
...
Plan: 1 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 3, 2 unchanged and skipped, 1 refreshed, 1 not read.
...
audit: Creating...
audit: Creation complete after 0s [id=3fe13eb17d7ee300fd5f4871245bcc6e47abed5a]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed. [txn <txn>]
terminalexit 3
$pravo apply audit.plan
Error: saved plan is stale; run "pravo plan" again:
  - audit changed in state (revision 1, plan expected 0)

The first apply created audit. The saved plan expected to create it, found it, and refused. Plan again to get a plan that matches the world.

Clean up

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

If you added audit in the exercise, the summary says 3.

Recap

  • A saved plan is a file. pravo apply <file> runs exactly its changes or refuses.
  • A plan is refused, with exit code 3, when the files or a record it touches have changed.
  • --only applies a closed part of a plan and leaves the rest in the file.

Check yourself

Why is --only manifest refused when the release is pending?

The manifest reads the release. A selection must include every pending change it depends on.

Which exit code means a saved plan is stale?

Exit code 3. The error lists each reason.

Next, B8 changes what an address means.