• cloud
  • terraform

Terraform landing zones: the five decisions you can't undo later

Account structure, naming, state layout, network topology and identity. Everything else in a landing zone is reversible; these five are not, so decide them first.

Most of a cloud landing zone is reversible. Instance types, autoscaling thresholds, alerting rules, even which managed database you picked — all of it can be changed later by someone with an afternoon and a change window.

Five things cannot, or at least not without a migration project that nobody will fund. They are worth an unhurried week before the first terraform apply, because the cost of getting them wrong is not paid on the day you notice.

1. Account and subscription structure

The decision: how many accounts, split along which axis.

The axis choices are roughly per environment (prod / staging / dev), per team, per product, or some combination. Whichever you pick becomes the boundary for billing, quotas, IAM blast radius and — most importantly — the thing that is hard to cross.

The failure mode is the single-account start. Everything lives together, so everything can reach everything, and by the time the organisation wants isolation there are four hundred resources with implicit dependencies across the boundary that does not exist yet.

The defensible default is one account per environment per workload domain, even when that produces accounts that sit nearly empty for a year. An empty account costs nothing. A resource in the wrong account costs a migration.

What makes this irreversible is not the resources — it is the identifiers. Account IDs appear in trust policies, bucket ARNs, peering configurations, log destinations and every piece of automation anyone wrote in between.

2. Naming and tagging

The decision: the exact string format for resource names, and the tag keys that are mandatory.

This sounds like bikeshedding and is not. Names are how every subsequent piece of automation finds things, and tags are the only mechanism by which a finance team can ever answer "what does this product cost".

Pick a scheme that is parseable, not just readable:

locals {
  # <org>-<env>-<domain>-<component>[-<index>]
  name_prefix = "br-${var.env}-${var.domain}"
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "${local.name_prefix}-artifacts"

  tags = {
    Environment = var.env
    Domain      = var.domain
    Component   = "artifacts"
    ManagedBy   = "terraform"
    Repo        = var.repo_url
  }
}

The irreversible part is not the convention — it is the resources that cannot be renamed in place. S3 buckets, storage accounts, DNS names, database identifiers: renaming means create-copy-cutover-delete. A scheme adopted in month nine applies only to what is built after month nine, and you live with two conventions permanently.

Make ManagedBy and Repo mandatory from the first resource. The question "who created this and where is the code" gets asked constantly, and answering it by archaeology is how afternoons disappear.

3. State layout

The decision: how many state files, and where the boundaries between them run.

One state file for everything is fast to start and becomes unusable at a pace that surprises people: plans take minutes, every change locks every team, and one corrupted apply threatens the whole estate.

Split state along the axis of change frequency and ownership, not tidiness:

LayerChangesOwned by
Accounts, org policies, IAM rolesRarelyPlatform
Network: VPCs, subnets, peering, DNS zonesRarelyPlatform
Shared data servicesOccasionallyPlatform + product
Application workloadsContinuouslyProduct teams

Splitting later is possible — terraform state mv, or import into a new root module — but it is a careful, manual, downtime-adjacent operation across every resource, done by the person who understands the estate best, on a day they wanted to do something else.

4. Network topology and address space

The decision: the CIDR ranges, and the shape of connectivity between them.

Address space is the classic one-way door. A /24 per environment feels generous until someone needs a Kubernetes cluster with a pod-per-IP network model, and there is no contiguous space left to grow into. Renumbering a live VPC is not a change; it is a rebuild.

Allocate from a plan with room to be wrong:

  • Reserve a large, contiguous block for the whole organisation before allocating any of it.
  • Give each environment a range that does not overlap any other environment, any office network, any VPN client pool, or any acquisition target's likely range. Overlapping ranges make future peering impossible without NAT.
  • Leave gaps. The cost of an unused range is zero.

The connectivity shape — hub-and-spoke through a transit gateway, full mesh peering, or strict isolation — is somewhat more reversible, but it determines routing, egress cost and where inspection happens, all of which get baked into other decisions quickly.

5. Identity and the CI/CD trust path

The decision: how humans authenticate, and how the pipeline authenticates.

The pipeline half is the one that gets deferred. It starts as a long-lived access key in a CI secret, because that takes ten minutes and OIDC federation takes an afternoon. Two years later that key has been copied into four systems, its permissions have grown by accretion, and nobody can revoke it because nobody knows everything it is used by.

Set up short-lived, federated credentials on day one:

resource "aws_iam_role" "ci_deploy" {
  name = "${local.name_prefix}-ci-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        # Scope to one repo and one branch. Without this condition, ANY repository
        # in the organisation can assume this role.
        StringLike = {
          "token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }]
  })
}

The human half is equally sticky: whether identity comes from a directory you already run, and whether access is standing or requested. Retrofitting SSO onto an estate where fifty engineers have personal accounts is a project with a name and a budget.

What this buys

None of the five is difficult in isolation. What makes them worth separating out is that each one is cheap this week and expensive in a year, and they are all invisible in the demo. A landing zone is judged on whether the first workload deployed — which is exactly the wrong test, because the first workload deploys fine on top of every one of these decisions made badly.

Start a conversation

Tell us what you're trying to build.

A short description is enough to start. If we're not the right fit, we'll say so and point you somewhere better.

  • 01We reply within one business day.
  • 02A 30-minute call with an engineer, not a salesperson.
  • 03If it's a fit, a written scope and a fixed-price discovery.

Book a call
Terraform landing zones: the five decisions you can't undo later · BinaryRise