OpcotechOpcotech

When a Terraform repo stops being a platform

Infrastructure code becomes a bottleneck when state, modules, and deployment responsibilities lack clear boundaries. Here's what to separate first.

OpcotechInfrastructure as code · Platform engineering

Putting infrastructure in Terraform is not the same as building an internal platform.

A repository becomes difficult to maintain when unrelated systems share state, modules lack stable interfaces, and engineers need to understand the entire infrastructure before making a small change.

At that point, the problem is not Terraform syntax. It is the ownership and deployment boundaries encoded in the repository.

Infrastructure code is not a platform contract

An internal platform is a contract between people who build product and people who build the system underneath it. The contract says what a team gets without asking, what they are allowed to change, and what will be rejected.

A Terraform repository does not say any of that by existing. It says that some resources were once declared. If every team copies a folder, edits the inside, and opens a pull request against a root module the size of a novel, you have infrastructure code. You do not have a path.

You can see the difference in review. In a platform, a product engineer changes inputs: a name, a size, an allow-list. The plan is short. In a history repo, the same engineer changes resources they do not fully recognize, and the plan includes an unrelated security group because someone refactored a local last quarter. Reviewers either block the change or stop reading. Both outcomes are expensive.

How Terraform repositories become difficult to maintain

It usually happens in this order.

Someone creates a root module for the VPC, then adds the cluster, then adds the database the cluster needed, then adds the second environment by copying the directory. CI applies it with a cloud key that can do anything, because splitting IAM was going to be next month’s work. A hotfix edits state. A sandbox is created in the same account so a developer can move faster, and its resources are imported “temporarily.”

None of these decisions is stupid in the week it is made. Together they remove the boundary the platform needed.

The symptoms are specific:

  • Plans regularly include resources the author did not mean to touch.
  • State is shared across concerns that fail independently.
  • There is no versioned module, only a folder that every stack has pasted.
  • The console and the code disagree, and the next apply is how you find out.
  • Product engineers wait on the one person who is willing to run it.

At that point, adding a developer portal on top is a category error. The portal would be a friendlier way to be confused.

Where to introduce module and state boundaries

Do not import the entire account into a cleaner layout before you know which boundary hurts. Pick the slice people are afraid to change. Often that is the network, the cluster, or a single environment’s data stores. Sometimes it is the root module that has become the company’s change-management process.

Draw two kinds of code:

  • Foundation. Account structure, network, identity, the cluster, shared observability. Product teams consume outputs. They do not edit the resources.
  • Workload. The things a single service owns. Its identity, its database, its queue. A team should be able to plan this without planning the VPC.

The foundation is allowed to move slowly. The workload stack is allowed to move every day. When those share a state file, you have chosen the slower speed for both.

A module earns its existence when you repeat a decision and you need the decision to stay constrained. A module for every cloud resource is how registries become junk drawers. A module with forty boolean inputs is a root module wearing a hat. Prefer a small surface: the choices a team is qualified to make, and no others.

module "workload_identity" {
  source = "git::ssh://git@github.com/example/terraform-modules.git//workload-identity?ref=v1.4.0"

  name        = "billing-api"
  environment = "production"
  namespace   = "billing"
}

The version matters more than the tool. Teams pin a ref. Improvements move by upgrading the ref, not by copying a bugfix into twelve folders. OpenTofu and Terraform are the same story here. Switching between them will not repair a missing boundary.

State isolation, permissions, and deployment safety

State boundaries are blast-radius boundaries. A lock that covers production and the sandbox means a sandbox experiment can block a production fix, and a production apply can destroy a sandbox someone forgot was in the graph.

Separate state by the unit you are willing to break at once. Give CI a role that can apply that unit and not the account. Humans approve plans that leave the happy path. An apply that requires a long-lived admin key on a laptop is not a workflow. It is a privilege you have not designed.

Drift is the other half. If the console remains a legitimate way to change production, the repository is a suggestion. Detect the drift you care about, or stop pretending. You do not need a policy engine on day one. You need one rule: a change that matters is a pull request, and the alternative is visible.

Refactoring mistakes to avoid

Do not start with a catalog of every service. Do not boil every stack in one quarter. Do not write modules for resources you have one of. Do not treat “100% of resources in Terraform” as the goal. Abandoned experiments can stay abandoned until cost or risk makes them interesting.

The goal is narrower. A product engineer can create or change the infrastructure their service is allowed to own, the plan is reviewable, and the foundation moves on a different cadence. When that is true, you have the beginning of a platform. The repository is how it is expressed. It was never the platform itself.

The goal is not to create the perfect Terraform repository.

It is to make routine infrastructure changes understandable, reviewable, and limited to the systems they are intended to affect.

Start with one change that your engineers currently avoid. Follow it from pull request to apply, identify the dependencies that make it risky, and use that evidence to decide which boundary needs to change.

Explore Terraform consulting.