Most AWS environments grow in the same direction: one account becomes two, two becomes five, and eventually someone is manually clicking through the console trying to remember which account has which guardrails. We've seen this pattern repeatedly, both in our own infrastructure at NextLink Labs and across the client engagements we run. AWS Organizations with Service Control Policies (SCPs) is the right fix. Terraform is the right tool to manage it.
This post covers the account structure we use, the Terraform patterns behind it, and the SCP logic you need to lock things down without breaking your teams.
AWS Organizations sits above your individual accounts and gives you three things worth caring about: consolidated billing, Service Control Policies, and delegated administration for security services like GuardDuty, Security Hub, and Config.
SCPs are a ceiling, not a floor. An SCP that allows s3:* does not grant anyone S3 access. It just means S3 access is not blocked at the organization level. IAM still has to permit it. SCPs can only restrict permissions, not grant them.
Terraform is what keeps this manageable over time. Without it, SCPs drift. Someone adds an exception through the console, nobody documents it, and six months later you're debugging a broken deployment because an SCP was silently blocking an API call. We treat any console-based SCP change as immediate technical debt.
Before writing a line of Terraform, you need an OU hierarchy. Here is what ours looks like at NextLink, and the same structure is what we bring to new client engagements as a starting point:
We keep Organizations Terraform in a dedicated root module, separate from workload infrastructure. It runs from the management account with elevated permissions, and changes here have a blast radius across every account in the org.
Rather than building all of this from scratch every time, we have an internal module that handles the OU structure, account creation, SCP attachment, and the backend configuration in one pass. It cuts the setup time significantly on new client engagements and ensures we are not reinventing the same patterns each time. The directory layout it produces looks like this:
Keeping SCP JSON in separate files rather than inline HEREDOCs makes diffs readable and lets you lint the JSON independently.
feature_set = "ALL" is required to use SCPs. If you are importing an existing org that was created with consolidated billing only, enabling ALL features requires acceptance from each member account. Plan for that change window.
AWS account creation is eventually consistent and takes a few minutes. If you create accounts and attach SCPs in the same apply, add depends_on chains or split it into two stages.
If your org, OUs, or accounts already exist and were created manually, you will need to import them into Terraform state before managing them. The commands follow the same pattern but the IDs are not obvious if you have not done this before.
You can find the org ID, OU IDs, and account IDs in the AWS console under AWS Organizations, or by running aws organizations describe-organization and aws organizations list-organizational-units-for-parent. Run the imports before doing anything else, otherwise Terraform will try to create resources that already exist and fail.
No workload should ever use the root user. This SCP enforces that at the org level:
Attach this to every OU except the management account root, where you may legitimately need root for billing operations.
Restrict accounts to the regions you actually use. This prevents accidental deployments to unmonitored regions and reduces your exposure from resource-based policies:
The NotAction list is critical here. Global services like IAM, Route 53, CloudFront, and billing have no region concept, so if you block them with a region condition you will break your accounts in ways that are confusing to debug. The list above covers the standard set; review it against any other global services you are using.
This one prevents an account from being removed from your org. It is a common technique attackers use when an account is compromised:
On the Security and Infrastructure OUs, deny any action that would disable your visibility into what is happening:
The condition carves out a break-glass role so you are not completely locked out when you need to make a legitimate change. That role should require MFA and every assumption should be logged.
For sandbox accounts, we block expensive instance types at the policy level rather than relying on developers to self-police:
SCPs attach to OUs rather than accounts directly, though account-level attachment is available. OU-level is almost always the right choice because new accounts inherit policies automatically when they are moved into the OU.
SCPs are not forgiving. A misconfigured policy attached to the root can break all accounts at the same time, which is a bad situation to be in on a Friday afternoon.
Before attaching anything to a production OU, we follow a consistent process:
For the region lockdown SCP specifically, test from an account with active workloads and confirm that us-east-1 global service calls (IAM, STS, etc.) still work before rolling it out more broadly.
It is also worth setting up an EventBridge rule to alert when any SCP is modified outside of Terraform. CloudTrail logs every organizations: API call, so catching a console change is straightforward.
This rule lives in the management account and catches any SCP modification regardless of how it was made. If something changes outside of a pipeline run, you will know about it.
The management account Terraform state should live in an S3 backend in the management account itself, with DynamoDB locking. Do not store it in a member account. If that account is compromised or an SCP change locks you out, you lose state access.
The IAM role running this Terraform needs organizations:* and the ability to assume roles in member accounts if you are managing account-level resources. Keep this role separate from your standard admin roles and log every assumption.
We manage the Organization's Terraform through a dedicated GitLab CI pipeline rather than running it locally. Any change to SCPs or account structure goes through a merge request, requiring sign-off from both a tech lead and a senior engineer before anything touches the pipeline. Given that a bad change here can affect every account in the org simultaneously, having a second and third set of eyes is not optional.
The pipeline itself is straightforward. On every push, it runs terraform plan and posts the output as an MR comment so reviewers can see exactly what will change without having to run anything locally. The apply job is manual and gated behind MR approval, so it cannot run until the required reviewers have signed off.
The CI runner assumes an IAM role with the permissions needed to manage Organizations resources. Credentials are passed in as CI/CD variables and never stored in the repository. We also have branch protection enabled on main so that direct pushes are blocked entirely. Every change has a paper trail, which matters when you need to explain why a permission is blocked or when something breaks after a deployment.
Get the OU structure right before you start writing resources, because reorganizing it later means moving accounts around and re-testing SCP inheritance. Everything else is easier to change.
The parts that take the most time in practice are the region lockdown NotAction list and the break-glass carveouts on the security tooling SCP. Both need real testing against live workloads before you roll them to production OUs.
If you are starting from an existing multi-account setup without Organizations, you will need to accept a features invitation from each member account before SCPs are available. Give account owners a heads up and plan for a change window.
If you are dealing with a messy multi-account setup, starting from scratch, or just want someone to do this properly the first time, we can help. This is work we do regularly, both for our own infrastructure and for clients across a range of AWS environments.
Most companies we talk to are one bad SCP change away from a production outage they can't roll back. If that sounds familiar, let's talk.