GitHub Copilot for OpenTofu

GitHub Copilot for DevOps & InfrastructureAcademy lesson 44Cluster 4 · Lesson 6 of 13Intermediate12 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot for OpenTofuGitHub Copilot for DevOps & Infrastructure6Intermediate/github-copilot/devops/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.

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 targetstate, 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

examples/cluster-4/opentofu

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

Copilot promptGenerate the configurationCopilot Chat

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
0

The 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 failed

message 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

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:

Copilot promptMigration, with the checks attachedCopilot Chat

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

Accepting generated OpenTofu
  1. Check the toolHuman judgementDoes the suggestion say tofu or terraform? Are provider sources fully qualified against registry.opentofu.org?
  2. tofu fmt -recursiveSame as Terraform. Removes style from the diff.
  3. tofu init -backend=false && tofu validateType-checks against the provider schema with no credentials.
  4. Check the encryption blockHuman judgementIs the passphrase a variable? Is `plan` encrypted as well as `state`?
  5. Run a policy scanCheckov reads OpenTofu HCL unchanged.
  6. 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 plan as well as state.
  • 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 plan is empty.

Common mistakes

  • Accepting a terraform command in an OpenTofu repository because it looks right.
  • Encrypting state and leaving the plan file unencrypted.
  • Putting the passphrase in terraform.tfvars and 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.

Primary sources