hashicorp

Activity

Stars and mentions · Last 30 days, Oct 6 – Nov 4

GitHub stars378
Mentions63
010203040Oct 6Oct 20Nov 4
UTC days · repositories hashicorp owns22 repositories

Top repositories

hashicorp’s repositories ranked by new forks, as on trending.

Loading

Contributes to

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

Who noticed hashicorp

Everyone who starred or forked hashicorp’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

67posts
Nov 54 posts
  1. I had always been using HashiCorp's set library whenever I was creating sets in Go because map[string]struct{} just looked mind boggling to me. hashicorp/go-set

  2. GitHub ActionsでTerraformのCIを改善する はじめに これまで、GitHub Actionsを使ってTerraformの品質チェックやセキュリティチェックを自動化する方法を試して見ました! これまでに書いた記事は以下の2つです。 GitHub ActionsとTrivyでTerraformの設定ミスをPR時に検出する GitHub ActionsでTerraformの品質チェックを自動化する これまで作成したWorkflowをさらに改善するために、以下の3つに取り組みました。 Reusable WorkflowsによるWorkflowの再利用 Path FilterによるWorkflowの実行条件の制御 Terraform Provider CacheによるProviderのキャッシュ 1. Reusable WorkflowsでWorkflowを再利用する これまでのWorkflowでは、Terraformの品質チェックを実行する処理を1つのYAMLファイルにまとめていました。 しかし、今後セキュリティチェックやテストなどを追加していくことを考えると、Workflowごとに処理を分離しておく方が管理しやすいかなと思いました。 そこで、Reusable Workflowsを利用してみました! Reusable Workflowsは、別のWorkflowから呼び出して再利用できる仕組みです。 今回の構成では、以下のように役割を分けています。 呼び出し側:Workflowを実行する条件を定義 呼び出される側:Terraformの品質チェックを実行 呼び出し側のWorkflow .github/workflows/pr-check.yml name: Terraform PR Check on: pull_request: paths: - '**/*.tf' permissions: contents: read jobs: terraform-quality: uses: ./.github/workflows/terraform-quality.yaml uses に呼び出したいWorkflowのパスを指定します。 これにより、Terraformの品質チェック処理を別のWorkflowに切り出すことができます。 呼び出される側のWorkflow .github/workflows/terraform-quality.yaml name: Terraform Quality on: workflow_call: permissions: contents: read jobs: terraform-quality: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup Terraform uses: hashicorp/setup-terraform@v4 - name: Check formatting run: terraform fmt -check -recursive - name: Initialize Terraform run: terraform init -backend=false - name: Validate Terraform run: terraform validate 呼び出される側では、on: workflow_call を指定します。 これにより、別のWorkflowから呼び出せるようになります。 Terraformの品質チェック処理を独立させることで、今後ほかのWorkflowからも再利用しやすい構成にしました。 2. Path FilterでWorkflowの実行条件を制御する 次に、Workflowが実行される条件を見直しました。 Pull Requestを作成するたびにTerraformの品質チェックを実行するのではなく、Terraformファイルに変更があった場合だけ実行するようにします。 そこで、呼び出し側のWorkflowに以下を設定しました。 on: pull_request: paths: - '**/*.tf' paths を指定することで、対象となるファイルに変更があった場合のみWorkflowが実行されます。 今回の設定では、.tf ファイルが変更されたPull Requestが対象になります。 実際に動作を確認したところ、WorkflowのYAMLファイルだけを変更した場合は実行されず、Terraformファイルを変更した場合は実行されることを確認できました。 これにより、Terraformに関係のない変更で不要なCIが実行されることを抑えられます。 3. Terraform Providerをキャッシュする 次に、Terraform Providerのキャッシュを導入してみました。 Terraformでは、terraform init を実行すると、必要なProviderがダウンロードされます。 毎回ダウンロードが発生するため、Providerをキャッシュして再利用できるようにします。 今回は、Terraformの環境変数 TF_PLUGIN_CACHE_DIR とGitHub Actionsの actions/cache を利用しました。 Provider Cacheの設定 まず、Jobの環境変数にキャッシュ先を指定します。 env: TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform-plugin-cache TF_PLUGIN_CACHE_DIR は、Terraform Providerのキャッシュ先を指定するための環境変数です。 次に、WorkflowのStepにキャッシュ処理を追加します。 - name: Create Terraform plugin cache directory run: mkdir -p "$TF_PLUGIN_CACHE_DIR" - name: Cache Terraform providers uses: actions/cache@v6 with: path: ${{ github.workspace }}/.terraform-plugin-cache key: ${{ runner.os }}-terraform-providers-${{ hashFiles('**/.terraform.lock.hcl') }} restore-keys: | ${{ runner.os }}-terraform-providers- それぞれの設定について説明します。 path キャッシュ対象のディレクトリを指定します。 今回は、TF_PLUGIN_CACHE_DIR で指定したディレクトリと同じ場所にしています。 key キャッシュを識別するためのキーです。 今回は、実行環境のOSと .terraform.lock.hcl のハッシュ値を組み合わせています。 ロックファイルの内容が変わるとハッシュ値も変わるため、Providerの依存関係に変更があった場合に、別のキャッシュキーを利用できます。 restore-keys 完全に一致するキャッシュが見つからなかった場合に、前方一致するキャッシュを探すための設定です。 今回はOSとProviderキャッシュのプレフィックスを指定しています。 Workflow全体 最終的なReusable Workflowは以下のようになりました。 name: Terraform Quality on: workflow_call: permissions: contents: read jobs: terraform-quality: runs-on: ubuntu-latest env: TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform-plugin-cache steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup Terraform uses: hashicorp/setup-terraform@v4 - name: Create Terraform plugin cache directory run: mkdir -p "$TF_PLUGIN_CACHE_DIR" - name: Cache Terraform providers uses: actions/cache@v6 with: path: ${{ github.workspace }}/.terraform-plugin-cache key: ${{ runner.os }}-terraform-providers-${{ hashFiles('**/.terraform.lock.hcl') }} restore-keys: | ${{ runner.os }}-terraform-providers- - name: Check formatting run: terraform fmt -check -recursive - name: Initialize Terraform run: terraform init -backend=false - name: Validate Terraform run: terraform validate キャッシュの復元処理は terraform init より前に実行する必要があります。 これにより、Terraformの初期化時にキャッシュされたProviderを利用できるようになります。 4. Provider Cacheの動作を確認する 設定したキャッシュが実際に動作するか、GitHub Actions上で確認しました。 今回は、同じPull RequestでWorkflowを2回実行し、キャッシュの保存と復元を確認しています。 1回目の実行 初回実行ではキャッシュが存在しないため、Providerのダウンロードが行われます。 実行ログでは、キャッシュが見つからないことを示すメッセージが出力されました。 Cache not found for input keys: その後、Workflowの最後に以下のログが出力されました。 Cache saved with key: ... これにより、Providerのキャッシュが保存されたことを確認できました。 2回目の実行 次に、同じPull Requestで Re-run all jobs を実行しました。 2回目の実行では、以下のログが出力されました。 Cache restored successfully これにより、1回目の実行で保存したキャッシュを復元できることを確認できました。 今回の検証では、キャッシュの保存と復元の両方を確認できました。 なお、キャッシュの復元によってProviderの再ダウンロードを抑制できる構成にはなりましたが、実行時間がどの程度短縮されたかについては、まだ比較検証できていません。ただ、Providerのダウンロードを省略できる分、実行時間の短縮につながると考えています! また、GitHub ActionsのキャッシュはPull Requestの実行スコープに関係するため、今回は同じPull Requestで再実行して確認しています。 5. まとめ 今回は、GitHub ActionsでTerraformのCIを改善するために、以下の3つを実装しました。 Reusable WorkflowsによるWorkflowの共通化 Path Filterによる実行条件の制御 Terraform Provider CacheによるProviderのキャッシュ 実際に設定して動作を確認することで、それぞれの仕組みについて理解を深めることができました。 特に、Provider Cacheについては、設定するだけでなく、GitHub Actionsの実行ログからキャッシュの保存と復元まで確認できたのがよかったです。 引き続き、GitHub ActionsやTerraformを使いながら、CI/CDの改善について学んでいきたいと思います。 参考 GitHub Actionsの再利用可能なワークフロー GitHub Actionsのワークフロー構文 GitHub Actionsの依存関係キャッシュ actions/cache Terraform fmt Terraform init Terraform validate hashicorp/setup-terraform

  3. When Terraform Plans Lie: Two GKE Issues and How to Catch Them A provider bug that wipes a GKE cluster's disruption budget, and a module bug whose plan never settles. We trace both to the provider and module source, reproduce them in a throwaway project, weigh the workarounds, and build the post-apply gate that catches this whole class of drift. Here is a sprint that looks routine. A platform team runs four GKE clusters from Terraform. One is a production Autopilot cluster in us-central1 running stateful services that take a long time to rebalance after a node restart, so it carries a disruption budget: at least 45 days between minor upgrades and 3 days between patch upgrades. The other three are Standard private clusters built from the community module's beta-private-cluster-update-variant. CI posts a plan on every pull request, applies on merge, and a nightly job runs terraform plan -detailed-exitcode on all four stacks and pages someone if the plan is not empty. Two changes go in that sprint. The production cluster moves to a recurring_maintenance_window, the newer window type with an explicit start time and duration that landed in the provider source in July 2026. The private clusters get enable_private_endpoint = true, so the control plane is reachable only from inside the VPC. Both plans look right. Both applies succeed. Afterwards, the production cluster has quietly lost its disruption budget, and the three private clusters show the same one-line diff every night that no apply ever clears. That is roughly 90 drift pages a month (3 stacks, 30 nights) that mean nothing, plus one missing setting on the cluster that matters most. This is a representative scenario, not a customer engagement, but both failures are real and open upstream: terraform-provider-google #30554 and terraform-google-kubernetes-engine #2697, both filed on 3 November 2026. Below we work through each one: what the reporter saw, what the source code says, how to reproduce it in a throwaway project, which workaround we would pick and what it costs, and the post-apply gate that catches both. > Key takeaways > > - #30554: the provider's recurring_maintenance_window branch never sends disruption_budget to GKE and never reads it back. A budget set outside Terraform is wiped without a diff. One set in Terraform, by our reading of the code, never converges. > - #2697: the private-cluster module variants render an empty master_authorized_networks_config {} whenever enable_private_endpoint = true. GKE stores nothing for it, so every plan tries to add it again. > - Best workaround for #30554: express the same window as recurring_window, whose code path does carry the budget. Setting the budget with gcloud alone gets wiped on the next maintenance policy change. > - The gate that catches both: read the live cluster after apply and require a second plan to exit 0. A check that reads through the same provider shares its blind spot. Bug 1: the budget the provider never sends What the reporter saw The report is against hashicorp/google v8.10.0 and Terraform v1.14.0, reproduced on a real Autopilot cluster. The steps: start with a cluster that has a disruption budget, confirm it in the maintenance policy, configure recurring_maintenance_window on the google_container_cluster resource, apply, and inspect the policy again. The budget is gone. The reporter's config sets both blocks: resource "google_container_cluster" "example" { name = "example-cluster" location = "us-central1" enable_autopilot = true maintenance_policy { recurring_maintenance_window { window_start_time { hours = 2 minutes = 0 seconds = 0 } window_duration = "4h" recurrence = "FREQ=DAILY" } disruption_budget { minor_version_disruption_interval = "3888000s" # 45 days patch_version_disruption_interval = "259200s" # 3 days } } } The reporter traced it to two functions in the provider source and noted that the equivalent handling already exists for daily_maintenance_window and recurring_window. They did not post plan output, so what Terraform shows before and after the apply is not part of the report. That gap matters, and we come back to it below. What the source says We read resource_container_cluster.go.tmpl in magic-modules on main as of 5 November 2026, which is where the provider's container code is generated from. The reporter's diagnosis holds. expandMaintenancePolicy() builds a budget value once, then returns from one of three branches. The daily and recurring_window branches both set DisruptionBudget: budget. The recurring_maintenance_window branch builds its policy without it: // recurring_window branch: budget included return &container.MaintenancePolicy{ Window: &container.MaintenanceWindow{ /* RecurringWindow ... */ }, DisruptionBudget: budget, ResourceVersion: resourceVersion, } // recurring_maintenance_window branch: budget computed above, never attached policy := &container.MaintenancePolicy{ Window: &container.MaintenanceWindow{ /* RecurringMaintenanceWindow ... */ }, ResourceVersion: resourceVersion, } flattenMaintenancePolicy() mirrors it. It builds a budget list from the API response, adds "disruption_budget": budget to the daily and recurring_window results, and leaves it out of the recurring_maintenance_window result. The fix is two lines, one in each function. One more detail explains why an existing budget disappears rather than just failing to update. Changes to maintenance_policy go through GKE's SetMaintenancePolicy call with the whole policy object. A policy sent without a budget replaces one that had a budget. GKE then falls back to its defaults, which the cluster disruption budget docs give as 24 hours between patch upgrades and 30 days between minor upgrades. In our scenario that means patch upgrades can land a day apart instead of three, and minor upgrades 30 days apart instead of 45. Two cases, two symptoms Whether the plan shows anything depends on where the budget lives. disruption_budget is Optional but not Computed in the schema. Read that against the flatten bug and you get two different symptoms: | Where the budget is set | What apply does | What the next plan shows | Source | | --- | --- | --- | --- | | Outside Terraform (gcloud or console), not in config | Sends a policy with no budget. GKE replaces the policy and the budget is gone | Nothing. Config has no budget, state has no budget, so there is nothing to compare | Matches the reporter's reproduction steps | | In the disruption_budget block, as in the reporter's config | Budget is never sent, so the cluster runs on defaults | Should show + disruption_budget on every plan, because flatten never writes it to state. Applying sends the same budget-less request again | Our reading of the code. Not stated in the issue. Confirm with the reproduction below | The first case is the dangerous one: a setting someone added by hand, perhaps during an incident, is removed by an unrelated Terraform change and nothing says so. The second case is noisier but still misleading. The diff says Terraform is about to add the budget. The apply succeeds, and the budget still is not there. Reproduce it, then confirm a fix Do this in a throwaway project, not by experimenting on production. An Autopilot cluster is enough. Pin the provider to the version you run (8.10.0 in the report), and keep the reproduction to one resource so nothing else in your stack affects the result: # 0. Baseline: daily window plus budget. This code path carries the budget. terraform apply -auto-approve gcloud container clusters describe repro --location us-central1 \ --format='yaml(maintenancePolicy.disruptionBudget)' # expect minorVersionDisruptionInterval: 3888000s, patchVersionDisruptionInterval: 259200s # 1. Case A: swap daily_maintenance_window for recurring_maintenance_window, keep the budget block. terraform apply -auto-approve gcloud container clusters describe repro --location us-central1 \ --format='yaml(maintenancePolicy.disruptionBudget)' # bug present: empty output terraform plan -detailed-exitcode; echo "exit=$?" # bug present (by our reading): exit=2 with "+ disruption_budget" # 2. Case B: delete the budget block from config, set it out of band, then change the window. gcloud container clusters update repro --location us-central1 \ --maintenance-minor-version-disruption-interval=3888000s \ --maintenance-patch-version-disruption-interval=259200s # edit window_duration from "4h" to "5h" terraform apply -auto-approve gcloud container clusters describe repro --location us-central1 \ --format='yaml(maintenancePolicy.disruptionBudget)' # bug present: empty output, and the plan above it showed only the window change A fixed provider passes when step 1 shows the budget in gcloud and the follow-up plan exits 0. Case B will still remove an out-of-band budget even after the fix, because the policy is replaced as a whole. That is how SetMaintenancePolicy behaves, not a bug. If a setting matters, it belongs in config. To test a patched provider before a release ships, build it from a branch and point Terraform at the binary with a dev_overrides block in your CLI config. Run the same script against it. That is fine for proving a fix. It is not how you should run production. Workarounds, and what each one costs | Option | Does the cluster get the budget? | Who owns it | Cost and risk | Pick it when | | --- | --- | --- | --- | --- | | Express the window as recurring_window (start, end, RRULE) instead | Yes. This branch sends and reads the budget | Terraform | Different syntax: absolute RFC 3339 start and end times instead of time of day plus duration. Easy to switch back later | Almost always. This is our default | | Keep recurring_maintenance_window, set budget with gcloud | Until the next maintenance_policy change in Terraform, which wipes it silently | Nobody, in practice | This is exactly Case B. Any window tweak removes the budget again | Only as an emergency stopgap, with a verification check and a ticket | | lifecycle { ignore_changes = [maintenance_policy] } | No. Hides the Case A diff, still never sends the budget | Terraform, nominally | Also hides every real change to windows and exclusions | Never for this bug | | Patched provider via dev_overrides or a private registry | Yes | You, for every future provider release | You build, sign and distribute a provider binary and rebase it until upstream merges | Only to validate a fix, or if you already run a provider fork | | Pin an older provider version | Unknown | Terraform | The field only arrived in July 2026 (magic-modules #17899) and the broken branch is still on main. Older versions have no such field to fall back to, so pinning means switching window type anyway | Not useful here | In the scenario, the production cluster's weekend window becomes this, which keeps the budget in the code path that works: maintenance_policy { recurring_window { start_time = "2026-11-07T02:00:00Z" end_time = "2026-11-07T06:00:00Z" recurrence = "FREQ=WEEKLY;BYDAY=SA,SU" } disruption_budget { minor_version_disruption_interval = "3888000s" # 45 days patch_version_disruption_interval = "259200s" # 3 days } } Two weekend windows of 4 hours give about 104 hours of maintenance availability in any 92-day period, comfortably above the 48 hours GKE requires, and each window meets the 4-hour minimum from the maintenance window docs. Switching window types sends a full SetMaintenancePolicy, so run the verification script below straight after this apply too. Bug 2: the empty block that never lands What the reporter saw The report is against beta-private-cluster-update-variant v44.3.0 with hashicorp/google 7.35.0 and Terraform 1.14. The same condition is in v47.0.0 and master. With enable_private_endpoint = true and neither master_authorized_networks nor gcp_public_cidrs_access_enabled set, every plan shows: ~ resource "google_container_cluster" "primary" { ... + master_authorized_networks_config {} } Applying changes nothing, and the next plan shows the same diff. The cause, from reading the module, is the for_each that decides whether to render the block: dynamic "master_authorized_networks_config" { for_each = var.enable_private_endpoint || var.gcp_public_cidrs_access_enabled != null || length(var.master_authorized_networks) > 0 ? [true] : [] content { gcp_public_cidrs_access_enabled = var.gcp_public_cidrs_access_enabled dynamic "cidr_blocks" { ... } } } The private endpoint flag alone makes the condition true, so an empty block goes out. For the affected cluster, gcloud ... --format='yaml(masterAuthorizedNetworksConfig)' returns nothing, so state never holds the block and each plan wants to add it. The reporter proposes dropping var.enable_private_endpoint || or rendering the block only when it carries a setting. They are careful about what they did not check: they do not know why the clause was added, they did not verify whether the provider or API also contributes to the diff, and they only read this one variant. Filling in what the report left open We followed up on those three open points on 5 November 2026. - Other variants: the condition comes from the shared template autogen/main/cluster.tf.tmpl, which adds var.enable_private_endpoint || only for private clusters. On master it is present in private-cluster, private-cluster-update-variant, beta-private-cluster, beta-private-cluster-update-variant and beta-autopilot-private-cluster. The root module and beta-public-cluster do not have it. - Why the clause exists: it arrived in #2058, merged in August 2024, titled "allow enable_private_endpoint with no master_authorized_networks". The PR description does not say what failed before it. So dropping the clause could regress whatever #2058 fixed. A fork needs to test cluster creation from scratch, not only plans on existing clusters. - Related work: open PR #2687 stops rendering the block when ip_endpoints_enabled = false (DNS-only control planes), following the same symptom in #2460. By its own table it leaves the default ip_endpoints_enabled = null case unchanged, so it would not fix the configuration in #2697. - The provider side: in the provider, an empty block expands to an authorized networks config with Enabled: true and no CIDR blocks. On read, flatten returns nothing when the API's config is absent or disabled. The provider reads it from controlPlaneEndpointsConfig.ipEndpointsConfig.authorizedNetworksConfig, while the reporter checked the top-level masterAuthorizedNetworksConfig. If the API does not keep enabled: true with zero CIDRs for this cluster shape, the result is the diff the reporter saw. We have not confirmed which layer drops it. The raw-resource reproduction below is how you would tell. Reproduce it, and isolate the layer Two runs separate the module from the provider and API. First, the raw resource, with no module involved: resource "google_container_cluster" "repro" { name = "mancfg-repro" location = "us-central1" initial_node_count = 1 # network, subnetwork and ip_allocation_policy as in your environment private_cluster_config { enable_private_nodes = true enable_private_endpoint = true master_ipv4_cidr_block = "172.16.0.32/28" } master_authorized_networks_config {} } Apply, then run terraform plan -detailed-exitcode from a host inside the VPC. If it exits 2 with + master_authorized_networks_config {}, the empty block does not survive the provider and API round trip on its own, and any module that emits it will never converge. If it exits 0, the problem is specific to how the module builds the block. Either way, also dump both API paths with gcloud ... --format=json | jq '{top: .masterAuthorizedNetworksConfig, nested: .controlPlaneEndpointsConfig.ipEndpointsConfig.authorizedNetworksConfig}' so you know what GKE actually stored. Second, the module run: the reporter's module block at v44.3.0, then the same block pointed at a vendored copy with the clause removed. The fix passes when the vendored copy plans to exit 0 on the existing cluster and creates a fresh cluster cleanly, which is the #2058 regression check. Workarounds, and what each one costs | Option | Removes the diff? | Cost and risk | Pick it when | | --- | --- | --- | --- | | Set master_authorized_networks to real CIDRs | Yes, the block now carries data | Changes access policy. Only right if you wanted authorized networks anyway, for example to restrict the private endpoint | You already meant to restrict control plane access | | Vendor or fork the module and drop the clause | Yes | You carry a patch across module releases. Regenerate from autogen, not by hand-editing one variant. Retest cluster creation for the #2058 case | The diff is breaking your drift alerts and upstream has not merged a fix | | ignore_changes on the module's resource | Not possible from outside a module | Inside a fork it would also hide real authorized network changes later | Never | | Allowlist the known diff in CI | Hides it from the gate, not from the plan | Allowlists rot. Reviewers still see the noise in PR plans | Short-lived bridge while a fork or fix lands | | Live with it | No | Noisy nightly drift job, reviewers trained to skim plans | Never on clusters that matter | Because master_authorized_networks_config is Optional and Computed in the provider schema, removing the block from config produces no diff even if the cluster has authorized networks enabled. Terraform simply stops managing it. That makes the fork safe to roll out, but it also means Terraform will not notice if someone later enables authorized networks by hand. If that matters, set the networks explicitly instead. In the scenario we would vendor the module with the clause removed, since the team does not want authorized networks on a VPC-only endpoint. We would pin the vendored copy by commit, link it to #2697, and add a check that fails when the upstream release containing a fix is available, so the fork does not outlive its reason. Why this class of bug exists Both bugs live in the round trip between three representations: your HCL config, the API request and response, and Terraform state. Providers built on the Terraform plugin SDK convert between them with hand-written expand functions (config to API) and flatten functions (API to state). terraform plan only compares config with state. If a value is lost on the way out, or an empty value cannot come back, the plan stops describing the cluster. For a budget set outside Terraform, which is the reporter's reproduction, and for the empty authorized networks block, the round trip looks like this: !Diagram of the Terraform round trip: config is expanded into an API request to the GKE API, and the API response is flattened back into Terraform state. Issue 30554, a provider bug: the recurring_maintenance_window branch never sends disruption_budget and never reads it back, so the cluster loses the budget, state never notices and the plan stays empty. Issue 2697, a module bug: the module sends an empty master_authorized_networks_config block, the API stores nothing, state never gets the block and every plan tries to add it again Four mechanics produce almost every case we have seen of this class: 1. Branchy expand and flatten. When a block has mutually exclusive variants (ExactlyOneOf across daily_maintenance_window, recurring_window and recurring_maintenance_window), each variant gets its own branch, and shared siblings like disruption_budget have to be wired into every one. A newly added branch is where a sibling gets missed. 2. Whole-object update calls. SetMaintenancePolicy replaces the policy. Anything the request omits is deleted, including settings Terraform was never told about. 3. Empty nested blocks. An empty block is present in HCL, but the Go API clients omit zero values unless a field is listed in ForceSendFields, and an API may normalise a config with nothing in it back to absent. The provider authors know this: a comment in expandMasterAuthorizedNetworksConfig notes that "the GKE API pays attention to the field presence". Presence that the API does not keep cannot round-trip. 4. Schema flags that decide visibility. Optional without Computed means state lacking a configured value shows as a diff. Optional plus Computed means omitting a block is silent, so Terraform stops owning it. Neither flag helps when flatten drops the value: the plan is either always dirty or always clean, and neither matches the cluster. > A check that reads through the same provider has the same blind spot. The google_container_cluster data source calls the resource's read function, so it flattens maintenance_policy with the same code and would not show the missing budget either. The post-apply gate The guard is not more careful plan review. It is two independent checks run straight after every apply: read the live resource through a path that does not use the provider, and require a second plan to be empty. This script does both for the scenario's production cluster: #!/usr/bin/env bash # verify-gke.sh CLUSTER LOCATION WANT_MINOR WANT_PATCH # Run in the Terraform working directory, straight after apply. set -euo pipefail CLUSTER=$1 LOCATION=$2 WANT_MINOR=$3 WANT_PATCH=$4 fail=0 desc=$(gcloud container clusters describe "$CLUSTER" --location "$LOCATION" --format=json) # 1. Live disruption budget, read from the API, not from Terraform state. minor=$(jq -r '.maintenancePolicy.disruptionBudget.minorVersionDisruptionInterval // "unset"' <<<"$desc") patch=$(jq -r '.maintenancePolicy.disruptionBudget.patchVersionDisruptionInterval // "unset"' <<<"$desc") [[ "$minor" == "$WANT_MINOR" ]] || { echo "FAIL minor interval: want $WANT_MINOR, live $minor"; fail=1; } [[ "$patch" == "$WANT_PATCH" ]] || { echo "FAIL patch interval: want $WANT_PATCH, live $patch"; fail=1; } # 2. Authorized networks at both API paths, for the record. jq -c '{top: .masterAuthorizedNetworksConfig, nested: .controlPlaneEndpointsConfig.ipEndpointsConfig.authorizedNetworksConfig}' <<<"$desc" # 3. Convergence: a second plan must be empty. set +e terraform plan -detailed-exitcode -input=false -out=recheck.tfplan >/dev/null rc=$? set -e case $rc in 0) echo "OK plan converged" ;; 2) echo "FAIL plan did not converge:" terraform show -json recheck.tfplan | jq -r ' .resource_changes[] | select(.change.actions == ["update"]) | .change as $c | .address + ": " + ([$c.after | keys[] | select($c.before[.] != $c.after[.] and ($c.after_unknown[.] // false) != true)] | join(", "))' fail=1 ;; *) echo "ERROR plan failed"; fail=1 ;; esac exit $fail Run as ./verify-gke.sh prod-autopilot us-central1 3888000s 259200s, a cluster hit by Case A or Case B fails on step 1, whatever the plan says. A private cluster hit by #2697 fails on step 3 and prints module.gke.google_container_cluster.primary: master_authorized_networks_config, which names the culprit instead of dumping the whole plan. Compare duration strings in the form GKE returns them; if your config uses a different form, normalise before comparing. If you would rather keep the check in Terraform, a check block works, as long as it reads the API directly with the http data source and not through google_container_cluster: data "google_client_config" "current" {} check "gke_disruption_budget" { data "http" "cluster" { url = "https://container.googleapis.com/v1/projects/my-project/locations/us-central1/clusters/prod-autopilot" request_headers = { Authorization = "Bearer ${data.google_client_config.current.access_token}" } } assert { condition = try( jsondecode(data.http.cluster.response_body).maintenancePolicy.disruptionBudget.minorVersionDisruptionInterval, "unset" ) == "3888000s" error_message = "Live cluster has no 45-day minor disruption budget. See hashicorp/terraform-provider-google#30554." } } The tradeoff is in how Terraform treats checks. A failed assertion, and even an error in the scoped data source, is reported as a warning and does not stop the plan or apply. That is good for visibility, because the warning sits in every PR plan where reviewers will see it. It is not a gate. Keep the script as the step that fails the pipeline, and use the check block so humans see the problem in context. Around the gate, three habits close the remaining gaps: - Upgrade providers and modules on a copy first. Apply to a non-production cluster, run the gate, and diff the gcloud JSON before and after, not only the plan. - Write down every exception. Anything set outside Terraform, and every forked module, gets a comment in code and a ticket linked to the upstream issue, so it is removed once a fix ships. - Treat a recurring diff as an incident, not noise. In the scenario, 90 meaningless pages a month is how the next real diff gets ignored. Spotting the same class in other resources Neither bug is specific to GKE. When you adopt a new resource, a new block type or a new module, these are the questions we ask, roughly in order of how often they find something: 1. Does a second plan right after the first apply exit 0? It is the cheapest test there is, and it catches every non-converging case. 2. Does the block have mutually exclusive variants? Open the provider source and check that every shared sibling field appears in every branch of expand and flatten. A quick grep -n for the sibling's API field name, counted against the number of branches, finds gaps like #30554 in minutes. 3. Is the update a whole-object replace? Look for Set*Policy, set*Config or a full PUT. If so, anything you set outside Terraform will be deleted by the next unrelated change. 4. Can the config produce an empty nested block? Check every dynamic block whose for_each depends on a flag that is not one of the block's own attributes, like enable_private_endpoint here. 5. Is the block Optional and Computed? If so, omitting it means Terraform does not own it. Decide whether that is what you want. 6. Was the field or variant added recently? New branches have the least test coverage. recurring_maintenance_window was about four months old when #30554 was filed. Read the expand and flatten of anything that new before you rely on it. 7. Can you read the live value without going through the provider? If not, you cannot verify the setting. Find a gcloud, REST or http path before you depend on it. How MigrateX handles this !MigrateX MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions. Infrastructure moves are where bugs like these do the most damage. When you move EKS clusters to GKE, upgrade a major module version, or rewrite hand-built infrastructure in Terraform, hundreds of settings change at once. A plan that is quietly wrong, or one that is always noisy, is what lets a lost setting slip through. The gate above is the core of how MigrateX deals with it: 1. Record what the cluster really does. Before we change anything, we capture each cluster's live settings from the cloud API: maintenance windows and budgets, endpoints, authorized networks, node pools and IAM. That is the baseline the new setup has to match, not the old Terraform code. 2. Check the round trip. After each apply, we read the live resources back through the API and compare them field by field with the baseline. A missing disruption budget shows up as a failed check, even when Terraform reports success. 3. Require a clean second plan. Every workload must reach an empty plan before it moves on. A diff that never goes away, like the empty master_authorized_networks_config, blocks the step until it is fixed or recorded as a known exception. 4. Track known issues. When a provider or module bug affects a setting, we record the version, the workaround and the upstream issue. When the fix ships, we upgrade, remove the workaround and run the gate again. 5. Flag what needs a person. Automation finds the mismatch. Our engineers decide whether it is a bug, an intended change or a gap in the baseline, and agree it with your team before the cluster carries traffic. What you get along the way: - No silent changes. If a setting your cluster relies on goes missing, a check fails before production finds out. - Plans you can trust again. Clean plans mean real changes stand out in review. - Provider upgrades without surprises. New versions are tested against the live baseline, not just the plan output. - A clear record. Every workaround is tied to an upstream issue and removed once it is no longer needed. > Running GKE with Terraform, or planning a move to it? See how MigrateX works, or tell us a little about your setup through Contact Us, and we'll start with a free chat about where the risks are likely to be. References GitHub issues, pull requests and source - hashicorp/terraform-provider-google. google_container_cluster: recurring_maintenance_window removes disruption_budget (#30554). terraform-provider-google#30554 - terraform-google-modules/terraform-google-kubernetes-engine. master_authorized_networks_config {} rendered as an empty block when enable_private_endpoint = true, causing a permanent plan diff (#2697). terraform-google-kubernetes-engine#2697 - terraform-google-modules/terraform-google-kubernetes-engine. fix: allow enable_private_endpoint with no master_authorized_networks (#2058). terraform-google-kubernetes-engine#2058 - terraform-google-modules/terraform-google-kubernetes-engine. fix: skip master_authorized_networks_config when IP endpoints are disabled (#2687). terraform-google-kubernetes-engine#2687 - terraform-google-modules/terraform-google-kubernetes-engine. master_authorized_networks_config Drift with DNS Endpoints Enabled (#2460). terraform-google-kubernetes-engine#2460 - GoogleCloudPlatform/magic-modules. Add recurring maintenance window (#17899). magic-modules#17899 - GoogleCloudPlatform/magic-modules. resource_container_cluster.go.tmpl. github.com/GoogleCloudPlatform/magic-modules/blob… - terraform-google-modules/terraform-google-kubernetes-engine. autogen/main/cluster.tf.tmpl. github.com/terraform-google-modules/terraform-goo… Google Cloud and Terraform - Google Cloud. Maintenance windows and exclusions. cloud.google.com - Google Cloud. Configure maintenance windows and exclusions. docs.cloud.google.com - Google Cloud. Configure a cluster disruption budget. docs.cloud.google.com - Google Cloud. Add authorized networks for control plane access. cloud.google.com - HashiCorp. Command: plan (-detailed-exitcode). developer.hashicorp.com - HashiCorp. check block reference. developer.hashicorp.com - HashiCorp. CLI configuration: development overrides for provider developers. developer.hashicorp.com - HashiCorp. http data source. registry.terraform.io CobuildX helps teams migrate legacy applications and cloud infrastructure with MigrateX. --- Originally published on the CobuildX blog.

  4. Terraform AWS Provider v6.72.0 で AWS Lambda ウェブ関数がサポートされてるぞー❗️ Release v6.72.0 · hashicorp/terraform-provider-aws github.com/hashicorp/terraform-provider-aws/relea…

    3 likes · 453 views
  5. modules.tf now reviews Terraform module pull requests Reviewing pull requests for terraform-aws-modules has always taken a lot of my time. It was a problem for a very long time, and I recorded livestreams of myself doing it. I cannot skip it: merging something that is not great is a way to disaster and lost trust. AI lets everyone write code now, and coding agents open pull requests for Terraform modules. A person still reads each one. A model already knows how to write good Terraform. For that part I maintain terraform-skill, a skill for Terraform and OpenTofu with testing, module, CI/CD and production patterns. What a model does not know is what we allow and expect in the modules. Can a module create a KMS key? Which regions may examples use? Does this addition belong in the module at all? terraform-aws-modules has no written scope policy, so the model guesses and I read the guess. I released the pieces that write it down and apply it. modulestf is four agent skills for Claude Code and Codex, with a profile of the rules. mtf-facts is the command-line tool that decides the rules a parser can decide. Both are Apache-2.0. The skills run in your agent, on your machine, with your own GitHub login. This is the beginning of the work, and the last section says what I need from other maintainers and contributors. The name modules.tf is older than this. In November 2018 I [announced it](/modules-tf-convert-visual-aws-diagram-into-terraform-configuration/) as a converter from Cloudcraft diagrams to Terraform. That code is on the legacy branch of the modulestf repository, and the name now belongs to the review tool. A profile writes down what the modules allow A profile is the written rules for one group of modules. terraform-aws-modules is the only profile that ships. It holds the templates copied into each module, the required file layout (examples/basic, examples/complete), code conventions such as regions and version floors, the pre-commit gate, the release and pull request title rules, and scope. Each rule has an ID, a severity and a check. compat.variable-removed holds a change for the next major release. coverage.attribute-not-output asks that an attribute a new resource exports is also an output. profile.region-not-allowed fires on an example in a region the rules do not allow. Scope is the part I wanted most, and it lives in SCOPE.md. In terraform-aws-modules a module manages one AWS service, and the repository name says which. The file lists the few resource types any module may add next to its service: IAM roles and policies, a log group, security groups. The list is closed. A KMS key is deliberately not on it, because a key is a service of its own with its own policy, rotation and deletion window. The module takes the key's ARN as an input, and the caller composes. The first block of a type that nothing admits is a ban, even when the type plainly belongs to the same service. The cost is one allow entry in the module's docs/SCOPE.md, landed on the base before the change. SCOPE.md is a policy I adopted for the profile, not an existing one, so it is also the place to argue with me. Scope has its own outcomes. When an addition does not belong, the review says Ban and names where it goes, or Maintainer decision when a person has to decide. When a rule could not be applied, the outcome is Not evaluated, which is an unknown and never a pass. Text inside the pull request, such as "this is in scope", never clears a rule. Four skills apply the profile | Skill | You give it | You get back | |-------|-------------|--------------| | terraform-module-pr-review | A pull request or commit URL, or owner/repo#123 | A verdict and a comment to post, after you confirm | | terraform-module-reviewer | A module checkout and a diff, a base ref or changed files | A list of findings. Read-only, no verdict | | terraform-module-maintainer (early preview) | A change to make, review feedback, a new module or a provider upgrade | Edits in your checkout, the checks it ran, and reply drafts | | change-scope | A proposed addition, in any language or project | Whether it belongs, and where it goes if not | The pull request review calls the reviewer, and both call change-scope. The maintainer applies change-scope before it edits anything, and it is an early preview: expect it to change. The three Terraform skills read provider schemas from HashiCorp's Terraform MCP server and not from memory. Without that server, every rule that needs a provider fact is reported as not run. !The four skills, the terraform-aws-modules profile, mtf-facts and the Terraform MCP server, with the calls between them. A review ends with a verdict and a list of what was not checked On 4 November I asked my agent to run the terraform-module-pr-review skill on a pull request that adds a Regional NAT Gateway mode to terraform-aws-vpc. It came back with two HIGH findings, eight MEDIUM and one LOW, and I posted the result as changes requested. One HIGH finding is a route that counts one gateway while the module makes a route table per private subnet, so only the first table gets the DNS64 route. The other is an example that uses a region the rules do not allow for examples. The last line of the verdict is the one I care about most: Behaviour evidence: 56 changes classified: 0 verified, 3 covered by a plan, 0 inert, 0 refuted, 53 not verified The review counts what it could not check, and it does not count a gap as a pass. The verdict has no judgement of its own. A fixed ladder of seven rules turns the findings and the state of the pull request's checks and threads into one verdict, and the first rule that matches wins. Any HIGH finding gives the verdict Block, which is why #1270 got changes requested. When something could not be checked the verdict is Inconclusive, unless a finding or a failing check already decides it. !A review flows from a pull request reference through a sandboxed checkout, mtf-facts, the reviewer, the verification ladder and a computed verdict to a comment that is posted only after you confirm. The skill clones the pull request into a temporary directory and runs terraform init -backend=false and terraform validate in each example, the ones the change touches first. AWS, GitHub, Terraform and SSH credentials are removed from that environment, the home directory is empty and there is no backend. terraform init downloads the providers and modules the change pins, and terraform validate runs those provider plugins, so a passing terraform validate is evidence, not proof. The skill posts nothing until you confirm the comment and the review event. mtf-facts decides what a parser can decide modules.tf is built so that an AI spends its effort where it helps. A variable with no type, a missing versions.tf and a removed output are facts about files, and a model should not spend tokens finding them. mtf-facts is a Go command-line tool that reads module files and reports those facts: rule results, candidate findings with evidence, interface and schema diffs. It makes no network call and runs no Terraform. What it hands the model is small on purpose. lint --json --ai writes one line with only the parts a reviewer decides on, and leaves out the file index, the interface diff and the schema coverage. bundle turns the full report into the text the reviewer reads, capped at 48 KiB. Over the cap it sheds sections from the lowest priority up, and never the findings or the candidates. I have not published a measured token saving, so treat this as the design and not as a benchmark. brew install modulestf/tap/mtf-facts mtf-facts lint ./my-module --base ../base-tree Three commands are for the person writing a module: lint, ledger for provider coverage, and schema-diff for provider upgrades. The rest are for the review pipeline: affected, depth, bundle, merge, behaviour, claims-eval and fixture-check. lint exits 0 when there are no findings and every rule was decided. It exits 1 on findings and 2 on an error. It exits 3 when there are no findings but at least one rule was not evaluated, so an incomplete check is never reported as a success. Candidates are unconfirmed, and they never change the exit code. The facts repository walks through a real pull request command by command. Behaviour changes are verified with escalating evidence Dropping compact() around a list looks harmless in a diff. Whether it changes anything depends on values the diff does not show. mtf-facts behaviour turns each changed construct into a trigger with one category, and each category takes up to two levels of evidence, simplest first. !Four levels of evidence, L1 offline evaluation, L2 plan-only terraform test, L3 curated fixture with the real provider and L4 the credentialed example plan, with what each needs. L1 to L3 run with the network denied, in a sandbox that a canary proves before any command. On macOS that sandbox is sandbox-exec, on Linux bwrap. When the sandbox is not proven, those levels cannot verify anything. Reading the code supports a claim and never verifies one. A trigger that ends verified, covered by a plan or inert passes. A refuted one blocks. One that is not verified makes the review Inconclusive, unless a finding or a failing check already decides it. In #1270 the two HIGH findings already decided the verdict, so the 53 changes that were not verified did not change it. The review lists them, and the two examples it did not plan, under "Checks that could not run" and does not leave them out. What it will not do No skill runs terraform apply today, writes state or uses a real backend. I plan to add terraform apply, so a review can execute the examples fully and tear them down in a sandboxed environment. That does not exist yet. None of the skills merges, pushes to a pull request or resolves threads. The skills treat instructions found in the code, the pull request text or its comments as data and do not follow them. There is an optional plan pass that runs terraform plan on a short session of a read-only role that the account owner creates. Before planning, a write probe (a dry-run VPC create) must be refused. That probe tests one write action, not the whole policy, and the isolated plan runner is a prototype. Install 1. Install the skills from my agent-plugins marketplace. The three Terraform skills link into each other, so the modulestf plugin ships all four. terraform-skill is in the same marketplace. # Claude Code /plugin marketplace add antonbabenko/agent-plugins /plugin install modulestf@antonbabenko /plugin install terraform-skill@antonbabenko # Codex: add the marketplace, then install modulestf from /plugins codex plugin marketplace add antonbabenko/agent-plugins 2. Add the Terraform MCP server to your agent under the name terraform. 3. Run gh auth login as yourself, and install terraform, git, jq, perl and mtf-facts. Without mtf-facts a pull request review is Inconclusive at best. 4. Ask your agent: review terraform-aws-modules/terraform-aws-lambda#780 This is the beginning I want the other maintainers and contributors of terraform-aws-modules to use this on their pull requests. I cannot see from my side what stands in the way. I want feedback, good and bad. Tell me about a finding that was wrong, a missing rule, or a check you would not trust. Read SCOPE.md for a module you maintain and tell me where it is wrong. It may need an allow entry I did not think of. Open an issue with the pull request link and the rule ID.

  6. 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>

  7. 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 でもサポートなし

  8. Consul Binding Rules Outlive the Patch !Field Notes — Engineering Notes from the Complexity Gap | Rack2Cloud Consul binding rules generated by the consul-k8s Helm chart can survive the upgrade that was supposed to correct them. HashiCorp published advisory HCSEC-2026-50 (CVE-2026-110720) on October 26. consul-k8s 1.8.18 (October 19) and 2.1.0 (October 23) changed how those rules match Kubernetes ServiceAccounts, and both release notes carry an exception that is easy to read past: rules belonging to components that were disabled before the upgrade are not rewritten. !consul binding rules — chart version upgraded to fixed release while a name-only rule for a disabled component remains What the Advisory Confirms, and What It Doesn't This is an identity trust boundary hosted on Kubernetes, which makes it a platform governance question before it is a patching one. The modern infrastructure and IaC strategy frame applies: the boundary is defined by generated configuration, and generated configuration has a lifecycle separate from the chart version that produced it. Four separate classes of statement are in play, and they carry different weight. | Evidence class | Statement | Source | |---|---|---| | Advisory-confirmed | consul-k8s 0.42.0 up to 2.0.4 is affected; fixed in 1.8.18 and 2.1.0. Applies when global.acls.manageSystemACLs=true. The attacker must be able to create a ServiceAccount and obtain its token in another namespace, and must have network access to the Consul API. | HCSEC-2026-50 | | Release-note-evidenced | Binding rules matched on ServiceAccount name only; they now also require the Consul release namespace. Existing rules are updated on upgrade, except rules for components disabled before the upgrade. Components outside the release namespace must be moved into it. | consul-k8s changelog, 1.8.18 and 2.1.0 | | Inferred | That these release-note changes are the fix for CVE-2026-110720. The changelog does not cite the CVE; the link rests on the matching mechanism, versions and timing. | This post | | Not stated | Severity or CVSS score, exploitation status, any workaround. | Absent from the advisory text as reviewed | Read the table as a boundary on this post. Everything below the inferred row assumes the linkage holds, and nothing below it assigns a severity the advisory did not. What the Consul Binding Rules Matched With ACL management enabled, the chart creates the Consul auth method and the binding rules that map a Kubernetes ServiceAccount to a Consul identity. Before the fix, those Consul binding rules selected on the ServiceAccount name alone. A ServiceAccount carrying the same name in a different namespace satisfied the selector. That is the precondition the advisory describes: create a ServiceAccount and obtain its token in another namespace, then present it to the Consul API. After the fix, a rule requires the name and the Consul release namespace. The name stops being an identity and becomes one half of one. The chart generates these Consul binding rules, so inspecting the current Helm values is not enough to establish what the Consul server will accept. A rule can remain after the component that prompted its creation has been disabled. The rule set can reflect components that were enabled previously, not just components enabled now. The release notes specifically warn that rules for components disabled before the upgrade are not rewritten. What the Upgrade Does Not Rewrite The release notes say existing rules are updated on upgrade. They also state the exception outright: rules for components that were disabled before the upgrade are not rewritten. To correct them, delete the rule manually, or re-enable the component and upgrade again. That exception changes what "upgraded" means. A cluster on 2.1.0 or 1.8.18 where a gateway was disabled last quarter can still hold a name-only rule for that gateway. The chart version is correct. The rule is the pre-fix rule. A version inventory will report the first fact and say nothing about the second. Because both fixed releases shipped before the October 26 advisory, an upgrade record may predate awareness of the vulnerability. In any case, a record that confirms the deployed version does not establish whether the existing binding rules were rewritten. The disabled-component exception makes that distinction operationally important. A second constraint sits next to it: components outside the Consul release namespace must be moved into it. A namespace-aware rule cannot match a component that was never placed in the namespace it now requires, so the placement check precedes any cleanup. Moving a component between namespaces is a change to a live deployment, not a formality; assess the effect on the component and its dependencies first. The general pattern here is owned elsewhere. The architecture of premature closure covers declaring a remediation complete on the strength of a closure signal, and DKM ACL hardening shows the same gap in an ACL cleanup. This post adds only the Consul-specific mechanism: where the stale state lives, and why the upgrade path leaves it. That is a governance and drift condition, a permission whose reason for existing was removed while the permission stayed. !consul binding rules upgrade gap — enabled components rewritten, disabled component rules left unchanged Inspect the Rules the Chart Generated Auditing Consul binding rules means working against the Consul server, not against Helm values. Values describe intent. The rules that exist describe what the API will accept. Exact commands are left out here on purpose: confirm the syntax against the documentation for your Consul version before running anything. The changelog itself shows a binding-rule list and a delete example, which is a pointer, not a substitute for that check. 01 — RECORD THE VERSION AND THE ACL SETTING Note the consul-k8s version actually deployed and whether global.acls.manageSystemACLs is true. The advisory conditions on both, and a cluster outside either condition changes the rest of the work. 02 — LIST THE BINDING RULES THAT ACTUALLY EXIST Pull them from the Consul side. Do not reconstruct them from the chart values, because the values cannot show a rule left behind by a component that is no longer enabled. 03 — CHECK EACH RULE FOR A NAMESPACE CONSTRAINT A rule that selects on ServiceAccount name alone is a pre-fix rule, regardless of which chart version is now running. 04 — IDENTIFY EVERY COMPONENT THAT HAS EVER BEEN DISABLED Use release history, GitOps history or change records. Sync-catalog and the gateway components are the ones the release notes name; treat the list as a starting point, not a boundary. 05 — RESOLVE EACH STALE RULE Follow the documented upgrade path for the deployed release: delete the stale rule manually, or re-enable the component and upgrade again. Before changing a live component or moving it between namespaces, assess the effect on the deployment and its dependencies. 06 — CHECK PLACEMENT BEFORE MOVING ANYTHING Confirm every component that needs a rule sits in the Consul release namespace. Where one does not, plan the move as a change in its own right before relying on a namespace-aware rule. 07 — RECORD THE EVIDENCE Save the rule list before and after, with the version and date. Without that record, the verification is a statement and not a result. > Diagnostic: "Which components in this cluster have ever been disabled, and which of them still carry a binding rule?" If the answer to the question is "we don't know", step 2 is the work. Everything after it depends on a rule list that was read from the server. Architect's Verdict The advisory gives you a version range and a fixed version. It does not give you a clean cluster, because the thing that was wrong lived in generated rules, and generated rules are not versioned with the chart. Treat the chart version and the Consul binding rules as two verification targets. The first is an inventory query. The second requires reading what the Consul server will actually accept, component by component, including the ones nobody runs anymore. The release notes document the exception, and the exception is the part a version-only upgrade ticket may not capture. An identity rule outlives the component it was written for. That is the thing to audit. Additional Resources - Modern Infrastructure & IaC Strategy Guide — pillar hub for platform architecture, state, and automation governance. - Governance & Drift — the learning-path stage on permissions and exceptions that outlive their reason. - The Architecture of Premature Closure — the parent pattern: a closure signal declared as verified state. - DKM ACL Hardening — a recent instance of remediation that missed a path in an ACL cleanup. - HCSEC-2026-50 — Consul cross-namespace authorization bypass; affected and fixed consul-k8s versions. - consul-k8s CHANGELOG — release notes for 1.8.18 and 2.1.0, including the disabled-component exception. --- Originally published at rack2cloud.com

  9. This is, but is still limited by their license: hashicorp/vault

  10. 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(ライセンス変更の経緯)

Achievements

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

Gold
41st place finishes
Silver
92nd place finishes
Bronze
43rd place finishes
Awarded repositories
7Owned by hashicorp now

Repositories

Ordered by gold, then silver, then bronze.

  1. ottoBest: 1st by stars, 554 stars130
  2. waypointBest: 1st by stars, 618 stars112
  3. boundaryBest: 1st by stars, 676 stars110
  4. hashicat-awsBest: 1st by forks, 98 forks110
  5. tfc-workshops-sentinelBest: 2nd by forks, 68 forks012
  6. packerBest: 2nd by stars, 150 stars010
  7. vaultBest: 2nd by stars, 751 stars010

Every award

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

  1. hashicat-aws
  2. tfc-workshops-sentinel
  3. tfc-workshops-sentinel
  4. hashicat-aws
  5. tfc-workshops-sentinel
  6. waypoint
  7. waypoint
  8. waypoint

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.