- 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:
| Layer | Changes | Owned by |
|---|---|---|
| Accounts, org policies, IAM roles | Rarely | Platform |
| Network: VPCs, subnets, peering, DNS zones | Rarely | Platform |
| Shared data services | Occasionally | Platform + product |
| Application workloads | Continuously | Product 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.