AWS Landing Zone Automator
An account vending machine for the gap between one shared account with a root login and a platform team running Control Tower. One apply stands up a SOC 2 ready multi-account foundation; after that a new account is one block in a tfvars file and it arrives with guardrails, logging, budgets, and SSO already applied.
/01Problem
Multi-account AWS is the recommendation everyone gives and almost nobody implements, because the first account separation is where the work actually is: organizational units, service control policies, centralized immutable audit logging, and single sign-on all have to exist before the second account is worth having.
The target was the middle of that gap. Startups getting SOC 2 ready need account separation, immutable audit logs, least-privilege SSO, and root controls, which is most of what an auditor asks about first. SaaS teams need dev, staging, and prod per product. MSPs need a client to land inside guardrails on day one. All three are the same recurring workflow, not a one-time script.
/02Approach
- Organizations with all features enabled, and an OU tree of Security, Workloads/Prod, Workloads/NonProd, and Sandbox, so policy attaches to a boundary rather than to individual accounts.
- Four service control policies as the guardrail layer: deny root user actions, deny leaving the organization, a region allowlist, and CloudTrail tamper protection.
- An organization CloudTrail encrypted with SSE-KMS writing into a versioned, object-locked bucket in a dedicated log-archive account, so the audit trail is outside the accounts it is auditing and cannot be rewritten by them.
- Vending itself is an `account_requests` map: each entry creates an account in the right OU with tags, a monthly budget alarm at 80 percent, and an in-account baseline that sets an IAM alias, a strict password policy, a smoke-test role, and removes the default VPC.
/03Architecture
The apply runs in two stages, and the reason is a real Terraform constraint rather than a workaround. Provider configurations are static and must resolve at plan time, but the aliased providers that assume roles into the log-archive and vended accounts need account IDs that do not exist until the first stage finishes. A helper script copies those IDs from stage one outputs into a gitignored tfvars file, and the second apply completes the cross-account wiring.
Nothing sensitive reaches the repository. Account emails, notification addresses, and account IDs live only in gitignored tfvars and remote state, outputs carrying account IDs are marked sensitive, the state bucket name is passed through a gitignored backend config, and CI authenticates with GitHub OIDC against the committed example file only.
/04Outcome
Deployed live against a real organization and validated on six independent checks: an SCP explicitly denying a disallowed region, per-account organization CloudTrail delivery, least-privilege SSO assignment, budget alarms, tag and OU placement, and zero default VPCs in the vended accounts.
Teardown taught more than the build. Closed accounts block OU deletion until they are moved back to the root, the account-close waiter finishes a minute or so before AWS actually settles, and the SSO assignment loop needs the account request map emptied before the final destroy pass will complete. All three are documented in the repo, because the teardown path is the part of a landing zone nobody writes down.