§ 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.
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.
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"
}
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"
}
}
}
$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.
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 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.
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.statereplacesterraform.tfstate. - The commands
pravoreplacestofu. - Not carried over Taint, deposed objects and the records of the built-in provider. See M10.
Six steps, one gate
An empty plan opens step 5- 01Back upMake the OpenTofu plan empty. Copy the state file and the code.
- 02TranslateWrite the specification in CUE.
pravo validatechecks it offline against the provider schemas. - 03Convert state
pravo state import-tfstatereads format 4.$frompoints each record at its address. - 04Plan is empty
pravo planprintsNo changes.
Do not apply a plan that replaces or destroys.the gate - 05ApplyThe first apply gives each record its configuration hash. A small change proves read and write.
- 06Cut overRemove OpenTofu's claim on the objects, move the pipeline, archive the old state.
Every step before the first pravo apply with changes leaves your infrastructure and your OpenTofu state as they are. A read is the provider's own code. The promise
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.tfstatefile 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$settingsnames),pravo.lock.cueandcue.modin 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.
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.
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.
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.