DevOps

How to Upgrade the Terraform Kubernetes Provider to 3.4 Without Breaking Your Plans

Versions 3.3 and 3.4 of the Terraform Kubernetes provider reject null labels and a double-named Role. Here is a safe upgrade path, step by step.

Marcus Oyelaran

Security & DevOps Specialist

Published 7 min read
A detailed view of a blue lit computer server rack in a data center showcasing technology and hardware.
On this page 11 sections

To upgrade the Terraform Kubernetes provider to 3.4, first clean up null values in namespace and Role labels and annotations, then fix any Role that sets both name and generate_name. Pin the new version in a branch, run terraform plan, and expect a few harmless one-time in-place updates. Apply in staging before production.

This guide is for platform and DevOps engineers who manage clusters with Terraform. It covers what changed in versions 3.3.0 and 3.4.0, how to find the configuration that will fail, and how to roll the change out safely. Backtrace focuses on the unglamorous work of keeping a deploy pipeline boring, and a provider bump is a good example.

What Changed in 3.3.0 and 3.4.0

The two releases are small, but both include breaking changes. According to the provider changelog, version 3.3.0 shipped on October 1, 2026, and 3.4.0 followed on October 16.

Here is the short version of each release.

What Changed in 3.3.0 and 3.4.0
VersionResourceBreaking change
3.3.0kubernetes_namespace_v1Null entries in metadata labels and annotations are rejected during planning.
3.4.0kubernetes_role_v1Null entries in labels and annotations are rejected.
3.4.0kubernetes_role_v1Setting both metadata name and generate_name is rejected, even when generate_name is an empty string.

Older versions silently dropped null entries. That is the trap. A map like a label set built from a variable could contain a null value for months without any error, and now it stops the plan.

Both resources also moved from the older SDKv2 framework to the Plugin Framework. The resource names and configuration syntax stay the same. The move is why you may see small plan differences right after the upgrade.

Close-up of colorful programming code on a blurred computer monitor.

Step 1: Find the Configuration That Will Fail

Search your modules before you touch the version constraint. You are looking for three patterns: namespace or Role resources whose labels or annotations come from variables, locals that can hold null, and Roles that set both name and generate_name.

Run through this checklist:

  1. Grep for kubernetes_namespace_v1 and kubernetes_role_v1 across every repo and module.
  2. Check each labels and annotations block for values that could resolve to null.
  3. Check each Role for generate_name, including empty strings left over from a template.
  4. List the modules that other teams consume, since a shared module can fail in someone else's pipeline.

Shared modules deserve extra care. A module author who bumps the provider constraint can break every consumer on its next plan. Treat that like a breaking API change and announce it.

Step 2: Fix Null Labels and Annotations

The fix is simple. Omit the null entry, or give it a string value. The changelog confirms that whole maps can still be omitted or set to null. Only individual entries inside a map must be non-null strings.

A common pattern is a merge of default labels with an optional override. If the override variable defaults to null for a key, filter those keys out before the merge. A filtered map is explicit and easy to read in a diff.

Do this change on the old provider version first. The behavior is identical there, because the old provider dropped the nulls anyway. Landing the cleanup separately means the actual upgrade pull request contains only the version bump.

Overhead shot of a laptop with notebooks and a pencil on a patterned cloth.

Step 3: Handle Roles With Both name and generate_name

This is the change most likely to cause real trouble. The Kubernetes API ignores generate_name when name is set, so the old provider let both through. Version 3.4.0 rejects the pair during planning, even when generate_name is an empty string.

The changelog gives two ways out. If generate_name is empty, remove it and keep name. If you want generated names, remove name and keep generate_name. Be careful with the second option. Making that switch replaces the Role.

A replaced Role is not always harmless. Anything bound to the old name, such as a RoleBinding, needs to survive the swap. Review dependent bindings and plan the change for a quiet window if the Role guards a busy workload.

Which Terraform Version Do You Need for Moved Blocks?

You need Terraform 1.8 or later to use moved blocks when migrating from the deprecated kubernetes_role resource to kubernetes_role_v1. The changelog states that requirement for Roles. For namespaces, it supports moved blocks from the deprecated resource but names no minimum version, so test on your own pinned version.

If you still have the deprecated resource names in state, now is a good time to migrate. The provider deprecated the older names in favor of their _v1 replacements in its 3.0 release. Moving to the new names removes a future surprise.

Step 4: Pin, Plan and Read the Diff

With the cleanup merged, bump the constraint in a branch. Use a pessimistic constraint such as ~> 3.4 so a later minor release does not arrive unannounced. Run terraform init -upgrade, then terraform plan against a real environment.

Read the plan output slowly. Some changes are expected and safe, according to the same changelog:

  • Explicit empty metadata maps may show one in-place update.
  • Roles imported with an older provider may show an in-place update that sets empty labels and annotations to null.
  • Running plan with -refresh=false right after the upgrade may show an in-place update adding an empty resource_names list.

None of those replace the Role. A plan that shows a destroy and create for a namespace or Role is not on the expected list. Stop and investigate before you apply.

Detailed view of network cables plugged into a server rack in a data center.

Step 5: Roll Out in Stages

Apply to a development cluster first, then staging, then production. Each stage should use the same provider version and the same state backend setup. The goal is to see the one-time in-place updates settle, so the production run is a no-op.

Our comparison of blue-green and canary deployments applies to infrastructure code too. A canary workspace that applies the new provider first gives you a cheap early warning. If your pipeline runs on shared runners, the tradeoffs in GitHub Actions versus GitLab CI matter for how you gate the production apply.

Assign one person to own the rollout from start to finish. Provider upgrades tend to stall when three teams each assume another will test the shared modules. A named owner, a short checklist and a date turn a vague chore into a finished task.

Do not combine the provider bump with other changes. If a plan goes wrong, you want one variable to blame. The same discipline helps in other upgrades, as in our DuckDB 2.0 LTS upgrade guide.

Keep the Lock File and the Runners in Sync

A provider upgrade often fails in CI even when it works on a laptop. The usual cause is a lock file that was never committed, or a runner image that caches an older provider. Check that the dependency lock file lists the new version and its checksums for every platform your runners use.

The provider added a linux s390x build in version 3.2.0, so teams on unusual architectures have more options than before. Even so, run terraform providers lock for each platform you target. That one command prevents a checksum mismatch at the worst time.

It also helps to run the plan twice. The first plan after an upgrade can include the one-time in-place updates. The second should be clean. If the second plan still shows changes, something in your configuration is fighting the provider, and that is worth finding before production.

Import IDs and Other Small Gotchas

Imports changed slightly for Roles. An import ID must now include a non-empty namespace and name. Identity-based imports default an omitted namespace to default. If you script imports for disaster recovery, update those scripts.

The releases also fixed a few unrelated issues. Version 3.3.0 fixed a Missing Resource Identity After Update error for StatefulSets with wait_for_rollout enabled. Both releases bumped the gRPC library to resolve published security advisories. Those are good reasons to upgrade soon, not just eventually.

Two men analyzing code on computers in a modern office setting.

The provider's release page lists every version with its notes. Subscribe to it, or to your dependency bot's feed, so the next breaking change reaches you before it reaches production.

A Rollback Plan Worth Having

Know how you will back out before you apply. Keep the old provider constraint in version control, and take a state backup before the first production apply. If a Role was replaced and a binding broke, restoring the old configuration will not undo the replacement. You would need to recreate the binding.

Backtrace editors suggest writing the rollback steps into the pull request description. It takes two minutes, and it saves a tense conversation later.

Conclusion

The 3.3 and 3.4 releases of the Terraform Kubernetes provider are small, but they punish sloppy metadata. Clean up null labels, resolve double-named Roles, pin the new version, and read the plan before you apply. Roll through development and staging so production is boring. Start with the search in step one today, since it costs nothing and tells you how big the job is.

Why does Terraform reject null labels on my namespace now?
Starting with provider version 3.3.0, the kubernetes_namespace_v1 resource requires every entry in metadata labels and annotations to be a non-null string. Older versions silently dropped null entries. Omit the entry or give it a string value. Omitting the whole map or setting it to null is still allowed.
Will upgrading the Kubernetes provider replace my Roles?
Not by itself. The upgrade can show a one-time in-place update for empty metadata maps, which does not replace anything. A Role is replaced only if you switch between name and generate_name to fix the double-naming error in version 3.4.0. Review any RoleBindings first and apply during a quiet window.
Can I use moved blocks to migrate from kubernetes_role?
Yes, with Terraform 1.8 or later. The provider changelog says moved blocks from the deprecated kubernetes_role resource to kubernetes_role_v1 need that version. For namespaces, moved blocks are supported but no minimum is stated, so test on the Terraform version you actually run in your pipeline.
Should I pin the Terraform Kubernetes provider version?
Yes. A pessimistic constraint such as ~> 3.4 lets patch releases through while blocking a surprise minor release. Commit the dependency lock file too, so every runner uses the same build. Upgrade on purpose, in a branch, with a plan you have read, instead of letting init pick a new version.

Written by

Marcus Oyelaran

Security & DevOps Specialist

11+ years experience

Marcus Oyelaran spent eleven years moving between platform and security teams, including four years as a security engineer embedded inside a DevOps org at a healthcare SaaS company handling PHI at scale. He's run more incident postmortems than he can count, and most of what he writes for Backtrace comes directly out of those postmortems: the secret that leaked through a build log, the IAM policy that was too permissive by one wildcard, the deploy that skipped a gate because someone was in a hurry. He holds an OSCP and spends an unreasonable amount of time arguing about supply chain security on internal Slack channels.

Credentials

  • OSCP (Offensive Security Certified Professional)
  • 11 years in DevOps and security engineering
  • Former security engineer, healthcare SaaS platform (HIPAA-regulated infrastructure)