opentofu

Activity

Stars and mentions · Last 30 days, Oct 5 – Nov 3

GitHub stars257
Mentions9
06121824Oct 5Oct 19Nov 3
UTC days · repositories opentofu owns11 repositories

Top repositories

opentofu’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside opentofu’s account where opentofu opened or reviewed pull requests in the past 365 days.

Who noticed opentofu

Everyone who starred or forked opentofu’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

9posts
Nov 40 posts
  1. OpenTofu or Terraform: What to Pick for Existing Code Originally published on kuryzhev.cloud --- <p>The question of OpenTofu or Terraform tends to arrive on a quiet Tuesday. A repo with 200 resources, a remote state in S3 and a CI pipeline pinned to <code>terraform</code> works fine, until someone asks whether the next upgrade should be to the other tool. Nothing is broken, so the real question is whether switching buys anything that justifies the risk.</p> <p>The timing is worth noting. Both projects shipped new minor cycles recently: OpenTofu 1.13 (latest patch 1.13.5, cycle released 2026-09-30, <a href="github.com/opentofu/opentofu/releases">release notes</a>) and Terraform 1.17 (latest patch 1.17.1, cycle released 2026-10-21, <a href="github.com/hashicorp/terraform/releases">release notes</a>). Anyone planning a quarterly upgrade has two fresh targets. The two projects follow independent roadmaps, so check each changelog for features that exist in only one of them.</p> <p><strong>Short answer:</strong> if your code is plain AWS/GCP/Azure/Kubernetes resources and you have no contract with HCP Terraform, you can usually stay on Terraform 1.17 or move to OpenTofu 1.13 at low risk, so choose based on licensing and features. If you depend on HCP Terraform, Stacks or other HashiCorp-hosted services, stay on Terraform. If you want client-side state encryption or an open-governance licence, trial OpenTofu with the dry-run below before committing.</p> <h2>When the OpenTofu or Terraform choice actually matters</h2> <p class="devops-inline-image"></p> <p>For a small repo with a single backend, the choice is mostly cosmetic. It starts to matter in four situations:</p> <ul> <li><strong>Licensing review.</strong> Terraform releases after 1.5.x use the Business Source License; OpenTofu is MPL-2.0 under the Linux Foundation. Legal teams sometimes care, particularly if you ship tooling to customers. Read the current licence text yourself rather than relying on a blog summary.</li> <li><strong>State sensitivity.</strong> State holds secrets in plain text unless the backend encrypts it. OpenTofu has built-in state and plan encryption. Terraform relies on backend encryption and on its ephemeral and write-only mechanisms to keep values out of state. Check the Terraform docs for what your version supports.</li> <li><strong>Platform coupling.</strong> If your org runs workspaces, policies and run tasks in HCP Terraform or Terraform Enterprise, the tool and the platform are one decision.</li> <li><strong>Feature divergence.</strong> Since the 2023 fork the two projects have added different features and, in some cases, different syntax for similar ideas. Config that uses a tool-specific feature will not run on the other.</li> </ul> <p>Neither tool has a blanket advantage. Compare the changelogs for your specific version jump, not for the headline releases.</p> <h2>Option A: OpenTofu 1.13</h2> <p><strong>Pros</strong></p> <ul> <li>Open governance and an MPL-2.0 licence, which simplifies procurement and redistribution questions.</li> <li>Native state and plan encryption with pluggable key providers (including AWS KMS), configured in the <code>terraform</code> block. This matters when state lands in shared buckets.</li> <li>Features that landed after the fork, such as <code>for_each</code> on providers and the <code>-exclude</code> flag, are documented on <a href="opentofu.org">opentofu.org</a>. Confirm what is in 1.13 against its changelog.</li> <li>Same HCL, same provider protocol, same workflow. The binary is <code>tofu</code> and the commands match, so the CLI is familiar.</li> </ul> <p><strong>Cons</strong></p> <ul> <li>Migration becomes effectively one-way once you encrypt state or adopt OpenTofu-only syntax. Terraform cannot read encrypted state, so going back means decrypting with OpenTofu or reverting the config first.</li> <li>Provider and module sourcing goes through the OpenTofu registry. Your <code>.terraform.lock.hcl</code> entries will change, and any tooling hard-coded to <code>registry.terraform.io</code> needs a check.</li> <li>Third-party integrations (scanners, IDE plugins, CI templates) vary in how well they support <code>tofu</code>. Test yours rather than assuming.</li> <li>Commercial support comes from vendors rather than the original authors, which may or may not matter to your org.</li> </ul> <h2>Option B: Terraform 1.17</h2> <p><strong>Pros</strong></p> <ul> <li>Zero migration. Your pipeline, lock files, runbooks and team habits stay as they are, and upgrades inside the 1.x line follow the documented <a href="developer.hashicorp.com">upgrade guides</a>.</li> <li>First-party integration with HCP Terraform and Terraform Enterprise, including features that exist only there (Stacks, hosted policy and drift tooling). HashiCorp is now part of IBM, so vendor support contracts are a single conversation for many enterprises.</li> <li>Most vendor docs and provider examples still assume <code>terraform</code>, so troubleshooting is easier to search.</li> </ul> <p><strong>Cons</strong></p> <ul> <li>The BSL licence restricts offering a competing product. Most internal use is fine, but verify the terms against your situation, especially if you resell infrastructure automation.</li> <li>No built-in client-side state encryption. You depend on backend-side encryption (for example S3 with KMS) and on keeping secrets out of state in the first place.</li> <li>Roadmap and licence decisions sit with one vendor. That is a trade-off, not a defect, but it is a dependency you should name.</li> </ul> <h2>Decision matrix</h2> <table> <thead><tr><th>Situation</th><th>Lean toward</th><th>Why</th></tr></thead> <tbody> <tr><td>HCP Terraform / Enterprise in daily use</td><td>Terraform 1.17</td><td>Platform features are tied to the vendor CLI</td></tr> <tr><td>Self-managed CI with S3/GCS backend</td><td>Either</td><td>Low switching cost; decide on licence and features</td></tr> <tr><td>Need encrypted state independent of the backend</td><td>OpenTofu 1.13</td><td>Built-in encryption block</td></tr> <tr><td>Legal requires an OSI-approved licence</td><td>OpenTofu 1.13</td><td>MPL-2.0</td></tr> <tr><td>Heavy use of vendor-supported Terraform tooling</td><td>Terraform 1.17</td><td>Fewer integration surprises</td></tr> <tr><td>Config written for a much older Terraform (1.5.x or earlier)</td><td>Upgrade first, decide later</td><td>Big version jumps are the risk, not the tool</td></tr> <tr><td>Small team, no pain today</td><td>Stay put</td><td>Switching has a cost and no immediate payoff</td></tr> </tbody> </table> <p><strong>Watch out:</strong> do not combine a tool switch with a major provider upgrade in one change. When a plan shows unexpected diffs, you want exactly one variable to blame.</p> <h2>Evidence-based recommendation</h2> <p>The evidence for an urgent move in either direction is thin. Both tools plan and apply the same core HCL against the same providers. The differences that matter are licence, state handling, and platform lock-in, and you can evaluate those without touching production.</p> <p>So the practical approach is a read-only trial. The following runs OpenTofu against a copy of your current setup. It takes a state backup first, then produces a plan and never applies. Run it from a branch.</p> <pre><code class="language-bash"># 1. Back up current state with the tool you use today terraform state pull &gt; ../backup-$(date +%F).tfstate 2. Check versions; OpenTofu evaluates required_version against its own version number, so review any terraform { required_version } constraint terraform version tofu version 3. Re-init with OpenTofu (rewrites .terraform.lock.hcl; review the diff) rm -rf .terraform tofu init tofu providers lock -platform=linux_amd64 -platform=darwin_arm64 4. Read-only comparison: -lock=false avoids blocking teammates; never apply here tofu plan -lock=false -detailed-exitcode echo "exit code: $?" </code></pre> <p>To verify the result, check the exit code. <code>0</code> means no changes, which is what you want from an unchanged codebase. <code>2</code> means a diff, so read it before you do anything else. <code>1</code> is an error, typically a provider source or backend setting. If the plan is clean, run the same steps in CI on a non-production workspace.</p> <p><strong>Watch out:</strong> run <code>tofu apply</code> only on a workspace you can afford to roll back. A newer tool can write state that the older one refuses to read, and the backup from step 1 is your way out.</p> <p>If the trial is clean and you want OpenTofu's encryption, add it deliberately as a separate change. Existing state is unencrypted, so the first configuration needs an unencrypted fallback to read it. This example uses AWS KMS; the key alias is a placeholder for your own.</p> <pre><code class="language-hcl"># encryption.tf (OpenTofu only; Terraform will reject this block) terraform { encryption { key_provider "aws_kms" "main" { kms_key_id = "alias/tofu-state" # your KMS key alias region = "eu-west-1" key_spec = "AES_256" } method "aes_gcm" "default" { keys = key_provider.aws_kms.main } # Temporary: lets OpenTofu read the existing unencrypted state method "unencrypted" "migrate" {} state { method = method.aes_gcm.default fallback { method = method.unencrypted.migrate } } } } </code></pre> <p>After the next <code>tofu apply</code> writes encrypted state, remove the <code>unencrypted</code> method and the <code>fallback</code> block, and add <code>enforced = true</code> inside <code>state</code> so OpenTofu refuses to write unencrypted state. Verify by reading the raw object from the backend rather than through <code>tofu</code>, which decrypts transparently, for example <code>aws s3 cp s3://&lt;your-state-bucket&gt;/&lt;your-state-key&gt; - | head -c 200</code>. Encrypted state is a JSON envelope with encryption metadata instead of readable resource attributes. Details, plan encryption and rollback options are in the <a href="opentofu.org">OpenTofu documentation</a>, so follow those rather than improvising.</p> <p>The recommendation, with conditions:</p> <ul> <li><strong>Stay on Terraform 1.17</strong> if you use HCP Terraform or Enterprise, or if nothing in the matrix hurts today.</li> <li><strong>Move to OpenTofu 1.13</strong> if licence terms or state encryption are real requirements, and the dry-run above shows a clean plan.</li> <li><strong>Do neither yet</strong> if you are several minor versions behind. Upgrade within your current tool first, then compare.</li> </ul> <p>For related infrastructure patterns, browse more guides on <a href="kuryzhev.cloud">kuryzhev.cloud</a>. Whichever way you go, pin the binary version in CI so the choice stays a decision and never becomes drift.</p> <h2>Related</h2> <ul> <li><a href="kuryzhev.cloud">Terraform Vault Secrets: Keep Them Out of State and Git</a></li> <li><a href="kuryzhev.cloud">Terraform Modules for AWS VPC, IAM, and S3: Structure That Scales</a></li> <li><a href="kuryzhev.cloud">Why an LLM reviewer misses risky Terraform plan changes</a></li> </ul>

  2. aws login + assumeRole + terraform のエラー回避 問題の概要 条件 aws login を使って認証する。 そこからさらに assumeRole して別ロールを使う その状態で terraform を実行する 発生する現状: 以下のエラーが発生する ╷ │ Error: failed to load assume role arn:aws:iam::123456789012:role/MY-ROLE, of profile LOGIN-PROFILE, <nil> │ │ ╵ つまり、aws login と assumeRole を併用して terraform を使えない。 対処方法 ! 本対処は aws-sdk-go-v2 の現時点の実装を利用したワークアラウンドであり、正しい設定ではなく今後の aws-sdk-go-v2 の更新で使えなくなる可能性があります。 以下のように assumeRole 用のプロファイルを作成する: aws-sdk-go-v2 で aws login + assumeRole が失敗する問題回避用のプロファイル [profile ROLE-PROFILE-go] login_session = arn:aws:iam::123456789012:user/MY-USER region = ap-northeast-1 role_arn = arn:aws:iam::123456789012:role/MY-ROLE role_session_name = MY-USER 通常の aws login + assumeRole 用のプロファイル (参考) [profile ROLE-PROFILE] region = ap-northeast-1 role_arn = arn:aws:iam::123456789012:role/MY-ROLE role_session_name = MY-USER source_profile = LOGIN-PROFILE aws login で作成されるプロファイル [profile LOGIN-PROFILE] login_session = arn:aws:iam::123456789012:user/MY-USER region = ap-northeast-1 terraform 利用時にはこの ROLE-PROFILE-go を使う: AWS_PROFILE=ROLE-PROFILE-go terraform init 事象の詳細 再現手順 前提条件 コンソールログイン用のユーザーもしくはロールがある (IAM ユーザー MY-USER とする) そこから assumeRole して使うユーザーがある (IAM ロール MY-ROLE とする) MY-USER でコンソールログインする。 以下コマンドで awscli の認証を行う。 aws login --profile LOGIN-PROFILE ~/.aws/config に以下のようなエントリーが作成される: [profile LOGIN-PROFILE] login_session = arn:aws:iam::123456789012:user/MY-USER region = ap-northeast-1 ~/.aws/config に以下のようなエントリーを作成する: [profile ROLE-PROFILE] region = ap-northeast-1 role_arn = arn:aws:iam::123456789012:role/MY-ROLE role_session_name = MY-USER source_profile = LOGIN-PROFILE awscli でこのプロファイルで MY-ROLE として認証できることを確認する。 AWS_PROFILE=ROLE-PROFILE aws sts get-caller-identity aws へのアクセスが発生する Terraform テンプレートで terraform を実行する。バックエンドに S3 バケットを使用するなど。 AWS_PROFILE=ROLE-PROFILE terraform init バックエンドに S3 を使用しておらず AWS プロバイダーで AWS リソースにアクセスしている場合は init ではエラーにならず plan などでエラーになる。 以下のようなエラーが発生する。 ╷ │ Error: failed to load assume role arn:aws:iam::123456789012:role/MY-ROLE, of profile LOGIN-PROFILE, <nil> │ │ ╵ 原因 aws-sdk-go-v2 のバグによる問題です。 このため実際は terraform 固有の事象ではなく、 aws-sdk-go-v2 を使う golang アプリケーション共通の事象です。aws-sdk-go-v2 は AWS 公式のライブラリーのため、事実上、 golang 実装の AWS を扱うアプリケーションはすべて同じ問題に遭遇すると考えてよいでしょう。 正確にはバグなのか仕様なのかは不明です。本事象は 2026 年 6 月に Login session (aws login) credentials rejected when used as source_profile for an assume-role profile #3446 で報告されており、#3447 で修正案のプルリクエストも提出されているのですが、2026 年 11 月現在もマージされていません。 原因は github.com/aws/aws-sdk-go-v2/config のプロファイルが認証情報を作成できるか判定する処理 で、 「login_session があるときは認証情報を作成する」という判定がないことによるものです。 その結果、source_profile に login_session (aws login 由来) によって認証するプロファイルを指定すると、認証情報が生成できないとして「failed to load assume role (元のプロファイルで role_arn に指定しているプロファイル), of profile (source_profile に指定したプロファイル), (エラー情報。error が生成されていないので常に nil)」というエラーになります。 回避方法 ワークアラウンドプロファイルによる回避 aws-sdk-go-v2 の実装上、同一プロファイル内に login_session と role_arn の両方を書くと login_session (aws login 由来) を使って認証情報を解決した後、 assumeRole する動作をします。 golang アプリケーション (aws-sdk-go-v2) 用の設定 login_session を書いて aws login の認証情報を使った後、 role_arn で assumeRole で認証情報を作成する [profile ROLE-PROFILE-go] login_session = arn:aws:iam::123456789012:user/MY-USER region = ap-northeast-1 role_arn = arn:aws:iam::123456789012:role/MY-ROLE role_session_name = MY-USER コード上の該当箇所: login_session があると aws login 由来の認証情報を解決してきて、そのまま認証情報の解決処理を続行する: config/resolve_credentials.go role_arn があるとその時点の認証情報を使って assumeRole を行う: config/resolve_credentials.go なお、このプロファイルは botocore では role_arn があるのに source_profile がないため不適切な設定と判定されるため、 awscli などで使うとエラーになります: github.com/boto/botocore/blob/1.43.123/botocore/c… $ AWS_PROFILE=ROLE-PROFILE-go aws sts get-caller-identity aws: [ERROR]: Partial credentials found in assume-role, missing: source_profile or credential_source 一方、今回のワークアラウンドプロファイルにさらに source_profile を追加すると、aws-sdk-go-v2 側でワークアラウンドが機能するよりも先に今回の問題となっているチェック処理に引っかかるため、元の通りのエラーになってしまいます。 このため、このワークアラウンドプロファイルは golang アプリケーション専用に、awscli 等で使うプロファイルとは別に作っておく必要があります。 switch role してから aws login する AWS コンソール上で「他のロールに切り替える」の操作を行い、 MY-ROLE に切り替えてから aws login することで、直接 MY-ROLE の状態の認証情報を得ることが出来ます。 正規の方法で安全ですが、使用するロールが多い場合は切り替えのたびに aws login が必要なので煩わしいという問題があります。 また、aws login 用のトークン発行を許可するためにロールに SignInLocalDevelopmentAccess ポリシーを割り当てておく必要があります。 参考: コンソール認証情報を使用して AWS ローカル開発にログインする - AWS Command Line Interface Appendix: terraform / opentofu の aws login 対応バージョン terraform や opentofu で aws login を使うためには、一定以上のバージョンである必要があります。 また、S3 バックエンドを使う場合は terraform / opentofu 本体のバージョンが影響し、 AWS 上のリソースの作成については AWS プロバイダー (terraform-aws-provider) のバージョンが影響します。 必要な aws-sdk-v2-go のバージョン github.com/aws/aws-sdk-go-v2/credentials v1.19.0 以上 github.com/aws/aws-sdk-go-v2/config v1.32.0 以上 already reviewed add logincreds provider - #3230 がマージされたバージョンを使う必要がある。 Terraform のバージョン: v1.15.0 以上、または v1.14.9 以上 v1.15.0 での更新: 12276b8 v1.14.9 での更新: c78dd8d OpenTofu のバージョン: v1.12.0 以上、または v1.11.10 以上 v1.12.0 での更新: 5b6ca0a v1.11.10 での更新: 5e94874 AWS プロバイダー (terraform-aws-provider) のバージョン: v6.23.0 以上、または v6.22.1 以上、 v6.23.0 / v6.22.1 での更新: bead7e4 v5 系は現時点の最新の v5.100.0 でもサポートなし

  3. OpenTofu を選ぶ理由はライセンスだけではない: 1.7〜1.12 の機能で変わる構成管理 OpenTofu を選ぶ理由として語られるのはライセンスの話が中心ですが、1.7 以降に入った機能のほうが日々の構成管理には効きます。lifecycle の enabled と動的な prevent_destroy、backend 設定への静的評価、provider の for_each で、環境ごとに root module を複製したり count = var.x ? 1 : 0 を書いたりする必要がなくなります。加えて、state と plan のクライアント側暗号化という防御層があります。いずれも 2026 年 10 月時点の Terraform 1.17 にはない機能です。 この記事では、OpenTofu を採用する理由をライセンスと機能の両面から整理し、最後に「どんな構成なら乗り換える価値があるか」の判断基準を示します。Terraform から OpenTofu への移行を検討している方、すでに OpenTofu を使っているが「ライセンス以外に何が良いのか」を説明しきれていない方向けです。 記事の内容は 2026 年 10 月時点の公式ドキュメントと CHANGELOG に基づきます。OpenTofu は 1.13.2、Terraform は 1.17.1 まで確認しています。手元の環境は OpenTofu v1.13.2 で、1.13 ではこの記事で扱う機能に挙動変更はありません。コード例は AWS を題材にし、環境名を var.env(dev / prod など)で受け取る前提で書いています。 ライセンスとガバナンスの話は前提にすぎない まず前提を確認します。Terraform は 2023 年 8 月に MPL 2.0 から BUSL 1.1 に変わり、HashiCorp は 2025 年に IBM による買収が完了しました。OpenTofu は Terraform からフォークされ、MPL 2.0 で Linux Foundation 配下のプロジェクトとして運営されています。 これは「将来ライセンスが変わっても困らない」という保険の話で、採用理由として間違ってはいません。ただ、これだけだと「Terraform でも実害はない」という反論に答えられません。本当に効くのは次の機能面の差です。 採用理由になる 4 つの機能 いずれも Terraform 1.17(2026 年 10 月時点)にはない機能です。Terraform は版を重ねるごとに差を埋めてきているので(後述の比較表を参照)、「ない」は常に版付きで考えてください。日常的に効く順に並べます。 lifecycle の新しい引数: 条件付きリソースと削除防止を素直に書く(1.11 / 1.12〜) lifecycle ブロックに加わった引数のうち、Terraform にないのは次の 2 つです。 引数 導入版 置き換えるもの enabled = <bool> 1.11 count = var.x ? 1 : 0 動的な prevent_destroy 1.12 環境ごとの module 複製や別定義 同じ 1.12 で入った destroy = false は Terraform 1.16 にも同等の引数があるため、後の章で扱います。 enabled(1.11〜) リソースや module を 0 個か 1 個で出し分けるための引数です。count = var.x ? 1 : 0 と [0] 参照の代わりになります。 resource "aws_instance" "bastion" { ami = var.bastion_ami instance_type = "t3.micro" lifecycle { enabled = var.env == "dev" } } 挙動と制約は次の通りです。 false のときはリソースが設定に存在しないものとして扱われ、参照は null になります。既存の実物があれば count を 0 にしたときと同様に destroy が計画されるので、切り替え時は plan を確認してください count 方式からの移行に state 操作は要りません。aws_instance.bastion[0] は自動で aws_instance.bastion に移されます(moved ブロック不要) 無効化されたリソースの属性を直接参照すると plan / apply が失敗します。try(aws_instance.bastion.id, null) か aws_instance.bastion != null ? ... : ... で守ります count / for_each とは併用できません。値は plan 時に確定している必要があり、sensitive・null・ephemeral な値も使えません 1.11.4 以降、ローカルに provider 設定を持つ module では enabled を使えません。count / for_each / depends_on に元からあった制約と揃えた修正で、設計時の見落としがパッチ版で直されたものです 使いどころは、環境によって作る・作らないが分かれる単発のリソースです。本番だけの監視、dev だけの踏み台などが典型で、[0] が消えて参照側が読みやすくなります。 動的な prevent_destroy(1.12〜) 従来は定数しか書けなかった prevent_destroy が、同じ module 内の変数を参照できるようになりました。 resource "aws_db_instance" "main" { ... lifecycle { prevent_destroy = var.env == "prod" } } 本番だけ削除防止を効かせたい DB やバケットに使います。prevent_destroy のためだけに環境別の定義を持つ必要がなくなります。 注意点として、prevent_destroy は resource ブロックごと消すと一緒に消えるので、ブロック削除による destroy は防げません。それを防ぐのは後述の destroy = false です。 使い分け 目的 使うもの 環境によって作る・作らないを切り替える enabled 本番だけ誤削除を防ぐ(ブロックを消さない前提) 動的な prevent_destroy 設定から消しても実物を残す destroy = false(後述。Terraform 1.16 にもある) 静的評価: backend 設定に変数が使える(1.8〜) Terraform では backend 設定に変数を書けないため、環境ごとに root module を複製するか、外側のツールで生成するのが定番でした。OpenTofu 1.8 からは backend 設定、module の source と version、state 暗号化の設定に var と local を書けます。 Terraform も 1.15 で module の source / version(const = true を付けた変数のみ)、1.17 で required_providers に変数を使えるようになりましたが、backend 設定と暗号化設定には引き続き書けません。OpenTofu 固有の利点はこの 2 か所です。 variable "env" { type = string } terraform { backend "s3" { bucket = "tfstate-${var.env}" key = "network/terraform.tfstate" region = "ap-northeast-1" } } 値はリソースや data source、module の output に依存しないものに限られ、plan 前に確定している必要があります。tofu init -var env=prod のように init 時に渡し、plan / apply でも同じ値を渡します。env.tfvars のようなファイルにまとめて -var-file で渡すのが楽です。これで環境別の root module 複製を 1 つにまとめられます。 module の version を locals で一元管理する使い方もできます(こちらは Terraform 1.15 以降でも可能です)。 locals { vpc_module_version = "5.21.0" } module "vpc" { source = "terraform-aws-modules/vpc/aws" version = local.vpc_module_version } provider の for_each: リージョン別・アカウント別の複製をなくす(1.9〜) alias 付きの provider 設定に for_each を書けます。リージョンやアカウントの一覧を変数で持ち、provider とリソースをまとめて繰り返せます。 variable "aws_regions" { type = map(object({ vpc_cidr_block = string })) } provider "aws" { alias = "by_region" for_each = var.aws_regions region = each.key } resource "aws_vpc" "main" { for_each = var.aws_regions provider = aws.by_region[each.key] cidr_block = each.value.vpc_cidr_block } for_each は alias と組み合わせたときだけ使えます。リージョンごとに provider ブロックとリソースを手書きで複製していた構成は、これで 1 セットになります。 state と plan のクライアント側暗号化(1.7〜) Terraform の state 保護は backend 側の暗号化(S3 の encrypt、GCS の顧客管理鍵など)とアクセス制御に依存します。state ファイルを取得できた人は中身を読めます。 OpenTofu は state と plan をクライアント側で暗号化でき、backend の暗号化とは独立した防御層になります。 terraform { encryption { key_provider "aws_kms" "main" { kms_key_id = var.state_kms_key_id region = "ap-northeast-1" key_spec = "AES_256" } method "aes_gcm" "main" { keys = key_provider.aws_kms.main } state { method = method.aes_gcm.main } plan { method = method.aes_gcm.main } } } 鍵の供給元は PBKDF2 のパスフレーズ、AWS KMS、GCP KMS、Azure Key Vault、OpenBao、外部コマンドから選べます。鍵や方式を切り替えるときは fallback ブロックで旧方式を残しておくと、読み込みは旧方式に自動で倒れ、書き込みは新方式で行われます。 注意点はデフォルトでは無効なことです。terraform { encryption { ... } } に key_provider、method、state、plan を書いて初めて有効になります。「OpenTofu にしたから state は安全」ではなく、設定して初めて効く機能として扱ってください。運用では、鍵を state ごとに分け、ローテーションを自動化するのが公式の推奨です。AES-GCM は同じ鍵で暗号化できる回数に上限があるためです。 そのほか: Terraform にもある機能 この章は「乗り換えても失わない機能」の確認です。OpenTofu が先行したものも含め、2026 年 10 月時点で Terraform にも同等の機能があるので、採用理由は前章の 4 つで判断してください。 機能 OpenTofu Terraform S3 ネイティブロック(use_lockfile = true) 1.10 1.10(1.11 で GA) Ephemeral 値 1.11 1.10 write-only 属性 1.11 1.11 lifecycle { destroy = false } 1.12 1.16 module の source / version に変数 1.8 1.15 OCI(Open Container Initiative)レジストリからの module / provider 取得 1.10 なし(1.17 までの CHANGELOG に記載なし。1.12 の「OCI Object Storage backend」は Oracle Cloud の話で別物) S3 ネイティブロック S3 backend に use_lockfile = true を書くと、DynamoDB なしでロックが取れます。state 用の DynamoDB テーブルを環境ごとに作っていた構成では、そのテーブルを消せます。 terraform { backend "s3" { bucket = "tfstate-prod" key = "network/terraform.tfstate" region = "ap-northeast-1" use_lockfile = true } } Ephemeral 値と write-only 属性 Ephemeral 値は、plan や apply の実行中だけメモリ上に存在し、state にも plan ファイルにも保存されない値です。write-only 属性は、provider が「書き込みのみ」と宣言した属性で、パスワードのように設定はするが state に残したくない値に使います。どちらも「秘密情報が state に残る」問題への対処で、前章のクライアント側暗号化と組み合わせると、state に残さない・残ったものは暗号化する、の二段構えになります。 destroy = false(OpenTofu 1.12〜、Terraform 1.16〜) リソースが設定から消えたとき、replace が必要なとき、tofu destroy のときに、実物を壊さず state から外すだけ(forget) にします。 resource "aws_s3_bucket" "archive" { bucket = "archive-${var.env}" lifecycle { destroy = false } } plan の action が destroy ではなく forget になります。replace が必要な場合は forget してから新規作成します 一度 apply すると設定が state に残り、true に戻すか引数を消すまで destroy は計画されません destroy = false があると prevent_destroy は効きません 定数の true / false のみで、変数は使えません tofu destroy は forget 対象を残すため非ゼロで終了します。CI で回すなら tofu destroy -suppress-forget-errors でゼロ終了にできます(1.12〜) 同じアドレスで再定義すると新規作成を試みるので、実物が残っていれば失敗します 使いどころは、管理を OpenTofu から別の仕組みに移すときや、実物を残したまま module 構成を組み替えるときです。removed { destroy = false } を毎回書く代わりに、リソース側で宣言しておけます。Terraform 1.16 の destroy = false も「state から外すだけで実物は残す」という趣旨は同じですが、定数のみか・prevent_destroy との優先関係・destroy コマンドの終了コードが同じかは Terraform 側のドキュメントに記載がないため、Terraform で使う場合は挙動を確認してください。 OCI レジストリ ここでの OCI は Open Container Initiative、つまりコンテナイメージと同じレジストリのことです。module は oci: スキームの source で、provider は provider ミラーとして OCI レジストリから取得できます。社内の module や provider を配布するために独自の仕組みを持っている場合、既存のコンテナレジストリに相乗りできます。 乗り換える価値がある構成、差が小さい構成 効く構成 環境ごとに root module を複製している(静的評価で 1 つにできる) リージョンやアカウントをまたいで provider とリソースを複製している(provider の for_each) count = var.x ? 1 : 0 と [0] 参照が多い(enabled) 本番だけ削除防止を効かせるために環境別の定義を持っている(動的な prevent_destroy) state に秘密情報が入り、backend 側の暗号化だけでは不安がある(クライアント側暗号化) 差が小さい構成 単一環境・単一リージョンで、既存の Terraform 構成が安定している 上の構成パターンに当てはまらず、ライセンスの保険だけが動機 後者でも乗り換えを否定するものではありませんが、「ライセンスの保険」以上の理由は出しにくいと考えてください。 まとめ OpenTofu の採用理由はライセンスだけでなく、lifecycle の enabled と動的な prevent_destroy(1.11 / 1.12)、backend と暗号化設定への静的評価(1.8)、provider の for_each(1.9)、state と plan のクライアント側暗号化(1.7)という、Terraform 1.17 時点にはない機能にあります S3 ネイティブロック、Ephemeral 値、write-only 属性、destroy = false、module source への変数は Terraform にもあるので、採用理由ではなく「乗り換えても失わない機能」として扱います。Terraform は版ごとに差を埋めてきているので、比較は版付きで見直してください state 暗号化はデフォルト無効です。採用したら設定を忘れないでください 乗り換える価値は、環境・リージョンの複製や count による出し分けがどれだけあるかで決まります 参考 OpenTofu: The enabled Meta-Argument OpenTofu: Resource Behavior(lifecycle の prevent_destroy と destroy) OpenTofu 1.8: What's new(静的評価) OpenTofu: Provider Configuration(provider の for_each) OpenTofu: State and Plan Encryption OpenTofu: S3 backend(use_lockfile) OpenTofu CHANGELOG: v1.8 / v1.9 / v1.10 / v1.11 / v1.12 Terraform CHANGELOG: v1.10 / v1.11 / v1.15 / v1.16 / v1.17 Terraform: Backend Configuration(backend に変数は不可) Terraform: The lifecycle Meta-Argument(引数はリテラルのみ) Terraform: Sensitive Data in State(state の暗号化は backend 依存) The OpenTofu Manifesto(ライセンス変更の経緯)

  4. OpenTofu 1.13: Symbol Libraries, Linting Experiment, and Fewer Unknowns in Planning On September 30, 2026, the OpenTofu project released version 1.13.0 — deliberately as a leaner release: The development time was shortened to align with the Go release cycle, which keeps the series under security support longer (until August 2027). Precisely because of the reduced feature load, two experiments are all the more remarkable, as they show where things are heading: built-in linting and a new artifact called "Symbol Libraries." In addition, there are new functions for more precise plans, official Windows ARM64 packages, and some breaking changes to consider when upgrading. For IaC teams, the release is also a good opportunity to review the project's position: OpenTofu is now the established Terraform fork hosted under the Linux Foundation and has been a CNCF sandbox project since April 2025. Less "(known after apply)" with convert and assume OpenTofu's core workflow depends on predicting the apply as accurately as possible. Values that cannot be determined during planning appear in the plan as "(known after apply)" — practical, but problematic: The meta-arguments enabled, for_each, and count reject unknown values, and automatic policy checks that evaluate the machine-readable plan output are practically blinded by unknowns. OpenTofu 1.13 therefore introduces two families of tools with which module authors can give the tool additional hints: - convert() specifies an expected type, for example when an unknown string is parsed via jsondecode: The result remains unknown, but OpenTofu then knows that accessing id yields a string. - The assume... functions (e.g., assumenotnull, assumestringprefix) make statements about the value itself. A typical example: The id of an AWS resource object is unknown at plan time. With a hint, the comparison with null can be turned into a known false: output "vpc_id" { value = assumenotnull( assumestringprefix( aws_vpc.example.id, "vpc-", ) ) } The decisive condition: The hints must actually hold true at apply time. If the promised prefix does not match, the function fails during apply. Where provider or API documentation provides a guarantee, such assumptions can be cleanly encapsulated in a shared module. Two experiments: Linting and Symbol Libraries Version 1.13 contains very early work on built-in linting: initial infrastructure to enable lint rules and report results as warnings, plus some built-in example rules for the OpenTofu language. The project intends to develop this into a comprehensive feature long-term that also covers policy enforcement. The second experiment is "Symbol Libraries": reusable collections of values, functions, and type aliases that can be imported from all source types also supported for modules. This allows, for example, an organization-wide convention for DNS records to be defined centrally: typedef "recordset" { type = set({ name = string type = string ttl = number records = set(string) }) } function "a_records" { type = symbols::dns_recordset() parameter "records" { type = map(set(string)) } parameter "ttl" { type = number } return = [ for name, ip_addrs in param.records : { name = name type = "A" ttl = param.ttl records = ip_addrs } ] } Both experiments are deliberately marked as immature and are not intended for production use — the project is explicitly seeking early feedback from the community. Windows on ARM64 and the farewell to WinRM New is official support for Windows on ARM64: The major cloud providers have corresponding windows_arm64 packages; with third-party providers, a provider upgrade may be necessary. On the removal list, on the other hand, is WinRM: The connection types of the remote-exec and file provisioners are no longer supported because the underlying Go libraries are unmaintained. Affected configurations should migrate to OpenSSH for Windows. Further breaking changes when upgrading - base64gzip now produces equivalent but no longer byte-identical results due to a new DEFLATE implementation — OpenTofu may therefore suggest resource changes; the ignore_changes meta-argument serves as a transition. - macOS builds require at least macOS 13 Ventura. - v1.13 is the last series with official 32-bit packages (386, arm); calling tofu init warns about this, and v1.14 will remove them entirely. - The first patch, v1.13.1 from October 1, fixes among other things tofu show -json with ephemeral resources. Where OpenTofu stands in 2026 The fork was created in August 2023 after HashiCorp converted Terraform from MPL 2.0 to the Business Source License; since September 2023 the project lives under the Linux Foundation, the first stable version 1.6.0 was released in January 2024, and CNCF acceptance as a sandbox project followed in April 2025. In 2026, OpenTofu continues to introduce features that Terraform does not offer — such as built-in state encryption (1.7), early variable evaluation (1.8), and dynamic prevent_destroy (1.12); provider for_each (1.9) and ephemeral values (1.11) also came first in OpenTofu. Currently, the 1.11, 1.12, and 1.13 lines are maintained; patch 1.12.7 most recently closed security gaps in the SSH provisioner (CVE-2026-78662, CVE-2026-56855). Major companies are also migrating: Fidelity Investments documented its migration from Terraform to OpenTofu in the official blog. Conclusion OpenTofu 1.13 is a signal release: With the assume... functions, planning becomes more precise, and the experiments on linting and symbol libraries point to the next evolutionary stage of the tool — quality assurance and reusable logic directly in the core instead of in external tools. Teams should plan for WinRM removal, base64gzip changes, and the 32-bit deprecation when upgrading. Looking at v1.14 (without 32-bit packages) and the 1.13 series supported until August 2027 is worthwhile for every IaC roadmap — OpenTofu is no longer a side project in 2026, but an independent force in the IaC ecosystem. Sources - OpenTofu v1.13.0 (official blog post) — feature details, experiments, breaking changes - opentofu/opentofu Releases (GitHub) — v1.13.0/v1.13.1/v1.12.7, support periods, CVEs - OpenTofu Blog — incl. "An Introduction to OpenTofu Symbol Libraries" (09/17/2026), "A Vision for Built-in Linting" (09/03/2026), Fidelity migration report - Spacelift: OpenTofu – The Open-Source Alternative to Terraform — project history, release cadence, feature timeline - env.dev: OpenTofu in 2026 — Terraform comparison, migration path

  5. I prefer the seamless native encryption of the state file with OpenTofu. Quite a big selling point for me actually. So far I didn't run into issues with OpenTofu. And commit activity looks healthy to me. github.com/opentofu/opentofu/graphs/commit-activi…

    1 like
  6. Still terraform. OpenTofu barely sees commits opentofu/opentofu

  7. Terraform vs OpenTofu in 2026, and how to migrate Terraform and OpenTofu both turn configuration into cloud infrastructure. In 2026 the question is which one you should choose. If you are a startup choosing for the first time, changing your mind later means a migration. If you pick Terraform today, you will most likely be on the latest release, and that narrows where you can run it. Some third-party platforms stop at 1.5.7, the last release under the old license, which can make HCP Terraform the main option that runs current Terraform for you. Its free tier covers up to 500 managed resources, and above that you pay per managed resource, so the cost climbs quickly as your infrastructure grows. For a new stack that is not tied to HCP Terraform, a procurement rule, or a vendor support contract, start with OpenTofu. The reason is ecosystem fit and open governance, not a gap in what Terraform can build. If you froze Terraform at 1.5.7 or 1.6, you are running a release that no longer gets security fixes. If you kept upgrading, OpenTofu publishes a migration guide for every Terraform line up to 1.9. The move from 1.5.7 needs no code changes. The guides for 1.6 through 1.9 list a short set of known differences, mostly S3 backend options, removed blocks, test overrides and a few functions, and each one comes with a fix. If you are on a currently supported Terraform release, which today means 1.15 or 1.16, the documented path runs out and the two products have started to drift. The action block and terraform query, added in Terraform 1.14, are in those releases, and as of October 2026 OpenTofu has no equivalent. We expect more of this, because OpenTofu cannot copy newer Terraform code and each side now builds its own features. The further the forks move apart, the more risk you carry and the more work a later migration takes. A stack that uses these features has more to unwind. A stack that does not can still move, but no guide covers it. How did we get here? On August 10, 2023, HashiCorp announced that future Terraform releases would move to the Business Source License 1.1. That license is source-available, not open source by the OSI definition. Competing vendors would no longer be able to use those releases in competing offerings, as HashiCorp defined them, while APIs, SDKs and most libraries stayed under MPL 2.0. Terraform 1.5.7 is the last MPL release and the BSL begins at 1.6.0. Each BSL release converts to MPL 2.0 four years after publication, so 1.6.0 converts around October 2027. Within weeks a community group published a manifesto and forked the last MPL commit. The public OpenTofu repository went live on September 5, 2023, the Linux Foundation announced it would host the project on September 20, and OpenTofu 1.6 reached general availability on January 10, 2024. In April 2024 HashiCorp sent a cease-and-desist letter, which OpenTofu's counsel disputed, and no public outcome has been reported. IBM completed its acquisition of HashiCorp in February 2025 and the BSL stayed in place. The restriction fell on vendors hosting Terraform, not on a team using it for their own cloud. A company could keep upgrading. Hosted platforms that compete with HCP Terraform could not ship those releases, so they stopped at 1.5.7. What changed The project has shipped steadily. OpenTofu has released seven minor versions since 1.6, most recently 1.13 in September 2026, with security support through August 2027. The CNCF accepted it into the Sandbox on April 23, 2025. Sandbox is the entry maturity level, so it has not reached Incubating. It also has people behind it. Companies pledged funded engineering staff for at least five years starting in 2023, and the supporters page now lists 163 companies, 12 projects, one foundation and about 791 individuals. The project is governed by a technical steering committee and hosted by the Linux Foundation and the CNCF, so no single vendor owns it. How OpenTofu and Terraform differ in 2026 Day-to-day HCL, providers, modules, backends and the core CLI are the same, and providers did not change license. The differences are in newer features, and matching version numbers do not mean matching behavior, because OpenTofu 1.11 and Terraform 1.11 are different things. The table lists the features that matter most as of October 2026, ordered by who shipped first. The comparison is OpenTofu 1.13 against Terraform 1.16, taken from the OpenTofu changelog and the Terraform changelog. No equivalent means no equivalent in the open-source CLI for that release. | Capability | OpenTofu | Terraform | Shipped first | |---|---|---|---| | Client-side state and plan encryption | 1.7 | No equivalent | OpenTofu | | Provider-defined functions | 1.7 | 1.8 | OpenTofu | | Variables in backend and encryption config | 1.8 | No equivalent | OpenTofu | | Variables and locals in module source | 1.8 | 1.15 | OpenTofu | | Provider for_each | 1.9 | No equivalent | OpenTofu | | -exclude flag | 1.9 | No equivalent | OpenTofu | | OCI registry sourcing for modules and providers | 1.10 | No equivalent | OpenTofu | | enabled argument on resources and modules | 1.11 | No equivalent | OpenTofu | | language block | 1.12 | No equivalent | OpenTofu | | Ephemeral resources | 1.11 | 1.10 | Terraform | | Write-only attributes | 1.11 | 1.11 | Same release number | | terraform stacks (HCP Terraform only) | No equivalent | 1.13 | Terraform | | action blocks | No equivalent | 1.14 | Terraform | | List resources and terraform query | No equivalent | 1.14 | Terraform | OpenTofu shipped most of the language features first. Terraform shipped ephemeral resources a release earlier and now leads on actions and queries. Both tools now have a const argument on input variables, but the documented behavior differs. OpenTofu infers whether a variable is constant when you leave const unset, and const = false opts out. Terraform's documentation says const must be set before a variable can be used in module and provider source and version attributes. Setting const = true explicitly is the form both tools document. terraform stacks and Sentinel belong to HCP Terraform and Terraform Enterprise, so teams outside those products can ignore them. The action block and terraform query are core language and CLI features, so teams that never touch HCP will face them. If your delivery model is built around HCP Terraform, that is a legitimate reason to stay. Which migration guide matches your Terraform version If you run Terraform 1.5 through 1.9, OpenTofu publishes a guide for your exact line, and 1.5.x is the easiest case. Each guide is exact to one patch release, so you may need to move to that patch first. The guides target older OpenTofu releases, so finish by upgrading to a supported release in a separate change. If you kept upgrading past 1.9, there is no versioned guide for your release. OpenTofu's general migration guide walks through a backup, a plan, and an apply. Its page on interdependent configurations says OpenTofu reads Terraform 1.x state, and that Terraform may not reliably read state OpenTofu wrote. Neither page is a guide for Terraform 1.15 or 1.16, so the validation is yours. Upgrade carefully and start with a trial run on a copy of your state. The next section covers how. How to migrate from Terraform to OpenTofu Migrate one stack at a time, starting with a low-stakes one. Each stack goes through the same four steps. 1. Prepare. Check what you are about to migrate. Note each stack and workspace, since each has its own state, plus the providers and modules they use and any required_version constraint, which can block tofu. Apply any pending changes and confirm terraform plan shows nothing. Make sure you can roll state back, either by turning on versioning for the state bucket or by saving a copy with terraform state pull. The pulled state, and any plan you save later, can contain secrets. Keep those files out of git and treat them like credentials. terraform plan # expect: No changes terraform state pull > backup-pre-cutover.tfstate 2. Run a trial on a copy. Copy the backup to a trial state file. cp backup-pre-cutover.tfstate trial.tfstate Then comment out the real backend block and add a local backend that points at the copy. This is a code change, not a command, and it keeps the trial away from the shared state and its lock. terraform { # backend "<your backend type>" { ... } backend "local" { path = "trial.tfstate" } } Run tofu init -reconfigure. Because you swapped backends, init will not continue without a flag, and -reconfigure points at the new backend without copying any state, so the trial uses the file you just made. Skip -upgrade, which also moves provider versions and clouds the diff. Expect init to rewrite the lock file, switching each provider from registry.terraform.io to registry.opentofu.org. It keeps the versions but not the hashes, because OpenTofu's provider releases are not byte-for-byte identical to HashiCorp's. Finish with tofu plan. A plan that reports no changes means this copy is readable and this configuration plans clean. It does not prove the real backend, the workspace you will cut over, or the production run. Any diff is a reason to stop and investigate. tofu init -reconfigure tofu plan # expect: No changes 3. Cut over. Up to here OpenTofu has only worked on a copy, and the shared state has not been written to. This step is the first time OpenTofu writes to that state, and the stack becomes OpenTofu-owned. Rolling back after that means restoring a backup or repairing the state, and it gets harder once a real change has been applied. OpenTofu's migration guide calls the process reversible. The section on switching back says when that holds. Start by undoing the trial. Un-comment the original backend block and delete the local one, so the configuration is back to what it was. terraform { backend "<your backend type>" { ... } } Then point back at the real backend with -reconfigure. Never use -migrate-state here, because it could offer to copy the trial state over the real one. The commitment point is the first command that writes state. Apply is the intended one, because even with no changes it lets OpenTofu update the state file format if needed. Other commands that write state, including state mv, state rm, state push and import, make the stack OpenTofu-owned the same way. A saved plan keeps what you reviewed identical to what you apply. Commit the rewritten lock file with the cutover. tofu init -reconfigure tofu plan -out=cutover.tfplan # expect: No changes tofu apply cutover.tfplan # only after a clean plan 4. Follow with a small change. Make a small, non-critical change under OpenTofu. Then run read-only plans from both engines against the same commit for a couple of weeks and diff the results. That is the best canary. A clean plan in one workspace does not prove the others, so repeat steps 2 and 3 for each workspace. Update your tooling as you go. Switch one pipeline at a time, lowest stakes first, and keep a disabled fallback job. GitHub Actions has the community-maintained opentofu/setup-opentofu action. For other tools, look for an engine or runtime setting. After you migrate The lock file now lists registry.opentofu.org providers. The versions carry over and the hashes are new. provider "registry.opentofu.org/hashicorp/time" { version = "0.14.2" constraints = ">= 0.14.2" hashes = [ "h1:0lkmuDlyUBEK2vAxb9r8jY8kMpqYbMoSb4GWnSbA9iY=", "h1:4ccOXW03+ENKJieXGwTfMvRlkpT9o+ra6dw240C1UFE=", # ... ] } The init output still prints the short name, hashicorp/time. That is normal. Can you switch back from OpenTofu to Terraform? Going back is harder than going in, and it gets harder the longer you stay. OpenTofu's documentation calls the process reversible, and right after cutover, with the same features and no real changes, it is. Every apply, every new resource and every OpenTofu-only feature moves you further from that point. OpenTofu's migration pages cover one direction only, so your mileage will vary. Rollback has two cases. - No real change has been applied since cutover. Stop using OpenTofu, restore the state backup, run terraform init and plan, confirm nothing unexpected, and test a small change. - Changes have been applied under OpenTofu. Do not restore the backup. The backup is older than reality, so new resources become unmanaged and the next Terraform apply may try to recreate or destroy things. Inspect the current state before you point Terraform at it, and be ready to repair it by hand. OpenTofu records its own version in the state file after an apply, for example 1.13, so the state no longer looks like Terraform wrote it. Define rollback triggers up front, such as a destroy or replace on a resource that is unchanged in code, or an unclean plan on consecutive runs. Several things make the door one-way in practice. - State and plan encryption. The documentation does not say Terraform can read OpenTofu-encrypted state, so plan as though it cannot. Encryption can be unwound within OpenTofu by adding an unencrypted method as primary, keeping the old method as fallback, turning off enforced, applying, and then removing the block. That does not help you return to Terraform. - Language additions. Provider for_each, the enabled argument, the language block and variables in backend blocks have no Terraform equivalent. Terraform has its own const, and provider-defined functions work in Terraform from 1.8, so those are not one-way items. - .tofu files. These are a different hazard. If versions.tf and versions.tofu both exist, OpenTofu ignores versions.tf and reads versions.tofu. It still reads the other .tf files. Terraform does not read .tofu files, so after a rollback it runs versions.tf and never sees versions.tofu. The behavior is in OpenTofu's settings documentation. Avoid adding .tofu files until a return to Terraform is no longer a goal. - Pipeline features. The -exclude flag is a CLI flag, so it costs you a pipeline feature on rollback and nothing more. Keep Terraform installed and the old pipeline definitions on a branch for one full apply cycle, in case you need to roll back. Then remove both, so muscle memory does not send someone to run terraform against a stack that OpenTofu now owns. Adopt OpenTofu-only features deliberately, not during the migration window. In particular, do not turn on state encryption during migration, because it stacks a second risky change onto an already risky one. What breaks during a Terraform to OpenTofu migration - required_version constraints. OpenTofu changed how it reads these. Through 1.11 it compared any required_version against its own version number, so a constraint such as ~> 1.5.0 or < 1.6 rejected tofu. From 1.12, OpenTofu's settings documentation says a required_version in a plain .tf file is ignored, and constraints are enforced only in .tofu files or in a language block with compatible_with. Check which OpenTofu release you pin, search every root and shared module, and decide a policy before cutover. - Accidental Terraform runs. Reading Terraform's source code, a stray terraform plan on a migrated stack should stop at the lock file, which now lists registry.opentofu.org providers, and report an inconsistent dependency lock file. Do not count on the state to protect you. Current Terraform no longer compares the terraform_version recorded in state with its own version, so a state last written by OpenTofu is accepted. A plan still takes the state lock, so it can block a real run. terraform init rewrites the lock file and removes that protection, and terraform apply rewrites state with Terraform's version and provider addresses. Add a CI check on the engine in use, and tell people not to run terraform init on a migrated stack. - One engine owns a stack at a time. Freeze merges to the stack during its cutover window and gate the old pipeline, so a Terraform job cannot fire against state OpenTofu just wrote. - Registry addresses and missing providers. Fully qualified registry.terraform.io/... sources should become short forms, for providers and modules alike. Niche, vendor-specific and private providers and modules may be absent from the OpenTofu registry. Symptoms include failures to query available provider packages and module-not-found errors. Confirm the code works on Terraform, file a registry request, or use an explicit registry.terraform.io source. - Lock file churn. Hashes and source addresses change. Use tofu providers lock with a -platform= flag for each platform your developers and CI use, and commit the lock file with the engine swap. OpenTofu 1.12 records checksums for all supported platforms on init. - The cloud block and HCP Terraform. Since OpenTofu 1.6 the cloud and remote backends no longer default to app.terraform.io, so set hostname (or TF_CLOUD_HOSTNAME). OpenTofu's documentation does not promise compatibility with HCP Terraform, Terraform Enterprise or Sentinel, so those teams need a different plan, and policy as code moves to OPA or platform tooling. - Hardcoded binaries, tooling and policies. Dockerfiles, scripts, linters, scanners and policy engines often call terraform or assume .tf files. Search for it and test each tool against tofu and .tofu. In a mixed fleet, policies keyed to "version 1.x" become ambiguous, so log the engine too. - Remote state consumers. OpenTofu's page on interdependent configurations says to migrate consumers before the stacks they read from, so producers go last. OpenTofu reads Terraform state, but Terraform may not reliably read OpenTofu state. If you enable encryption later, consumers also need matching remote_state_data_sources settings, so configure encryption keys and those settings before dependent stacks run. Where to go from here The right choice depends on your team. If Terraform fits how you deliver today, because of HCP Terraform, procurement rules or a vendor support contract, staying is a sound decision. Just keep in mind that the gap between the two tools widens with every release, so this is a good time to look at your Terraform strategy on purpose and not by default. If you want to see where you stand, pick one low-stakes workspace and run the trial from step 2. It touches nothing shared, and the plan tells you how much of this post applies to you. If something blocks you, such as a missing provider, a pipeline tool that cannot run against OpenTofu, or a diff you cannot explain, you will have found it before committing anything. If the plan comes back clean, the copy is readable. The cutover is still a separate step. We hope this helps you make the call that fits your team. Whichever engine you choose, the next challenge is keeping infrastructure and application changes in step, which is the problem we built Admiral to solve. Admiral is an orchestrator. It brings infrastructure and workload changes into one workflow, and you keep your own tools. The work itself runs on agents in your own environment, a deliberate part of our security approach.

  8. OpenTofu 1.13: What's New and What Terraform Still Doesn't Have > Originally published on DevToolHub. OpenTofu 1.13.0 shipped on September 30, 2026. Officially-supported Windows on ARM64, a set of new functions, and upgrades to the state-encryption system it shipped ahead of Terraform. Here's what actually changed, verified against the official release changelog, plus a close look at state encryption itself — still the single biggest feature gap between OpenTofu and Terraform. What's Actually New in OpenTofu 1.13 Per the official v1.13 changelog: - Windows ARM64 is now officially supported. Previously unofficial or community-built; OpenTofu now ships official binaries for it. - New convert function for type conversion, plus a set of functions with an assume... prefix that "give OpenTofu additional hints about what's expected as the final result of an unknown value" — useful for reducing unknown-value noise in plan output. - Linting support (experimental), enabled with a -lint flag on supported commands. - Symbol libraries (experimental) — a new capability to define reusable functions and types in hcl-lang based libraries. - Unicode 17 for all string processing. - Plan files now include the provider schemas needed to render the plan, which matters if you consume .tfplan files outside the CLI. What Breaks in OpenTofu 1.13: Upgrade Notes Three things worth checking before you upgrade a CI pipeline to 1.13: - The winrm provisioner connection type is removed. If anything in your config still uses WinRM for Windows provisioning, it's gone — OpenSSH is the supported path on modern Windows. - base64gzip output changed. Results are "equivalent to but not equal to" previous output, due to optimized DEFLATE compression — don't diff old and new base64gzip output expecting a byte match. - macOS 13 Ventura is now the minimum. And 1.13 is explicitly called out as the last release series shipping official 32-bit CPU builds. None of these are exotic — they're the kind of thing that silently breaks a pipeline that hasn't been touched in a year. The Feature Terraform Still Doesn't Have: State Encryption OpenTofu's state and plan encryption isn't new to 1.13 — it shipped in an earlier release, and 1.13 extends it — but it remains the clearest structural difference between OpenTofu and Terraform. Per OpenTofu's encryption docs, it's stable, GA functionality, not experimental: "If an attacker were to gain access to your state file, they should not be able to read it and use the sensitive values (e.g. access keys) contained in the state file." The config lives inside the terraform block: terraform { encryption { key_provider "pbkdf2" "mykey" { passphrase = var.state_passphrase # minimum 16 characters } method "aes_gcm" "mymethod" { keys = key_provider.pbkdf2.mykey } state { method = method.aes_gcm.mymethod } plan { method = method.aes_gcm.mymethod } } } Supported key providers go beyond a passphrase: AWS KMS, GCP KMS, Azure Vault, OpenBao's Transit Secret Engine (compatible with HashiCorp Vault 1.14), and a generic external-program option for anything else. 1.13 specifically extended this: GCP KMS gained additional_authenticated_data support, AWS KMS gained an encryption_context field, and OpenBao gained an associated_data argument — all ways to bind the encryption to additional context so a stolen key alone isn't enough to decrypt a state file from a different environment. Terraform has no equivalent built-in feature. State-at-rest protection in Terraform means encrypting the storage backend itself (S3 bucket encryption, for instance) — the state content isn't encrypted by Terraform's own logic the way OpenTofu's encryption block does it. Two Quiet But Real Bug Fixes Two fixes in 1.13 are worth knowing even if they don't affect your specific setup: - connection.script_path escaping now prevents command injection. If a provisioner script path was ever built from a variable, this closes a real injection vector. - SSH proxy connection errors now report properly instead of crashing the run — previously a connection failure could crash rather than surface a readable error. Should You Upgrade Now? If you're not using WinRM provisioners and you're on macOS 13+ already, there's no real reason to hold back — the breaking changes are narrow and well-documented. If you're on an older macOS or a 32-bit build target, plan the migration now: 1.13 is the last release series supporting 32-bit builds, and there won't be a grace period on the next major bump. For a broader look at whether OpenTofu is the right call over Terraform for your team at all — licensing, provider registry compatibility, migration steps — see our OpenTofu vs Terraform guide. And if Terragrunt is part of your stack, it works the same way on top of either engine — see our Terragrunt vs Terraform guide for what it actually adds. If you're keeping module docs in sync as part of that CI pipeline, our terraform-docs GitHub Action guide covers automating that. Frequently Asked Questions Q: When did OpenTofu 1.13 release? A: September 30, 2026, per the official GitHub releases page. Q: Does OpenTofu 1.13 support Windows on ARM64? A: Yes, officially for the first time in this release, per the changelog. Q: Is OpenTofu's state encryption a new feature in 1.13? A: No — it's an existing, stable feature that 1.13 extended with additional key-provider options (GCP KMS authenticated data, AWS KMS encryption context, OpenBao associated data). The core encryption capability predates this release. Q: Does Terraform have state encryption like OpenTofu? A: Not as a built-in feature. Terraform relies on backend-level encryption (like S3 bucket encryption) rather than encrypting state file contents itself the way OpenTofu's encryption block does. Q: What breaks when upgrading to OpenTofu 1.13? A: The winrm provisioner connection type is removed, base64gzip output changed (not byte-identical to older output), and the minimum macOS version is now 13 Ventura. Quick Summary: - OpenTofu 1.13.0 released September 30, 2026: official Windows ARM64 support, new convert/assume... functions, experimental linting and symbol libraries - Breaking on upgrade: WinRM provisioner removed, base64gzip output changed, macOS 13+ required, last release with 32-bit builds - State and plan encryption remains OpenTofu's biggest structural advantage over Terraform — stable, GA, supports AWS/GCP/Azure KMS, OpenBao, and passphrase-based keys - 1.13 extended encryption with additional-authenticated-data options for GCP KMS, AWS KMS, and OpenBao - Two security-relevant bug fixes: a connection.script_path injection vector closed, SSH proxy errors now report instead of crashing Check the official OpenTofu changelog before pinning 1.13 in CI if you're running WinRM provisioners or 32-bit builds — those are the two changes actually capable of breaking a pipeline.

  9. OpenTofu 1.13 Re-Encodes base64gzip and Drops WinRM. We Upgraded the Same State to See What Breaks --- title: OpenTofu 1.13 Re-Encodes base64gzip and Drops WinRM. We Upgraded the Same State to See What Breaks published: true date: 2026-10-07 09:00:00 UTC tags: Terraform,OpenTofu,InfrastructureasCode,CloudInit canonical_url: devops-daily.com --- OpenTofu 1.13.0 was published on September 30 (the release announcement is dated September 29), and 1.13.1 followed on October 1 with two fixes for ephemeral values. The headline features are new functions that let module authors describe values OpenTofu cannot know until apply, plus two experiments: built-in linting and symbol libraries. The upgrade notes are what you will notice first. base64gzip now returns different bytes for the same input, WinRM provisioners are gone, and 1.13 is the last series with official 32-bit builds. Upgrade notes tell you what changed. They do not show you your next plan, or the step at which a removed feature fails. So we tested it. We applied configurations with OpenTofu 1.12.7, ran 1.13.1 against the same state, and recorded what each command printed. This post goes through the results and ends with a checklist you can run before you change the version in CI. TLDR - base64gzip in 1.13.1 returns a different string for the same input. OpenTofu moved to Go 1.27, and Go changed its DEFLATE encoder. Both outputs decompress to identical bytes, but providers compare the string. With no config change, our plan was 1 to add, 1 to change, 1 to destroy. - On aws_instance, a changed user_data_base64 means a stop/start, or a replacement if you set user_data_replace_on_change. On azurerm_linux_virtual_machine, a changed custom_data forces a new VM. - ignore_changes hides the diff. In our test it also hid a real edit to the cloud-init file. Gzip done inside the cloudinit provider did not change at all. - A winrm provisioner passes tofu validate and tofu plan on 1.13.1, with the old "will be removed in a future version" warning. It fails at apply, after the resource exists, and leaves the resource tainted. - assumenotnull fixed a classic Invalid count argument error. assumestringprefix moved a mis-wired module input from a mid-apply failure to a plan error. Older versions reject these functions, and since 1.12 a required_version in a .tf file does not stop them. - -lint=all is a useful experiment, but it only adds warnings. The exit code stays 0. Prerequisites - An OpenTofu 1.12.x codebase, or a Terraform codebase that you plan to move to OpenTofu - A CI job or shell that can run tofu plan with read access to your real state - jq, for the plan JSON recipe - The 1.13.1 zip for your platform from the GitHub release page, checked against its SHA256SUMS file How we tested We ran everything on a Raspberry Pi with a 64-bit (arm64) OS: OpenTofu 1.12.7 and 1.13.1 for linux_arm64, the 1.13.1 linux_arm (32-bit) build, and Terraform 1.16.5 for comparison. Every zip matched its published SHA256 sum. In the outputs below, tofu-1.12.7 and tofu-1.13.1 are the two release binaries side by side. We did not use a cloud account. The built-in terraform_data resource stands in for provider attributes. Its input argument updates in place when the value changes, and its triggers_replace argument forces a replacement. Those are the two ways real providers treat user data, which we checked in the provider docs. Every output in this post comes from these runs. Where we cut lines from an output, the block shows ... or the grep/tail we used, and long base64 strings are shortened with .... base64gzip: same input, different string base64gzip compresses a string with gzip and base64-encodes the result. Its most common job is cloud-init user data, because EC2 limits user data to 16 KB and compression gives you more room. Here is the same call on both versions, plus Terraform 1.16.5, and then a check of what a real payload (a 1,208-byte cloud-init file that installs nginx and writes a systemd unit) decompresses to: base64gzip across versions # same input, three binaries $ echo 'base64gzip("hello")' | ./tofu-1.12.7 console "H4sIAAAAAAAA/8pIzcnJBwAAAP//AQAA//+GphA2BQAAAA==" $ echo 'base64gzip("hello")' | ./tofu-1.13.1 console "H4sIAAAAAAAA/wAFAPr/aGVsbG8AAAD//wMAhqYQNgUAAAA=" $ echo 'base64gzip("hello")' | ./terraform-1.16.5 console "H4sIAAAAAAAA/8pIzcnJBwAAAP//AQAA//+GphA2BQAAAA==" # decompress the cloud-init payload from each version $ echo 'base64gzip(file("cloud-init.yaml"))' | ./tofu-1.12.7 console | tr -d '"' | base64 -d | gunzip | sha256sum 3a62ee85006428492dc1aeccc789867735496026ada3bfaef3d0cf00dcc1bcd1 - $ echo 'base64gzip(file("cloud-init.yaml"))' | ./tofu-1.13.1 console | tr -d '"' | base64 -d | gunzip | sha256sum 3a62ee85006428492dc1aeccc789867735496026ada3bfaef3d0cf00dcc1bcd1 - $ sha256sum < cloud-init.yaml 3a62ee85006428492dc1aeccc789867735496026ada3bfaef3d0cf00dcc1bcd1 - The encoded strings differ. The content does not: both outputs decompress to the exact bytes of the source file. The cause is upstream. OpenTofu 1.12.7 is built with Go 1.26.6 and 1.13.1 with Go 1.27.1 (each binary records its Go version). The Go 1.27 release notes say that "the exact encoded output from Writer may be different from Go 1.26 as a result of the encoder implementation change", and that this carries through to compress/gzip. The OpenTofu 1.13 changelog calls the new output "equivalent to _but not equal to_" the output of earlier releases. The new output is stable: three runs of 1.13.1 gave the same string. Terraform 1.16.5 is built with Go 1.26.8 and produced the same bytes as OpenTofu 1.12.7. So you also get this diff when you move from Terraform 1.16 to OpenTofu 1.13, not only when you upgrade OpenTofu. What the plan shows 1. cloud-init.yaml unchanged 2. base64gzip() Go 1.27 DEFLATE 3. New string same bytes after gunzip 4. Provider diff string != state 5. Plan update or replace This is the config we applied with 1.12.7: locals { user_data = base64gzip(file("${path.module}/cloud-init.yaml")) } # Stands in for an attribute that a provider updates in place, # such as user_data_base64 on aws_instance. resource "terraform_data" "web_in_place" { input = local.user_data } # Stands in for an attribute that forces replacement, # such as custom_data on azurerm_linux_virtual_machine. resource "terraform_data" "web_replace" { triggers_replace = local.user_data } After tofu-1.12.7 apply, a 1.12.7 plan reported no changes. Then we planned with 1.13.1 against the same state and the same config: $ tofu-1.13.1 plan terraform_data.web_replace: Refreshing state... [id=4437d213-ef2e-bd8b-9c81-b5831d1b46c8] terraform_data.web_in_place: Refreshing state... [id=b0ec5936-31c0-4653-0fb6-e353122675fa] OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place (current -> planned) -/+ destroy and then create replacement OpenTofu will perform the following actions: # terraform_data.web_in_place will be updated in-place ~ resource "terraform_data" "web_in_place" { id = "b0ec5936-31c0-4653-0fb6-e353122675fa" ~ input = "H4sIAAAAAAAA/5RU3U7zRhC9z..." -> "H4sIAAAAAAAA/5RTXW/jNhB89..." ~ output = "H4sIAAAAAAAA/5RU3U7zRhC9z..." -> (known after apply) } # terraform_data.web_replace must be replaced -/+ resource "terraform_data" "web_replace" { ~ id = "4437d213-ef2e-bd8b-9c81-b5831d1b46c8" -> (known after apply) ~ triggers_replace = "H4sIAAAAAAAA/5RU3U7zRhC9z..." -> "H4sIAAAAAAAA/5RTXW/jNhB89..." } Plan: 1 to add, 1 to change, 1 to destroy. Two resources, no edits, one update and one replacement. On real resources, the result depends on the provider: - aws_instance: for user_data_base64, where gzip output belongs, "Updates to this field will trigger a stop/start of the EC2 instance by default. If the user_data_replace_on_change is set then updates to this field will trigger a destroy and recreate of the EC2 instance." - azurerm_linux_virtual_machine: for custom_data, "Changing this forces a new resource to be created." - aws_launch_template: a changed user_data creates a new template version. Instances get it the next time your Auto Scaling group launches or refreshes from that version. None of this is a disaster if you plan for it. But a stop/start of a single production box at 2 p.m. is the kind of surprise you want to catch in plan review, not after someone approves an apply because "it is only a version bump". Find it before the upgrade Start with a search. Run it after tofu init, so that it also covers registry and git modules in .terraform/modules: grep -rnE --include='*.tf' --include='*.tofu' 'base64gzip\(|"winrm"' . On our test folder it found both problems this post covers: main.tf:2: user_data = base64gzip(file("${path.module}/cloud-init.yaml")) winrm/main.tf:6: type = "winrm" A search only finds literal calls. A plan is the reliable check. Run a 1.13.1 plan in a throwaway job, save it, and list each changed attribute with this jq filter (save it as changed-attrs.jq): .resource_changes[] | select(.change.actions != ["no-op"]) | . as $r | [($r.change.before // {}) | keys[] | select($r.change.before[.] != $r.change.after[.] and ($r.change.after_unknown[.] | not)) ] | "\($r.change.actions | join("+")) \($r.address) changed: \(join(", "))" $ tofu-1.13.1 plan -lock=false -out=upgrade.tfplan > /dev/null $ tofu-1.13.1 show -json upgrade.tfplan | jq -r -f changed-attrs.jq update terraform_data.web_in_place changed: input delete+create terraform_data.web_replace changed: triggers_replace A plan does not write state, and -lock=false stops a dry-run job from blocking a real apply. Remove the flag if you prefer to wait for the lock. You want a list in which every changed attribute is user data. Anything else in the list is real drift or a different upgrade change, so examine it separately. Three ways to handle it Handling the base64gzip diff Accept it # Plan with 1.13.1, review, and apply in a window you choose. # The content is identical, so the only effect is the # stop/start or replacement itself. tofu plan -out=upgrade.tfplan tofu show -json upgrade.tfplan | jq -r -f changed-attrs.jq tofu apply upgrade.tfplan Hide it (temporary) resource "aws_instance" "web" { # ... user_data_base64 = base64gzip(file("${path.module}/cloud-init.yaml")) lifecycle { # TEMPORARY: OpenTofu 1.13 re-encodes base64gzip output. # This also hides real cloud-init edits. Remove it on the next change. ignore_changes = [user_data_base64] } } Gzip in the provider data "cloudinit_config" "web" { gzip = true base64_encode = true part { content_type = "text/cloud-config" content = file("${path.module}/cloud-init.yaml") } } resource "aws_instance" "web" { # ... user_data_base64 = data.cloudinit_config.web.rendered } Accept it. This is usually the correct choice. Do the upgrade apply in a maintenance window, one environment at a time, and use the jq list as the change record. Hide it. The release notes suggest ignore_changes as a temporary fix, and it works: with ignore_changes on both test resources, the 1.13.1 plan said No changes. Then we added a line to cloud-init.yaml and planned again. It still said No changes. > Warning > > ignore_changes cannot tell the encoder change from a real edit. While it is in place, changes to your cloud-init file do not reach your instances, and the plan does not tell you. If you use it, open a ticket to remove it, and remove it on the next intentional user data change. Move the gzip into the provider. With cloudinit_config and gzip = true, the compression runs in the provider binary, not in OpenTofu. We rendered the same file with hashicorp/cloudinit v2.4.1 under both 1.12.7 and 1.13.1, and the SHA256 of rendered was identical. This does not make you immune. It moves the dependency: v2.4.1 is built with Go 1.26.8, and a future provider release built with Go 1.27 can cause the same one-time diff. So pin the provider version and read its changelog. The switch itself also changes your user data once, because cloudinit_config wraps parts in a MIME multi-part document. Do it in the same window as the upgrade. WinRM: passes validate and plan, fails at apply OpenTofu 1.12 deprecated the winrm connection type, and 1.13 removed it (#4012) because "some of the upstream libraries OpenTofu was using to implement these features are no longer maintained". We expected tofu validate to report it. It does not. This is the test config (the host is a closed local port with a short timeout, so that the run fails fast): resource "terraform_data" "bootstrap" { provisioner "remote-exec" { inline = ["powershell -Command Install-WindowsFeature Web-Server"] connection { type = "winrm" host = "127.0.0.1" user = "Administrator" password = "example-only" https = true timeout = "10s" } } } $ tofu-1.13.1 validate Warning: WinRM connection type is deprecated on main.tf line 6, in resource "terraform_data" "bootstrap": 5: connection { 6: type = "winrm" The winrm connection type is deprecated and will be removed in a future version of OpenTofu. ... Success! The configuration is valid, but there were some validation warnings as shown above. $ tofu-1.13.1 plan | grep 'Plan:' Plan: 1 to add, 0 to change, 0 to destroy. $ time tofu-1.13.1 apply -auto-approve ... Error: remote-exec provisioner error with terraform_data.bootstrap, on main.tf line 2, in resource "terraform_data" "bootstrap": 2: provisioner "remote-exec" { 'winrm' connections are not supported in OpenTofu v1.13 or later Error: Provisioners no longer support WinRM ... real 0m0.153s $ tofu-1.13.1 show | head -2 # terraform_data.bootstrap: (tainted) resource "terraform_data" "bootstrap" { 1.13.1 still prints the 1.12 deprecation warning at validate and plan. It says the feature "will be removed in a future version", but the feature is already gone in this version. The error comes in under a second at apply. For comparison, 1.12.7 tried to connect to port 5986 and failed at the 10-second timeout, as it should. The order matters. Provisioners run after the resource is created, and the OpenTofu docs say "if a creation-time provisioner fails, the resource is marked as tainted" and "will be planned for destruction and recreation upon the next tofu apply". With a real Windows VM, the VM is created and billed, then marked for replacement, and each apply after that recreates it and fails again until you remove the provisioner. Existing VMs whose provisioners ran long ago are not affected until something replaces them. Be careful with that last point: if a Windows VM uses gzipped custom_data, the base64gzip change above is exactly the kind of thing that replaces it, and then its WinRM provisioner runs again and fails. To fix it, move to SSH or remove the provisioner: - Windows Server 2019 and later can run OpenSSH Server. Set type = "ssh" and target_platform = "windows" in the connection block. If the SSH default shell is PowerShell, the connection docs also tell you to set script_path to a .ps1 path. - Better, if you can: bake the configuration into the image, or run it from custom_data or user_data, so that no provisioner has to connect at all. The grep above finds literal "winrm" strings. It does not find type = var.connection_type, so also search for connection blocks whose type comes from a variable. 32-bit builds: last series The 1.13 changelog says this is "the final release series that will include official builds for 32-bit CPU architectures" (*_386 and *_arm). The Pi's 64-bit kernel can run 32-bit ARM binaries, so we ran the linux_arm build: $ uname -m aarch64 $ tofu-1.13.1-linux-arm version OpenTofu v1.13.1 on linux_arm $ tofu-1.13.1-linux-arm init Warning: Support for 32-bit CPU architectures is ending soon OpenTofu v1.13 is the last release series that will include official release packages for 32-bit CPU architectures. We recommend planning to migrate to a 64-bit CPU architecture instead. Alternatively, you could build OpenTofu for linux_arm from source code yourself, ... Look at the second line of tofu version on each runner. If it says on linux_arm or on linux_386, that runner has to move. Common cases are 32-bit Raspberry Pi OS runners, old ARMv7 boards, and i386 container images. As the test shows, a 64-bit kernel does not help if the image or the binary you install is 32-bit. You have time: per the changelogs, the 1.13 series is supported until August 1, 2027, and 1.12 until February 1, 2027. The 1.11 series lost support on August 1, 2026. The new functions: hints for unknown values This is the main feature of the release. During a plan, any value that an API assigns at creation time is unknown, and OpenTofu cannot use an unknown value to decide how many instances to create. Here is the classic case: a network module creates a VPC, and a second module creates flow logs only when it gets a VPC ID. # modules/network/main.tf # terraform_data stands in for aws_vpc: its output is unknown until apply, # the same way a VPC id is decided by the AWS API at creation time. resource "terraform_data" "vpc" { input = "vpc-0a1b2c3d4e5f60718" } output "vpc_id" { value = terraform_data.vpc.output } # modules/flow_logs/main.tf variable "vpc_id" { type = string default = null } resource "terraform_data" "flow_log" { count = var.vpc_id != null ? 1 : 0 input = var.vpc_id } On a first plan, 1.12.7 and 1.13.1 fail the same way: Error: Invalid count argument on modules/flow_logs/main.tf line 7, in resource "terraform_data" "flow_log": 7: count = var.vpc_id != null ? 1 : 0 The "count" value depends on resource attributes that cannot be determined until apply, so OpenTofu cannot predict how many instances will be created. ... The assume... functions let the module author state what is true about the value even before it exists. Our first attempt failed: Error: Invalid function argument on modules/network/main.tf line 8, in output "vpc_id": 8: value = assumenotnull(terraform_data.vpc.output) Invalid value for "value" parameter: given value must have a known type; consider using the \"convert\" function to specify a type to assume. terraform_data.output takes on the type of input, so its type is also unknown during the plan. Most provider attributes, such as aws_vpc.id, are typed strings and do not have this problem. For a value like this, the docs tell you to combine the hint with the new convert function: output "vpc_id" { value = assumenotnull(convert(terraform_data.vpc.output, string)) } $ tofu-1.13.1 plan | grep -E 'will be created|Plan:' # module.flow_logs.terraform_data.flow_log[0] will be created # module.network.terraform_data.vpc will be created Plan: 2 to add, 0 to change, 0 to destroy. $ tofu-1.13.1 apply -auto-approve | tail -1 Apply complete! Resources: 2 added, 0 changed, 0 destroyed. The usual workaround for this error is a two-step apply with -exclude (the error message itself suggests it). Here, one plan is enough. A hint is a promise, and OpenTofu checks it. The docs say that if the value turns out to be null, "the function raises an error", so the apply fails. Use a hint only where the provider really guarantees it, for example that an ID is never null after create. Catch wiring mistakes at plan time assumestringprefix is useful together with variable validation. Here the caller connects the subnet output to an input that expects a VPC ID, a mistake that is easy to miss in review: # main.tf module "flow_logs" { source = "./modules/flow_logs" vpc_id = module.network.subnet_id # wrong output wired in } # modules/flow_logs/main.tf variable "vpc_id" { type = string validation { condition = startswith(var.vpc_id, "vpc-") error_message = "vpc_id must be a VPC id (vpc-...)." } } Without hints, the plan passes because the value is unknown, and validation waits for apply. The apply created the VPC and the subnet, and then failed. Both outputs are filtered to the key lines, and the IDs are shortened: $ tofu-1.13.1 plan Plan: 3 to add, 0 to change, 0 to destroy. $ tofu-1.13.1 apply -auto-approve module.network.terraform_data.subnet: Creation complete after 0s [id=...] module.network.terraform_data.vpc: Creation complete after 0s [id=...] Error: Invalid value for variable vpc_id must be a VPC id (vpc-...). $ tofu-1.13.1 state list module.network.terraform_data.subnet module.network.terraform_data.vpc Then we added prefix hints to the network module outputs: output "vpc_id" { value = assumenotnull(assumestringprefix(convert(terraform_data.vpc.output, string), "vpc-")) } output "subnet_id" { value = assumenotnull(assumestringprefix(convert(terraform_data.subnet.output, string), "subnet-")) } Now the same mistake fails the plan, before anything is created: $ tofu-1.13.1 plan Error: Invalid value for variable on main.tf line 7, in module "flow_logs": 7: vpc_id = module.network.subnet_id # wrong output wired in ├──────────────── │ var.vpc_id is a string vpc_id must be a VPC id (vpc-...). This was checked by the validation rule at modules/flow_logs/main.tf:4,3-13. If you maintain shared modules, the outputs of your network, IAM and DNS modules are the best places for these hints. Callers get the benefit without changing their own code. assumeequal goes further: the docs show it with the AWS provider's arn_build function to make an IAM role ARN fully known at plan time, so that policy checks can see the real policy document. Guard the module version correctly A module that uses these functions does not work on older versions, and the errors are not clear. On 1.12.7, assumenotnull(...) alone gives Call to unknown function. With convert(..., string), the error is Invalid reference, because 1.12 reads string as a resource address. Terraform 1.16.5 also gives Call to unknown function. You would usually add required_version = ">= 1.13.0" to a terraform block. That does not work here. Since 1.12, OpenTofu ignores required_version in .tf files and honors it only in .tofu files (see the RFC tracking issue and the settings docs). We tested this with a constraint that no version can meet: | Constraint and file | 1.12.7 | 1.13.1 | | --- | --- | --- | | required_version = ">= 99.0" in main.tf | ignored, validate passes | ignored, validate passes | | required_version = ">= 99.0" in versions.tofu | Incompatible module | Incompatible module | | language { compatible_with { opentofu = ">= 1.13" } } in versions.tofu | Incompatible module | plan passes | So put the guard in a .tofu file: # versions.tofu (the language block needs OpenTofu 1.12 or later) language { compatible_with { opentofu = ">= 1.13" } } On 1.12.7 this gives This module is not compatible with OpenTofu v1.12.7, which is the clear message you want. If a module must also work with Terraform, use the .tofu precedence rule: when outputs.tf and outputs.tofu are both present, OpenTofu loads only the .tofu file. We put a hinted output in outputs.tofu and a plain one in outputs.tf. tofu validate (1.13.1) and terraform validate (1.16.5) both passed. The lint experiment validate, plan, apply and refresh accept a new -lint flag. 1.13 has four rules, all for the root module only: core:no-type-variable, core:unused-variable, core:unused-local, and core:count-instead-enabled. The last one suggests the enabled lifecycle argument instead of count = cond ? 1 : 0. We ran the experiment on a small file that breaks all four rules: $ tofu-1.13.1 validate -lint=all -json | jq -r '.diagnostics[] | select(.severity == "warning") | .summary' Experimental linting enabled Input variable not used (core:unused-variable) Local value not used (core:unused-local) Could use enabled instead of count (core:count-instead-enabled) Variable with no type (core:no-type-variable) The exit code was 0. Lint results are warnings, and OpenTofu always adds an "Experimental linting enabled" warning. You can turn off one rule with !, for example -lint='all,!core:unused-variable'. The linting docs say that rules can change even in minor releases. For now, make it a non-blocking CI step and read the output. Do not gate merges on it yet. Smaller changes worth a line - Plan text. After No changes. Your infrastructure matches the configuration., 1.12.7 printed one more paragraph ("OpenTofu has compared your real infrastructure ... no changes are needed."). 1.13.1 does not. If a script greps for that paragraph, change it to use tofu plan -detailed-exitcode, which returns 0 for no changes, 1 for errors, and 2 for changes. Our upgrade plan returned 2. - Platforms. Windows on ARM64 is now an official platform, and macOS builds require macOS 13 Ventura or later. - State encryption. The aws_kms key provider accepts an encryption_context. gcp_kms accepts additional_authenticated_data, and openbao accepts associated_data. - Crash recovery. A Go panic now writes a partial errored.tfstate to help you recover. - Saved plans include the provider schemas, so tofu show on a plan file usually does not need to start providers. - If you stay on 1.12 for now , take 1.12.7. It fixes a deadlock that an attacker-controlled SSH server could cause in remote-exec and file provisioners (CVE-2026-78662). Upgrade checklist 1. After tofu init, search your code and .terraform/modules for base64gzip( and "winrm", and look for connection blocks whose type comes from a variable. 2. Remove WinRM provisioners before you upgrade. Validate and plan will not stop you, and a failure at apply taints the resource. 3. Run a 1.13.1 plan against real state in a throwaway job, save it, and run the jq filter. Every changed attribute should be user data. 4. For each user data change, decide: accept the stop/start or replacement in a window, or use a temporary ignore_changes with a ticket to remove it. 5. Check tofu version on every runner and workstation image, and move anything on linux_arm or linux_386 to 64-bit before August 1, 2027. 6. Change scripts that read plan text to use -detailed-exitcode. 7. In shared modules, add assume... hints to outputs whose IDs callers use in count, for_each or validation. Put a language block in a .tofu file to guard them. 8. Add -lint=all to CI as a non-blocking step and see what it finds. The upgrade is not hard, but two of these changes are not visible until a specific step: the base64gzip diff shows only in a plan against real state, and the WinRM removal shows only at apply. If you run steps 1 to 3 first, you see both before they affect a real server. _Originally published at devops-daily.com._

    5 reactions

Achievements

Awards earned by the repositories opentofu owns. Select an award to embed its badge.

Gold
141st place finishes
Silver
22nd place finishes
Bronze
93rd place finishes
Awarded repositories
2Owned by opentofu now

Repositories

Ordered by gold, then silver, then bronze.

  1. manifestoBest: 1st by stars, 26,326 stars816
  2. opentofuBest: 1st by stars, 5,797 stars613

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. opentofu
  2. opentofu
  3. opentofu
  4. opentofu
  5. opentofu
  6. opentofu
  7. opentofu
  8. opentofu

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.