GitHub Copilot for OpenTofu
OpenTofu is a fork of Terraform, created in 2023 after HashiCorp relicensed Terraform from the Mozilla Public License to the Business Source License. It is now a Linux Foundation project, remains MPL-2.0, and has diverged enough that “the same thing with a different binary name” is no longer accurate.
That divergence is the reason this lesson exists separately, and it is also the main risk when generating OpenTofu with Copilot. The overwhelming majority of public HCL is Terraform. A model producing OpenTofu is producing Terraform unless your context says otherwise — which is fine for the eighty percent that is identical, and wrong in specific, checkable ways for the rest.
Key takeaways
- The CLI is
tofu. A suggestion containingterraform planin an OpenTofu repository is a signal the rest was drawn from Terraform material. - Unqualified provider sources resolve from
registry.opentofu.orgunder OpenTofu andregistry.terraform.iounder Terraform. Write the host explicitly. - State encryption is the feature with no Terraform equivalent. OpenTofu encrypts the state document itself; Terraform relies on the backend encrypting at rest.
- Version numbers are not comparable.
required_version = "~> 1.12"means OpenTofu 1.12, and OpenTofu 1.12 is not Terraform 1.12. - Everything in the Terraform lesson about
for_each, plans, drift and policy scanning applies unchanged. This lesson covers only what is different.
What is actually different
Four things, in descending order of how likely they are to affect a generated file.
The registry. source = "hashicorp/aws" is not a fully qualified address —
it is shorthand that each tool expands against its own default registry host.
OpenTofu expands it to registry.opentofu.org/hashicorp/aws. This usually works
because the OpenTofu registry mirrors the common providers, and it is worth being
explicit anyway: a fully qualified source makes the file unambiguous and removes
a class of “works on my machine” where a colleague has the other tool installed.
State encryption. Covered below. This is the one genuinely new capability.
The licence. OpenTofu is MPL-2.0; Terraform is BUSL-1.1. For most engineers this is not a technical matter, and for some organisations it is the entire reason the tool is in use. It also means the two projects’ providers can differ in availability — a provider whose licence permits distribution through one registry may not appear in the other.
Divergent features. The two projects now ship things the other does not, and the set changes. Rather than list a snapshot that will be wrong shortly, the useful habit is to check the OpenTofu documentation for anything you have not personally used in OpenTofu before, and to be suspicious of a generated feature you recognise from a Terraform release note.
State encryption
This is the feature worth learning OpenTofu for, and it addresses a genuine weakness that the Terraform lesson describes and does not solve.
Terraform state contains every attribute of every resource in plain text —
database passwords, generated keys, anything a provider returns. sensitive = true hides values from CLI output and does nothing to state. The standard advice
is to use a backend that encrypts at rest, which protects against someone
reading the storage and not against anyone who can read the bucket.
OpenTofu encrypts the state document itself. The bucket holds ciphertext.
terraform {
encryption {
key_provider "pbkdf2" "primary" {
passphrase = var.state_passphrase
}
method "aes_gcm" "primary" {
keys = key_provider.pbkdf2.primary
}
state {
method = method.aes_gcm.primary
}
plan {
method = method.aes_gcm.primary
}
}
}Three parts: a key provider that produces key material, a method that
uses it, and a target — state, plan, or both. Encrypting the plan matters
as much as the state, because a saved plan file contains the same values.
Practical project: encryption, demonstrated
Practical example
A configuration that runs its whole lifecycle with no cloud account
Prove state encryption works by applying, reading the state file, and failing to decrypt it with the wrong passphrase.
- Status
- Tested implementation
- Runtime
- OpenTofu v1.12.6, providers random and local only
- Command
tofu fmt -check && tofu init -backend=false && tofu validate && tofu plan && tofu apply && tofu destroy- Result
- fmt exit 0; validate: Success! The configuration is valid. plan: 2 to add, 0 to change, 0 to destroy. apply: 2 added. State contained "encrypted_data" with PBKDF2 at 600,000 iterations and SHA-512; the generated resource value was not present in plaintext. A wrong passphrase produced "cipher: message authentication failed". destroy: 2 destroyed.
- Run on
- August 21, 2026
Why this example has no cloud provider
Every other infrastructure example in this cluster stops at validate, because
plan needs credentials and apply creates real resources. This one uses the
random and local providers, which create a name and a file in the example
directory and touch nothing else — so the entire lifecycle can be executed
honestly, including apply and destroy.
That is the only way to actually demonstrate state encryption. A configuration you cannot apply produces no state to inspect.
Files
copilot-opentofu-demo/ ├── versions.tf required_providers with explicit registry hosts, │ plus the encryption block ├── variables.tf state_passphrase, marked sensitive ├── main.tf random_pet and local_file ├── outputs.tf └── .github/ └── instructions/ └── opentofu.instructions.md
The prompt
This repository uses OpenTofu, not Terraform. The CLI is tofu.
Create a configuration using only the random and local providers, with provider sources fully qualified against registry.opentofu.org.
Add an encryption block using the pbkdf2 key provider and the aes_gcm method, encrypting both state and plan. The passphrase comes from a variable marked sensitive, supplied via TF_VAR_. Never write a literal passphrase.
Then run tofu fmt, tofu init -backend=false, tofu validate and tofu plan, and show me the output.
Observed output
$ tofu fmt -check -recursive
(exit 0)
$ tofu init -backend=false
OpenTofu has been successfully initialized!
$ tofu validate
Success! The configuration is valid.
$ tofu plan
Plan: 2 to add, 0 to change, 0 to destroy.
$ tofu apply -auto-approve
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
release_name = "welcomed-mallard"What the state file contains
{
"serial": 1,
"lineage": "49e0602f-1f2a-8867-3bcb-029e328ce919",
"meta": {
"key_provider.pbkdf2.primary": "eyJzYWx0IjoiOUhBclFkMXhzNHdDdDQyZjNONXRIQm9lS…"
},
"encrypted_data": …The meta value decodes to the PBKDF2 parameters — 600,000 iterations, SHA-512,
a 32-byte key — and a salt. The resource data is in encrypted_data.
The check that matters:
$ tofu output -raw release_name
welcomed-mallard
$ grep -c "welcomed-mallard" terraform.tfstate
0The value is retrievable through the CLI, which has the passphrase, and is not present in the file.
With the wrong passphrase
$ TF_VAR_state_passphrase="the-wrong-passphrase" tofu output
Error: decryption failed for all provided methods
attempted decryption failed for state: decryption failed:
cipher: message authentication failedmessage authentication failed rather than “wrong password” is AES-GCM working
correctly — it authenticates the ciphertext, so a wrong key and a tampered file
produce the same error. That is the property you want.
Validation
Validate before deploying
OpenTofu: the local validation sequence
| Step | Command | Executed here |
|---|---|---|
| Format | tofu fmt -check -recursive | PASSExit 0; no files needed reformatting |
| Initialise without a backend | tofu init -backend=false | PASSInitialised; providers resolved from registry.opentofu.org |
| Validate | tofu validate | PASSSuccess! The configuration is valid. |
| Plan | tofu plan | PASSPlan: 2 to add, 0 to change, 0 to destroy — safe because the configuration uses only the random and local providers |
| Apply, then inspect the state | tofu apply && head -c 300 terraform.tfstate | PASSState contained "encrypted_data" with PBKDF2 at 600,000 iterations and SHA-512; the resource value was not recoverable in plaintext. A wrong passphrase gave "cipher: message authentication failed". |
Key providers beyond a passphrase
The PBKDF2 example above is the right shape for a demonstration and the wrong shape for production, and the difference is worth understanding because generated configuration will default to the simple one.
A passphrase has three problems at scale. It is a shared secret, so every engineer and every CI job that touches state holds the same one. Rotating it means re-encrypting state, which is a deliberate operation rather than a credential update. And revoking access from one person means rotating for everyone.
The alternative is a key management service as the key provider, which turns all three problems into IAM problems you already know how to solve. Access becomes a policy on a key rather than possession of a string; revocation is removing a principal; and rotation is a property of the key rather than an operation on your state.
Two further capabilities are worth knowing about because they change what a migration looks like.
A fallback method lets OpenTofu read state encrypted with an old key while writing with a new one, which is what makes key rotation possible without a flag-day re-encryption of every state file.
Encrypting an existing unencrypted state is a supported transition — you add the encryption block with an unencrypted fallback, run once so the state is rewritten encrypted, then remove the fallback. Getting the order wrong locks you out of your own state, so it is worth reading the current documentation rather than accepting a generated sequence.
Running both tools in one organisation
Plenty of organisations end up with both, usually because a migration is partial or because different teams decided differently. Two practical points.
A repository should use one. Mixed repositories are where the registry-host
ambiguity becomes a real failure: a configuration that resolves providers from
the OpenTofu registry under tofu and the Terraform registry under terraform
can produce different provider versions from the same lock file. Fully qualified
sources make it explicit; a .tool-versions or equivalent makes the CLI
unambiguous.
Say which one in the repository instructions. This is the single most
effective thing you can do about generated code in a mixed environment. A model
reading .github/copilot-instructions.md that says “this repository uses
OpenTofu; the CLI is tofu” produces the right commands; one reading nothing
produces Terraform.
For CI, pin the tool version explicitly rather than using whatever a setup action resolves to. The two projects release on different schedules, and a floating version is a change to your infrastructure tooling that nobody reviewed.
Where Copilot helps specifically with OpenTofu
Most of the value is identical to Terraform and covered there. Three things are particular to this tool.
Explaining the difference. “What does this Terraform configuration need in order to run under OpenTofu?” is a well-posed reading question with a checkable answer, and it is more reliable than trying to remember the divergence yourself.
Writing the encryption block. It is a small, structured piece of configuration with a documented shape, three interlocking parts, and a syntax nobody memorises — which is exactly the profile of a task worth delegating. Check the passphrase source and the targets, and the rest is mechanical.
Producing the migration checklist. As in the prompt above: the value is the
enumeration, not the execution. A model listing every place required_version,
provider sources, backend configuration and CI invocations need to change is
doing useful work, and each item is independently verifiable.
Where it is weakest is anything version-specific. OpenTofu is younger than Terraform and its release notes are a much smaller share of public code, so claims about which OpenTofu version introduced what are the claims most worth checking against the documentation rather than accepting.
Migrating, and what to check in generated migration advice
Migration from Terraform to OpenTofu is well documented and mechanically
straightforward for recent Terraform versions — the state format is compatible,
so the path is install tofu, run tofu init, and continue.
Generated migration advice is where to be careful, because it tends to be optimistic in three specific ways.
Version compatibility is bounded. OpenTofu forked from a particular Terraform version and state written by a much later Terraform may not be readable. Check the current compatibility statement rather than assuming.
required_version constraints have to change. A configuration pinned to
~> 1.9 meaning Terraform 1.9 will be evaluated against OpenTofu’s version
number, which is a different sequence. Suggestions frequently leave the
constraint alone.
Provider availability is not guaranteed to be identical. Most providers are
in both registries. A niche or vendor-specific one may not be, and the failure is
at init rather than something you can see in review.
The safe shape for a migration prompt:
We are moving this configuration from Terraform to OpenTofu.
List what has to change, file by file, and for each item say how I verify it worked.
Include: required_version, provider source addresses, backend configuration, and any provider that may not exist in the OpenTofu registry.
Do not propose running anything that writes state. I will run tofu init and tofu plan myself and compare the plan to the Terraform one.
The last paragraph is the actual migration test: the plan should be empty. If switching binaries produces a non-empty plan, something is being interpreted differently and that is what to investigate before applying anything.
OpenTofu-specific risks
Generated Terraform in an OpenTofu repository. The default failure. tofu
versus terraform in commands, and registry addresses that resolve to the other
host.
A literal passphrase in the encryption block. Defeats the feature. Grep for
passphrase = followed by anything that is not var..
Losing the key material. Unrecoverable. This risk is created by adopting encryption and it is worth stating explicitly when you do.
Version constraint confusion. required_version compared against the wrong
project’s numbering.
Assuming feature parity in either direction. Both projects ship things the other does not. A generated configuration using a feature you have not used in this tool warrants a documentation check.
Everything else — for_each versus count, hard-coded credentials, wildcard
IAM, unread plans, state in Git — is identical to Terraform and is covered in
that lesson. Policy scanners including
Checkov read OpenTofu’s HCL without special handling, so the scanning step is
unchanged.
Destructive commands
One OpenTofu-specific addition: a destroy on a configuration with encryption
enabled and no passphrase available fails before it can do anything, because it
cannot read state. That is an accidental safety property rather than a designed
one, and it is not a control to rely on.
Review workflow
- Check the toolHuman judgementDoes the suggestion say tofu or terraform? Are provider sources fully qualified against registry.opentofu.org?
- tofu fmt -recursiveSame as Terraform. Removes style from the diff.
- tofu init -backend=false && tofu validateType-checks against the provider schema with no credentials.
- Check the encryption blockHuman judgementIs the passphrase a variable? Is `plan` encrypted as well as `state`?
- Run a policy scanCheckov reads OpenTofu HCL unchanged.
- Read the plan — destroys and replacements firstHuman judgement
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
Best practices
- Say “OpenTofu, not Terraform” in your repository instructions, and write it once rather than correcting it in every review.
- Fully qualify provider sources with the registry host.
- Enable state encryption, and encrypt
planas well asstate. - Take the passphrase from
TF_VAR_or a key management service, never a file. - Store the passphrase where your break-glass credentials live; losing it is unrecoverable.
- Verify a migration by confirming the first
tofu planis empty.
Common mistakes
- Accepting a
terraformcommand in an OpenTofu repository because it looks right. - Encrypting state and leaving the plan file unencrypted.
- Putting the passphrase in
terraform.tfvarsand committing it. - Assuming version numbers are comparable between the two projects.
- Migrating without a state backup and without checking the first plan is empty.
Where to go next
GitHub Copilot for Terraform covers
everything the two tools share, which is most of it — plans, for_each, modules,
drift and the policy scan. GitHub Copilot for AWS
covers the cloud side either tool provisions.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.
Was this lesson helpful?
Your answer is stored in this browser and is not sent anywhere.