Terraform is a great tool. It is also the wrong tool for building an Internal Developer Platform. This article is about why a 2014 design wrapped in 2026 CI/CD pipelines doesn’t solve today's requirements on an internal developer platform and doesn’t scale.
Those two statements do not contradict each other. Terraform was designed in a different era for different problems: a small ops team applying infrastructure changes from a workstation, occasionally, in a predictable manner. It solved that challenge so well that the industry adopted it as the default for every infrastructure problem from then on, including the problem, platform engineering is actually trying to solve.
*A Terraform-based platform is not really a platform. It is a CI/CD pipeline that owns a state machine, plus a growing pile of glue code that compensates for the fact that Terraform was never meant to be reconciled, multi-tenanted, or exposed as an API.*
**Key Takeaways**
**Lack of a Native Runtime:** Terraform operates as a CLI tool that executes, talks to providers, and exits, meaning it lacks a runtime for continuous reconciliation. Consequently, teams must "bolt on" CI/CD pipelines and glue code to compensate for these missing features, leading to increased architectural complexity.
**State Coordination Issues:** Because Terraform relies on state files that must be locked during operations, it creates significant bottlenecks. Teams are forced to deal with serialized actions, complex state management across multiple workspaces, and manual error recovery, which are artifacts of missing a proper platform runtime.
**Inadequate for Modern Automation:** Terraform fails to provide the transactional semantics and queryable API needed for modern development workflows, especially when interacting with autonomous agents. Unlike control-plane-based architectures (like Kubernetes/[Crossplane](https://www.crossplane.io/)) which offer continuous reconciliation and clear status feedback, a Terraform-based platform treats infrastructure as an imperative, error-prone workflow rather than an existing, self-healing state.
## **What an Internal Developer Platform actually is**
An IDP is not "infrastructure that has been automated." Platform engineering is the discipline of abstracting complexity by integrating the tools used across the development lifecycle and exposing the necessary options through an interface which can be an API, a CLI or a Web-UI, increasingly an AI-driven interface, to the platform user.
Structurally that means three things. There has to be a unified API that application teams interact with, not a pile of pipelines they pull-request into. There has to be a control plane that continuously enforces the desired state of the resources the platform exposes. And there has to be a clear boundary between the platform's internal implementation and the surface area the developer sees.
These are properties of the architecture, not of any particular toolchain. Heroku had them. Internal platforms at the hyperscalers have them, built on substrates most people will never see. The Kubernetes-native stack is the most accessible way to get there today. Kubernetes itself as a meta-platform to build platforms on, [Crossplane as a framework](https://www.youtube.com/watch?v=zu6V34BFksk) for abstracting integrations and exposing them via an API, ArgoCD as a deployment tool with continuous reconciliation. Each is a control plane with a reconciliation loop, exposing [CRDs](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) or APIs as its contract. [Backstage](https://backstage.io/) or another frontend sits on top and talks to that API layer, working well precisely because it stays out of the control-plane business.
What all of these share, regardless of substrate, is what Terraform-plus-pipelines does not provide: an API that exists between runs, a runtime that reconciles, and a clean boundary between platform internals and the developer-facing contract.
Terraform sits on the wrong side of every one of those boundaries.
## **The core problem: Terraform has no runtime**
Terraform is a CLI. It reads code, reads a state file, talks to providers, and exits. That is the entire execution model.
This sounds like a small detail. It isn't. It is the root cause of every architectural problem that follows. Because Terraform has no runtime, every capability you actually need from a platform has to be bolted on outside of Terraform, and the bolt-on is always a CI/CD pipeline plus some scripting.
Here is what your "platform" inherits the moment you build it on Terraform.
### **1\. State is a coordination problem YOU now own**
Terraform state is the source of truth for the mapping between code and infrastructure. It must be perfectly synchronized with reality, it must be locked during writes, it contains sensitive data, and crucially for a platform: there is exactly one state file per workspace.
That last bit is the killer. Because there is exactly one state file per workspace, every action against that workspace is serialized through it. Two developers provisioning two unrelated resources in the same project? They queue. A pipeline run that crashes mid-apply? The lock persists and someone has to break it by hand. Multi-environment promotion? You are now writing pipeline logic to choose, lock, unlock, and migrate state files.
The standard workaround is to slice everything into smaller workspaces. Now you have hundreds of state files, and the new problem is: who owns which one, how do they reference outputs across workspace boundaries, and what happens when one of them drifts? Platform teams I have worked with end up writing a state-file router in their pipelines. Which is not a feature. It is a homemade orchestrator compensating for a missing runtime.
### **2\. No continuous reconciliation**
When a developer claims a database from the platform, they are not asking for a database to be created. They are asking for a database to exist, with these properties, for as long as the claim exists. If someone clicks around in the AWS console and deletes a security group rule, the platform should put it back. If a node fails, the platform should heal. That is what "platform" means.
Terraform cannot do this. There is no controller, no reconciler, no informer. terraform plan shows you drift only when you run it, and terraform apply corrects it only when you trigger it. To get something resembling reconciliation, teams build cron jobs that run terraform apply on a schedule. Which is a polling loop pretending to be a control loop, and it has none of the properties (level-triggered semantics, exponential backoff, event-driven correction) that make real controllers reliable.
Compare this to Kubernetes-native tooling. A Crossplane composition is reconciled continuously by a controller that owns its lifecycle, has a status subresource, emits events, and recovers from partial failures by design. That is the substrate IDPs need.
### **3\. Cross-provider failures have no useful semantics**
This is the single most damning issue for platform use cases.
When a platform user requests an "application environment" they may need an AWS account, a DNS zone, a Kubernetes namespace, a path in Hashicorps Vault or [OpenBao](https://openbao.org/), an OIDC client, and a Git repository for example. Six providers in one logical transaction.
In Terraform, when the apply touches several providers and the third resource fails, here is what happens:
* The first two resources are created.
* The state file is written with the partial result.
* The run exits non-zero.
There is no transactional semantic, no compensation, no "fail this claim and clean up." The pipeline is left holding a half-provisioned environment and a state file that says "this is the truth.", while the code is untrue. From a platform-API perspective, the user asked for one thing and got a partially undefined state with no signal richer than "exit 1."
A real control plane treats a multi-resource composition as a unit. The composition either converges or it does not, and the user-facing API reflects that through status conditions, events, and readiness. The platform owns the partial-failure semantics instead of leaking them into a pipeline log.
### **4\. The "API" of a Terraform platform is a pull request**
Ask yourself: what is the developer-facing contract of a Terraform-based platform? It is almost always one of:
* "Open a PR against this repository with these variables filled in."
* "Run this pipeline with these parameters and wait for the green check."
* "Use this Backstage template that wraps a pipeline that wraps terraform apply."
None of these are APIs in any meaningful sense. They are workflows. There is no resource model the developer can query, no status to subscribe to, no contract that survives a restart of the pipeline runner. You cannot build a UI on top of a pull request without re-implementing half of what a control plane gives you for free.
The [Crossplane \+ Backstage](https://terasky-oss.github.io/backstage-plugins/plugins/crossplane/overview/) pattern works for the opposite reason: Backstage is only a frontend, and the backend is a real, queryable, reconciling API. A Terraform pipeline cannot offer that, no matter how much Backstage you put in front of it.
### **5\. Glue-code complexity keeps growing**
Every pain point above gets solved with another layer of pipeline logic. State collisions get custom locking and retry logic. Missing reconciliation gets scheduled apply jobs and drift-detection scripts. Multi-provider transactions need orchestration scripts that call Terraform in sequence and try to compensate on failure. The missing API gets papered over with Backstage scaffolder templates, Atlantis-style PR bots, ServiceNow integrations. Secrets in state means external secret stores, state encryption, and careful pipeline isolation on top. Cross-workspace dependencies pull in remote state data sources, output mirroring, custom resolvers. Drift in providers means wrapper scripts around terraform import. And module versioning eventually needs registry tooling, semantic version policies, and breaking-change runbooks.
That growing pile is your real platform. The Terraform invocation in the middle is, at this point, the smallest and least interesting part of it.
Every quarter, a new edge case adds another script to the pipeline, another guardrail to the policy bundle, another runbook to the wiki. And because the complexity lives in pipeline glue rather than in a coherent runtime, nobody can refactor it. Pipelines are hostile to refactoring. They have no type system, no tests in any meaningful sense, and side effects on production infrastructure.
This is how Terraform-based platforms slowly turn into the exact kind of artisanal bespoke ops the platform was supposed to replace, except now there is a Backstage logo on top.
## **And then we let the agents in**
The core problem, that Terraform has no runtime isn’t new. What changed is that we are now putting autonomous coding agents into the same systems, and the gap between "pipeline" and "platform" stopped being a matter of taste.
Agents are multipliers. A coding agent that needs a database, a queue, a DNS record, and a CI runner does not ask nicely. It tries things, fails, retries, branches, opens MRs, triggers pipelines. Whatever your platform exposes, the agent will hit it at machine speed and at scale. If the interface is a pull request against a Terraform repo, you have just authorized an agent to run unstandardized infrastructure changes through a workflow that has no transactional semantics and no usable failure signal. The blast radius of one confused agent in that setup is large, and the diagnosis cost is paid by humans reading pipeline logs.
What agents need is the same thing a good UX needs, just more strictly: a clear context and a contract that does not lie. Both are fundamental for spec-driven development workflows. The spec is only as good as the API describes it, and an API that occasionally hands back a partial state and an exit code is not a spec, it is a coin flip. A control-plane API gives agents both context and contract. The resource model is queryable, so an agent can ask "what exists" before it asks "create this." The status subresource gives a truthful answer to "did it work" without parsing logs. Events provide a stream the agent can subscribe to. And compositions hide the multi-provider mess behind a single semantic unit, so the agent reasons about an "environment" instead of orchestrating six providers and hoping the third one does not fail.
A pipeline cannot offer any of that. A pipeline is a side effect with an exit code. For an agent it is a bad surface: opaque while it runs, ambiguous when it finishes, and impossible to reason about without reading prose.
Humans tolerate a bad platform interface because they can ask a colleague, read a Confluence page, and pattern-match across thirty similar incidents. Agents cannot. They follow whatever interface you give them, including the broken parts. The platforms that hold up once agents are in the loop are the ones that exposed a real API in the first place.
## **"But we use Terragrunt / Atlantis / Spacelift / Env0..."**
They are useful, and the existence of an entire commercial category of "Terraform orchestrators" is itself evidence for the argument.
If your IaC tool needs a whole ecosystem of products around it to be usable for platforms, to handle state sharding, dependency ordering, drift detection, policy enforcement, RBAC, secrets, and PR-based workflows, then the IaC tool is not solving the platform problem. The orchestrator is. And the orchestrator is doing it by gluing imperative steps around a tool that refuses to have a runtime.
You can build a perfectly serviceable platform on Terragrunt plus Atlantis plus Spacelift. Many companies do. The question is whether you should, in 2026, choose that path knowing what it costs to maintain, and knowing the alternatives have caught up.
## **What an architecturally honest IDP stack looks like**
The alternative is not exotic anymore. It is the dominant pattern in cloud-native platform engineering. A control plane, [Kubernetes plus Crossplane](https://www.codecentric.de/en/knowledge-hub/blog/full-gitops-with-crossplane-and-argocd) or an equivalent control-plane-as-a-platform tool, exposes the platform's resources as CRDs. ClusterAPI handles the cluster layer itself, so even Kubernetes clusters become reconciled, declaratively managed objects rather than something a pipeline stamps out and forgets. A reconciliation loop owned by controllers, not by cron. Compositions that bundle multi-provider concerns into a single, transactional, status-aware abstraction. GitOps tools like ArgoCD or Flux deliver platform definitions into that runtime, but they are not the runtime themselves. And a frontend, Backstage, a custom UI, or an MCP server for AI agents, that talks to the control plane's API instead of a pipeline.
[ClusterAPI](https://cluster-api.sigs.k8s.io/) is worth pausing on, because it is the cleanest counter-example to the Terraform model at the layer where Terraform is most entrenched. Spinning up clusters has historically been one of the canonical "run Terraform from a pipeline" tasks, with all the partial-failure, drift, and lifecycle issues described above. ClusterAPI inverts that. A cluster becomes a Cluster resource, its nodes become MachineDeployment resources, and a controller keeps them converged. Upgrades, scale-outs, and node replacements are status transitions on objects, not pipeline runs. The same architecture that makes Crossplane work for application-level resources makes ClusterAPI work for the kubernetes infrastructure beneath them.
The substrate matters because it gives you the properties you actually need: continuous reconciliation, status semantics, event streams, multi-tenancy via namespaces and RBAC, an API contract that a UI or an AI agent can consume. And the pattern is consistent across layers, from the cluster fleet through the platform APIs up to the workloads themselves.
Newer entrants like [formae](https://www.codecentric.de/en/knowledge-hub/blog/formae-part-1-stop-fighting-your-state-file) from [Platform Engineering Labs](https://platform.engineering/), and the various "infrastructure from code" approaches, are interesting because they all try, in different ways, to escape the same trap: the assumption that infrastructure is a thing you apply rather than a thing that exists.
## **Where Terraform still belongs**
Terraform is not bad. It is a fine tool for what it was designed for.
Bootstrap a landing zone. Provision the seed infrastructure a Kubernetes-based platform sits on. Manage the long tail of stable, slow-moving resources: DNS zones, IAM baselines. Run it from a workstation, or from a quarterly pipeline. In that role, Terraform is excellent, mature, and well-understood.
What it should not be is the runtime of your developer platform. The moment a platform user, human or agent, wants to claim a resource and get a queryable, reconciled, status-aware response, you have asked Terraform to be a control plane, and it is not one.
**Conclusion**
The question was never whether Terraform is good. It is. The question is whether it is the right foundation for a developer platform, and the answer is structural, not preferential.
A platform is defined by three properties: a persistent API, a runtime that continuously enforces desired state, and a clean boundary between platform internals and the developer-facing contract. Terraform satisfies none of them by design. That is not a criticism. It was built for a different problem. Wrapping it in pipelines, orchestrators, and glue code does not retrofit those properties. It hides their absence behind increasing operational complexity.
The shift platform engineering needs is conceptual before it is technical. Infrastructure is not a thing you apply. It is a thing that exists, that drifts, that fails partially, and that needs to be continuously reconciled back to intent. Once you accept that framing, the tool selection follows naturally: you need a control plane, not a CLI.
Terraform belongs in that picture. Bootstrapping the substrate a real control plane runs on, managing stable low-churn resources that have no need for a reconciliation loop. That is a meaningful and appropriate role. What it should not be is the runtime your developers and agents interact with, because it was never designed to be one, and no amount of pipeline engineering changes that.
If you are designing a platform today, the question is not which IaC tool to standardize on. The question is what your platform's runtime is. Pick the runtime first. The IaC tool fits inside it, including, where it earns its place. Just stop pretending the pipeline is the platform.
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Blog author
Marc Schnitzius
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.