Why the landing zone must be code
A landing zone built by clicking is another piece of ClickOps debt, only with more important consequences. You cannot review it, reproduce it or reliably tell what changed. Putting it in Terraform gives you a history, a review process and the ability to build a second environment, or recover from a mistake, by reading a repository.
This page describes how we structure that code, as part of our ClickOps and cloud setup service. For wider Terraform engagements, see Terraform and infrastructure as code, and for how existing console resources are brought across, our migration plan and the blog guide from ClickOps to Terraform on AWS.
What good looks like
- All landing zone resources are defined in a repository you own.
- State is remote, encrypted, versioned and locked.
- Code is split into layers with clear dependencies.
- Changes need a reviewed plan, and apply from a pipeline.
- Provider and module versions are pinned.
- The list of things that are not in code is short, written down and justified.
Repository and layer design
One giant root module is the usual first mistake. Plans become slow, every change risks everything, and permissions cannot be scoped. We separate the code by how often it changes and who is allowed to change it:
| Layer | Contents | Change frequency |
|---|---|---|
| bootstrap | State bucket, pipeline roles | Rare |
| organisation | Organizations, OUs, accounts, SCPs | Occasional |
| identity | Identity Center, permission sets, assignments | Occasional |
| security | Delegated admins, GuardDuty, Security Hub, Config, trails | Occasional |
| network | VPCs, Transit Gateway, DNS zones | Occasional |
| account baseline | Per-account budgets, alarms, default settings | Per new account |
landing-zone/
bootstrap/
organisation/
identity/
security/
network/
baselines/
prod/
nonprod/
modules/
account-baseline/
vpc/
.github/workflows/
Each directory has its own state key, so a mistake in network cannot touch Organizations. Layers read each other’s outputs through explicit data sources or parameters, not by reaching into shared state.
How it works in practice
Bootstrapping state
State needs a home before Terraform can manage anything, which is a chicken-and-egg problem. We create the bucket with a small bootstrap, move its own state into it, and then protect it with versioning, encryption and a policy that denies deletion. A backend block then looks like this:
terraform {
required_version = "~> 1.9"
backend "s3" {
bucket = "acme-tfstate-shared"
key = "landing-zone/organisation/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}
Locking options depend on your Terraform version, so we check the one you run. More on state hygiene is in our Managed DevOps state management notes.
Credentials without secrets
The pipeline authenticates with OpenID Connect. GitHub Actions presents a short-lived token, AWS exchanges it for a role session, and the role’s trust policy limits which repository and branch may assume it. No access keys sit in secrets storage. A workflow fragment:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111111111:role/landing-zone-plan
aws-region: eu-west-1
- run: terraform plan -input=false -lock-timeout=5m
Plan jobs use a read-oriented role, and apply uses a stronger one reachable only from the protected main branch after approval. The account ID shown is a placeholder. The same pattern is described in our CI/CD pipeline work.
Checks on every pull request
terraform fmt -check, validate, tflint, a policy scanner such as Checkov and the plan itself run on every pull request. terraform-docs keeps module documentation current. We add a rule that plans touching IAM, SCPs or networking require a second reviewer, because those are the changes that hurt.
Dealing with the bits Terraform cannot do
Some steps stay manual: the very first login and root MFA (see root hardening), enabling certain organisation-level service access, and registrar settings. We record each, with who does it and when, so none is forgotten at the next account.
Creating accounts
New accounts are created from a module that applies the baseline in one step: SSO assignments, budget, security enrolment and tags. That removes the usual drift between the first account and the twentieth.
Decisions and trade-offs
Terragrunt or plain Terraform? Terragrunt reduces repetition across many accounts and regions but adds another tool to learn. Plain Terraform with a module per concern is enough for most startups.
Community modules or our own? Well-maintained community modules save time for VPCs. For organisation-level resources, small purpose-built modules are clearer and avoid upgrading someone else’s opinions.
Monorepo or repo per layer? One repository with directories is easier to review and keep consistent. Separate repositories help when different teams own different layers.
When not to codify. Throwaway experiments in a sandbox account need not be in code. The landing zone itself always should.
Common failure modes
- Giant state that makes every plan slow and frightening.
- Hand edits in the console leading to constant drift, which we catch with scheduled plans.
- Secrets in state or variables files. We keep secrets in a secrets manager.
- Unpinned providers changing behaviour on the next init.
- Only one person understands it. Documentation and a handover session address this.
Delivery and handover
You receive the repository with a README per layer, architecture decision records, the pipeline definition and a recorded walkthrough. We pair with your engineers on at least one real change, such as adding an account, so the process is tried before we leave. Ongoing maintenance, if you want it, is covered under Managed DevOps and discussed in our engagement models.
How to tell it is working
- Plans on a clean main branch show no changes.
- Scheduled drift detection stays quiet.
- A new account is a pull request that merges in an afternoon.
- No access keys exist in the pipeline settings.
- A new engineer can read the repository and explain the estate.
To scope a landing zone codebase for your environment, contact us.