A Kubernetes platform fails when the installation path and the application delivery path collapse into the same ownership model.

Situation

Kubernetes operators are no longer only installing clusters. They are installing ingress controllers, certificate managers, policy engines, observability agents, external DNS, secret synchronization, autoscalers, service meshes, admission controllers, and workload identity glue.

Most of these components are not applications in the product sense. They are platform capabilities. They create APIs, webhooks, CRDs, controllers, and cluster-wide behaviors that application teams consume indirectly.

That changes the automation question.

The old question was: how do we deploy Kubernetes objects?

The better question is: how do we install and evolve the shared platform without making the platform team responsible for every workload running on it?

Terraform is attractive here because it already models infrastructure dependencies, remote state, review workflows, and environment promotion. But Terraform becomes dangerous when it is treated as a universal Kubernetes deployment tool. The same mechanism that safely provisions a cluster can become the thing that accidentally owns every namespace, deployment, service, and chart in the organization.

The Problem

Kubernetes already has a reconciliation model. Terraform also has a reconciliation model. When both are pointed at the same object graph without a boundary, ownership becomes ambiguous.

Terraform expects to read declared resources, compare them to state, and converge remote infrastructure toward the plan. Kubernetes controllers expect to watch objects, mutate status, create dependent resources, and continuously reconcile toward their own desired state. Helm adds another layer by rendering templates and tracking releases.

The failure mode is not that any one tool is wrong. The failure mode is overlapping authority.

A platform team starts with Terraform installing the cluster and a few controllers. Then it adds namespaces. Then base network policies. Then Helm charts for shared services. Then team-specific releases because it is convenient. Eventually application delivery is coupled to infrastructure apply. A failed chart blocks a cluster change. A platform refactor risks deleting app objects. A Terraform state file becomes the hidden registry of application ownership.

The core question is: where should Terraform stop?

The Platform Installation Boundary

Terraform should install the platform contract, not every consumer of the platform.

That means using Terraform for resources whose lifecycle is tied to the platform itself: clusters, node pools, IAM bindings, cloud networking, DNS zones, controller installations, CRDs, shared policy engines, and bootstrap configuration. Application teams should use their own delivery systems for app releases: GitOps controllers, CI pipelines, Helm release workflows, or deployment platforms built on top of Kubernetes.

flowchart TD
  A[Terraform root module — platform intent] --> B[Cloud infrastructure — network and cluster]
  A --> C[Cluster bootstrap — providers and credentials]
  C --> D[Platform controllers — ingress certs policy observability]
  D --> E[Platform APIs — CRDs admission webhooks classes]
  E --> F[Application delivery boundary]
  F --> G[GitOps or CI — app owned releases]
  F --> H[Team namespaces — delegated ownership]
  G --> I[Workloads — deployments services jobs]
  H --> I

The clean boundary is not “Terraform versus Kubernetes.” Terraform will often create Kubernetes resources. The boundary is ownership.

Terraform is a good fit when the resource answers one of these questions:

  • Does this object define shared platform behavior?
  • Does changing it require platform review?
  • Would deletion affect many teams?
  • Does it belong to cluster bootstrap or controller installation?
  • Is it required before app delivery can safely run?

Terraform is a poor fit when the resource answers these questions:

  • Is this app released many times per day?
  • Does one product team own its behavior?
  • Is rollback controlled by the application team?
  • Does the object change with business logic?
  • Would platform approval slow down normal delivery?

A practical pattern is to split automation into three layers.

Layer one is infrastructure Terraform: VPCs, subnets, private endpoints, clusters, node pools, IAM, and DNS.

Layer two is platform Terraform: Kubernetes provider configuration, Helm releases for controllers, CRDs where needed, storage classes, ingress classes, policy engines, observability agents, and bootstrap namespaces.

Layer three is application delivery: GitOps repositories, CI deployment jobs, service catalogs, or release tooling owned by the teams that operate the software.

The platform team may provide templates, policies, base modules, and guardrails for layer three. It should not become the release manager for every application.

In Practice

Context: Kubernetes documents controllers as control loops that watch cluster state and move current state toward desired state. The Operator pattern extends that model by encoding operational knowledge into controllers. The documented pattern is reconciliation by controllers, not one-time imperative installation. Source: Kubernetes documentation on controllers and operators.

Action: Treat Terraform as the installer of controllers and the dependencies those controllers need. For example, Terraform can install cert-manager through Helm, create the DNS permissions it needs, and configure cluster issuers or policy constraints that are platform-owned. After that, cert-manager owns certificate reconciliation inside Kubernetes.

Result: Terraform remains responsible for the platform capability. The Kubernetes controller remains responsible for ongoing runtime reconciliation. Application teams request certificates through Kubernetes objects without needing Terraform access or platform-team pull requests for each certificate.

Learning: The ownership line is stable when Terraform installs the mechanism and Kubernetes-native workflows consume the mechanism.

Context: HashiCorp’s Kubernetes and Helm providers are documented as Terraform providers for managing Kubernetes resources and Helm releases. That makes Terraform capable of managing cluster objects, but capability is not the same as appropriate ownership. Source: HashiCorp provider documentation for the Kubernetes and Helm providers.

Action: Use those providers for platform-scoped releases: ingress controllers, external-dns, metrics agents, policy controllers, CSI drivers, and GitOps bootstrap controllers. Avoid placing product deployments, app config maps, and team release cadence inside the same Terraform state.

Result: Platform changes can be reviewed, planned, and applied independently from application releases. Application failures do not block unrelated infrastructure work, and infrastructure drift detection does not become noisy with expected app churn.

Learning: Terraform state should describe platform intent. It should not become a second application registry.

Context: GitOps tools such as Flux and Argo CD publicly document a model where Kubernetes desired state is stored in Git and reconciled into clusters by controllers. The documented pattern is pull-based application synchronization after bootstrap.

Action: Let Terraform install the GitOps controller and its cloud permissions, then hand application paths to the GitOps system. Terraform can create the initial repository connection or root application object, but the ongoing app graph belongs to the delivery system.

Result: Terraform owns the bootstrap path. GitOps owns app convergence. Teams can ship through normal review and release flows while the platform team keeps the cluster substrate consistent.

Learning: Bootstrap and delivery are different workflows. A healthy platform makes that distinction visible in code ownership, state files, and review paths.

Where It Breaks

TradeoffFailure ModeMitigation
Terraform manages Helm releasesChart upgrades can fail during infrastructure appliesKeep only platform charts in Terraform and test upgrades in lower environments
Terraform creates CRDsCRD lifecycle can race with dependent resourcesSeparate CRD installation from custom resource creation
Controllers mutate objectsTerraform may report drift on fields owned by KubernetesIgnore controller-owned fields or avoid managing those objects with Terraform
Shared state growsOne state file becomes a platform bottleneckSplit state by lifecycle and blast radius
App delivery uses TerraformProduct releases wait for platform reviewDelegate app release workflows to teams
GitOps is bootstrapped by TerraformBootstrap failure can leave the cluster partially configuredKeep bootstrap small and rerunnable
Platform modules hide too muchTeams cannot understand what is installedPublish module contracts, inputs, outputs, and ownership rules

The most common mistake is drawing the boundary by tool instead of lifecycle. “Terraform manages infrastructure, GitOps manages Kubernetes” sounds clean, but it breaks down immediately when Terraform needs to install a Kubernetes controller. “Terraform manages platform-owned lifecycle, app delivery manages team-owned lifecycle” is messier, but it matches reality.

What to Do Next

  • Problem: Your cluster installation path probably contains resources with different owners, review expectations, and change frequency.
  • Solution: Split Terraform into infrastructure and platform layers, then hand application releases to GitOps or CI-owned workflows.
  • Proof: Check whether a normal app deploy can happen without touching Terraform, and whether a platform controller upgrade can happen without reviewing product code.
  • Action: Audit one cluster state file this week. Mark every Kubernetes object as platform-owned, team-owned, or controller-owned. Move anything team-owned out of Terraform before it becomes operational debt.