A study in declarative infrastructure

The declarative layer, reimagined.

pravo is an open study of what OpenTofu and Terraform's declarative layer could be if it started today: state, plans, policy and language rebuilt from a clean slate, on the providers that already exist. It is built from ten years of running them.

12 minutes · no account · no cloud · Apache 2.0

main.cuevalid
package estate

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

$local: loc.#Provider

#Service: {
	name: string
	port: int & >=1024 & <=65535
	file: $local.#Resource.local_file & {
		filename: "\(name).env"
		content:  "PORT=\(port)\n"
	}
}

api: #Service & {name: "api", port: 8080, file: file_permission: "0600"}
web: #Service & {name: "web", port: 8081}
terminal
$pravo plan
pravo will perform the following actions:

  # api.file (local_file) will be created
  + api: file: {
...
  +     file_permission:      "0600"
  +     filename:             "api.env"
...
Plan: 2 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 2.

One definition, two instances and the plan for both.

Kept, revisited, tried

The providers stay. The foundation is new.

OpenTofu and Terraform got the providers and the plan-and-apply loop right. Where ten years of operating them point elsewhere, pravo starts from a clean slate: the state, the plan, the policy and the language.

Kept

What worked

  • Every OpenTofu and Terraform provider, unchanged.
  • Declarative desired state, not a program that runs.
  • A plan you read before anything changes.
Revisited

What ten years of operating it taught

  • State is one record per resource, not one file.
  • A plan reaches what a change can affect and skips the rest.
  • Locks are per resource, so applies on different resources run at once.
  • A refresh age says when to read real state again.
  • Policy is part of the tool, named by whoever runs it.
  • Providers ship as OCI images, with a hash for each platform.
  • The plan is a document that people, CI and agents read alike.
Tried

A different footing

  • CUE, a constraint language, in place of markup with imperative extras.
  • Modules are values: set any field of one, and the types still check.
  • Errors before any provider starts.

Plans and applies

Change one resource. Read one resource.

OpenTofu reads every resource in the state on every plan. pravo starts at your change, follows the recorded dependencies and reads what the change reaches. These are its default settings.

OpenTofu

resources read from their providers
2001
tofu plan
Plan: 1 to add, 0 to change, 1 to destroy.

pravo

resources read from their providers
1
pravo plan
Plan: 1 to add, 0 to change, 1 to destroy.
Graph: planned 1 of 2001, 2000 unchanged and skipped, 1 refreshed, 2000 not read.

The same estate written for each tool: local files in one state, applied, then one value changed. pravo ran with its default settings. Both plans propose the same change. In the Graph line, refreshed means read from the provider and not read means left alone.

Why it matters

Reach

Short plans, short applies

Each record names the resources it depends on. A plan starts at your change and follows those dependencies for as long as a value changes. It looks at what the change can touch and leaves the rest alone. You review a plan about your change, and the apply locks and writes only that.

What a plan reaches
Refresh age

A staleness budget

Each record remembers when its provider was last asked, and the refresh age says how stale it may be before it is read again. A plan that looks at a record within its age does not ask again, so repeated plans cost fewer calls. A resource can set its own age as a standing promise, and a scheduled pravo drift --due reads the records that are past theirs, oldest first, for a cost you cap. The default age is zero: what a plan looks at is read.

Choosing a refresh age
Both together

Calls follow your change

Reach decides what a plan looks at. The refresh age decides whether what it looks at is read, and how overdue a record must be before a scheduled sweep reads it. The calls to your providers follow the change and the ages you chose, not the number of resources in the state.

Cost follows the change
For a team

One state for a large estate

Split an estate by ownership, permissions or blast radius, not to make plans fast. People and CI can apply to different parts of one state at the same time, because an apply locks only the resources it writes; an overlapping apply is refused, not queued. Drift is checked on a schedule with pravo drift, not on every plan.

When to split a state

The trade-off A resource that a plan does not reach is not read, so drift in it is found by pravo drift or by a plan that reaches it.

What grows with the estate pravo evaluates the whole specification and opens every record on each run, it reads every data source unless a refresh age is set, and a change that reaches every resource costs what the estate costs.

Take it for a spin

Run it, bring your own stack, or learn CUE first.

Download

Download pravo

Binaries for macOS, Linux and Windows are on the releases page.

Or install from a terminal

$curl -fsSL https://pravo.systems/install.sh | sh

About

Why I built this.

I'm Marcin Wyszynski. I co-founded Spacelift and OpenTofu, and I had been exploring CUE for a while. Ten years of running Terraform and OpenTofu left me with notes on what I would do differently, and one day I decided to throw some tokens at the problem and see what gives.

pravo is the result. The notes are mine, and the code was written with Claude from them. It is a study: it drives real OpenTofu providers and its behaviour is documented and tested, but nobody runs it for real yet. It is a non-commercial side project, not a product or a company, released under Apache 2.0 for anyone to use, change and build on. Take it for a spin and tell me what you found.