§ Migrate

Migrate from OpenTofu or Terraform

A gated path from an OpenTofu or Terraform project to pravo, with the promise it keeps, what it applies to, and three ways to go.

View as Markdown

On this page

You run OpenTofu or Terraform today. pravo drives the same providers, so your infrastructure stays where it is. What changes is the specification, which you write in CUE, and the state, which pravo keeps in its own format.

pravo reads an OpenTofu or Terraform state file once and converts it. Then it plans against the real objects. When the translation is right, the plan has nothing to do. A plan that has something to do means the CUE does not yet describe the object, and nothing is applied until it does. That empty plan is the gate.

The OpenTofu file below ran once, and OpenTofu wrote the state file that the terminal converts and plans. The CUE file is the specification that the terminal plans.

main.tfnot run
terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

resource "random_pet" "server" {
  length = 2
}

resource "local_file" "motd" {
  filename = "motd.txt"
  content  = "serving ${random_pet.server.id}\n"
}

resource "local_file" "note" {
  for_each = toset(["alpha", "beta"])
  filename = "notes/${each.key}.txt"
  content  = "${each.key}\n"
}
stack.cuevalid
package estate

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

$random: rnd.#Provider & {$version: "~> 3.6"}
$local:  loc.#Provider & {$version: "~> 2.5"}

server: $random.#Resource.random_pet & {
	$from:  "random_pet.server"
	length: 2
}

motd: $local.#Resource.local_file & {
	$from:    "local_file.motd"
	filename: "motd.txt"
	content:  "serving \(server.id)\n"
}

note: {
	for k in ["alpha", "beta"] {
		"\(k)": $local.#Resource.local_file & {
			$from:    "local_file.note_\(k)"
			filename: "notes/\(k).txt"
			content:  "\(k)\n"
		}
	}
}
stack.cue names the old address of each resource with $from.

fig. 5a The same stack in both languages. OpenTofu 1.13.1 applied main.tf once; the terminal does not run it. stack.cue is the specification that the terminal plans, and pravo validate accepts it: 4 resources, 2 providers.

terminalvalid
$pravo state import-tfstate terraform.tfstate
Imported 4 resources and 0 outputs from terraform.tfstate into ./pravo.state

Renamed instances (these are the addresses now in the state; point each at its place in the specification with $from):
  local_file.note["alpha"] -> local_file.note_alpha
  local_file.note["beta"] -> local_file.note_beta
$pravo plan
pravo will perform the following actions:

  # random_pet.server was moved to server (record renamed; no change to the object)

  # local_file.motd was moved to motd (record renamed; no change to the object)

  # local_file.note_alpha was moved to note.alpha (record renamed; no change to the object)

  # local_file.note_beta was moved to note.beta (record renamed; no change to the object)

Plan: 0 to add, 0 to change, 0 to destroy.
Graph: planned 4 of 4, 4 refreshed, 4 moved.
$pravo plan
No changes. Your infrastructure matches the configuration.
Graph: planned 4 of 4, 4 refreshed.

fig. 5b The state file in format 4 that OpenTofu 1.13.1 wrote for main.tf, converted and planned. The output is a recording of pravo 0.1.0-dev run on it. After $from moved the 4 records, the second plan has nothing to do.

What stays

  • The providers The same OpenTofu providers, plugin protocol 5 and 6.
  • The infrastructure The objects are read, not recreated.
  • Review before apply A saved plan is read, then applied.

What changes

  • The language CUE replaces HCL. pravo has no converter for HCL.
  • The state format The directory pravo.state replaces terraform.tfstate.
  • The commands pravo replaces tofu.
  • Not carried over Taint, deposed objects and the records of the built-in provider. See M10.

The runbook is a sequence of gates. Each gate ends with a check that prints something you can read. Start the next page when the check passes.

The promise

Every step before the first pravo apply with changes leaves your infrastructure and your OpenTofu state as they are.

  • pravo reads the state file you name, with pravo state import-tfstate. It never writes a .tfstate file or an OpenTofu backend.
  • Before an apply, pravo calls only the read side of a provider: schema, validation, configuration, upgrade, read, import, plan, data sources, ephemeral resources and functions. It never calls the operation that creates, updates or deletes an object.
  • pravo writes pravo.state (or the bucket that $settings names), pravo.lock.cue and cue.mod in each root. It writes the provider cache in ~/.pravo (PRAVO_HOME). It writes a plan file when you name one with --out.

A plan writes to pravo.state: it refreshes records, carries out $from moves and $import declarations there, and adds an entry to the transaction log when it wrote anything. Those writes change pravo's records and nothing else.

Two limits belong to the promise.

  • A read is the provider's own code. A data source that runs a program runs it under pravo, as it does under OpenTofu. pravo cannot know what a provider does when it reads.
  • The promise holds while the pravo state path is not the OpenTofu state file. Leave the path at its default, the directory pravo.state.

pravo.state holds the attributes of every resource, including the values a provider marks sensitive, in plain files. pravo creates the files with mode 0600 and the directories with mode 0700. Keep the directory out of version control and out of reach of people who cannot read the OpenTofu state.

M3 shows the promise as numbers: checksums of the state file and of the objects, taken before the first command and after the last.

The runbook

Page You What it changes To undo
M1 Before you start Empty the OpenTofu plan, back up the state, scan for decisions, write the recovery plan. Nothing in the project. Nothing to undo.
M2 Concept map, M3 Worked example Read what each construct becomes, and watch one stack move. Nothing. Nothing to undo.
M4 Translate the specification Write CUE next to the HCL, one construct at a time. New .cue files, cue.mod and pravo.lock.cue. Delete them.
M5 Convert the state Convert a copy of the state, point each record at its address, plan until the plan is empty. pravo.state. Delete the directory.
M6 Rewrite the checks on plans Turn the CI checks that read the plan JSON into plan policies. Policy files. Delete them.
M7 Cut over Remove OpenTofu's claim on the objects, move the pipeline, archive the old state. The OpenTofu state and the pipeline. M8, after the cut over.
M8 Roll back Rehearse the way back before you need it. A copy only. Not applicable.
M9 Pitfalls, M10 Not in pravo Look up a message, or a feature that has no counterpart. Nothing. Not applicable.
M11 After the migration Rewrite the faithful translation in the idiom of CUE. The specification. Revert the commit.

The root is frozen from M1 to M7: nothing applies to it with OpenTofu, and nothing merges into it. A change that lands in between sends you back to M5.

One rule governs M4 to M6: apply nothing that creates, changes, replaces or destroys. A plan that shows one means the specification does not yet describe the object. Fix the specification and plan again.

Until the first pravo apply that changes an object, going back means deleting the pravo files. After it, the OpenTofu state is out of date, and going back means making it agree with the objects. M8 rehearses the first case and lays out the second.

Applies to

Supported
pravo and CUE pravo 0.1, CUE 0.17
Tools that wrote the state OpenTofu, and Terraform 0.12 or newer: any tool that writes state format 4
State format read 4. Any other format and legacy flat attributes are refused with an error.
Where the old state lives Any backend. Write it to a file with tofu state pull > FILE.
Workspaces One state for each workspace. Convert each one into its own pravo state: M5.
Provider plugin protocol 5 and 6
Provider source The OpenTofu registry. A state address on registry.terraform.io is read as the OpenTofu registry. Other registries: providers from a registry.
pravo state A local directory or an S3-compatible bucket

Choose a path

Translate and convert

Recommended. pravo manages every object of the root, and you remove OpenTofu's claim at cut-over.

Use when you want one tool per root and you are willing to translate the HCL by hand.

Translate the specification

Import selected resources

pravo manages the objects you import with $import or pravo import. OpenTofu keeps the rest.

Use when you move a few resources, or you add new resources in pravo beside an existing root.

Import and move

Side by side per root

Each root has one tool, and you move one root at a time.

Use when your roots are independent. Roots that read each other's outputs move together, the publisher first.

Roots that share outputs

pravo has no converter for HCL. You translate by hand, and the pages tell you how.

Start with Before you start. The concept map lists what each construct, command and function becomes. The worked example runs the whole path on one stack.