AWS Organizations is the backbone of any multi-account setup. Before any IaC can run, you need to solve a bootstrap chicken-and-egg problem.
This post covers the Organizations setup, the bootstrap problem, and how to structure accounts and organizational units for a small-to-medium deployment.
The Bootstrap Chicken-and-Egg
Before any Terraform or OpenTofu code runs, you need a management account. This is the one genuinely manual step - there's no API for creating an AWS account from nothing. You'll go through the AWS account creation process which requires an email address, credit card, phone verification, and choosing a support plan (Free tier is fine to start).
The good news: AWS offers a Free Tier that covers a surprising amount of what you'll use during bootstrap. The bad news: not everything in this series is free, and some costs can catch you off guard.
Once that account exists and you have credentials configured, everything else can be managed as code - including creating the organization itself via the aws_organizations_organization resource. But you still need to make some practical decisions about bootstrapping order:
- Create the management account manually (the real chicken-and-egg)
- Configure credentials (IAM user or SSO - whatever gets you started)
- Set up a state backend (even a local state file works initially)
- Run your IaC to create the organization, OUs, and member accounts
Treat root as emergency-only after bootstrap. Day-to-day access should go through IAM Identity Center permission sets.
What This Series Will Cost You
Here's what this multi-account setup actually costs. Not everything is Free Tier eligible, and some services will start billing from day one.
Free across this series:
| Service | Used in | Why it's free |
|---|---|---|
| AWS Organizations | Part 2 | Always free |
| IAM (users, roles, policies) | All parts | Always free |
| STS (role assumption, OIDC) | Parts 5, 8 | Always free |
| IAM Identity Center | Parts 6, 7 | Free with built-in identity store |
| ACM public certificates | Part 10 | Public certs are free |
Free Tier eligible (watch the limits):
| Service | Used in | Free Tier allowance | What to watch |
|---|---|---|---|
| S3 | Parts 3, 4 | 5 GB storage, 2,000 PUT and 20,000 GET requests/month | Terraform runs generate more requests than you'd expect - every plan and apply reads and writes state. CloudTrail logs add up too |
| DynamoDB | Part 4 | 25 GB storage, 25 WCU/25 RCU | State locking is lightweight, unlikely to exceed |
| CloudTrail | Part 3 | First trail per region (management events only) | One organization trail is typically enough |
| CloudWatch Logs | Part 3 | 5 GB ingestion/month | Only a concern if you enable DNS query logging |
Costs money from the start:
| Service | Used in | Cost | Notes |
|---|---|---|---|
| Route 53 hosted zones | Part 10 | $0.50/zone/month | Each domain needs a zone. Two zones = $1/month |
| Route 53 DNS queries | Part 10 | $0.40/million queries | Negligible for low-traffic sites |
| AWS Config | Part 3 | $1/account/month + $0.001/rule evaluation | Adds up across multiple accounts |
The S3 surprise: During initial setup, Terraform generates a lot of S3 API requests. Every tofu plan reads the state file, every tofu apply writes it back, and DynamoDB gets a lock/unlock cycle each time. When you're iterating on infrastructure code - running plan-apply dozens of times while getting things right - those 2,000 free PUT requests can disappear faster than you'd think. It won't break the bank (overage is $0.005 per 1,000 requests), but it's worth knowing it's not truly "free" during active development.
Realistic total: For the complete setup across this series, expect $3-10/month for a small organisation. Route 53 and AWS Config are the main drivers. Everything else is either free or costs pennies.
Understanding the Hierarchy
AWS Organizations creates a tree structure:
Root: The top of the hierarchy. Every organization has exactly one root. You can't delete it or create another.
Organizational Units (OUs): Containers for grouping accounts. OUs can be nested, though we keep them flat for simplicity in this case.
Accounts: The actual AWS accounts where resources live. Each account belongs to exactly one OU (or the root directly).
Management Account: The account where you created the organization. It has special privileges and restrictions - never run workloads here.
OU Structure
We use three organizational units:
organizational_units = {
"Production" = {}
"Infrastructure" = {}
"Security" = {}
}Production: Accounts running customer-facing workloads. The website production account lives here.
Infrastructure: Shared services like DNS. The DNS management account lives here.
Security: Security-focused services and accounts (for example log archive, delegated security tooling, and guardrail-related infrastructure). Even if initially light on workloads, this OU creates a clean boundary for future security centralization.
Some organisations also create OUs for Development, Staging, and Sandbox. That's valid for larger teams - see the recommended OU structure in AWS's multi-account whitepaper. For this setup, Production, Infrastructure, and Security provide a practical baseline.
Creating the Organization Resource
Here's the OpenTofu configuration that manages Organizations:
resource "aws_organizations_organization" "org" {
feature_set = "ALL"
enabled_policy_types = [
"SERVICE_CONTROL_POLICY"
]
aws_service_access_principals = [
"sso.amazonaws.com",
"cloudtrail.amazonaws.com",
"config.amazonaws.com",
"iam.amazonaws.com",
"account.amazonaws.com"
]
}feature_set = "ALL": Enables all Organizations features, not just consolidated billing. Required for SCPs and delegated administration.
enabled_policy_types: We enable Service Control Policies even though they're not heavily used yet. It's easier to enable upfront than retrofit later.
aws_service_access_principals: These allow AWS services to operate across your organization. Each one enables specific cross-account functionality:
| Service principal | Purpose |
|---|---|
sso.amazonaws.com | IAM Identity Center integration |
cloudtrail.amazonaws.com | Organization-wide CloudTrail |
config.amazonaws.com | Cross-account AWS Config |
iam.amazonaws.com | Centralized root access management |
account.amazonaws.com | AWS Account Management APIs |
Creating Organizational Units
OUs are straightforward:
resource "aws_organizations_organizational_unit" "ous" {
for_each = var.organizational_units
name = each.key
parent_id = data.aws_organizations_organization.org.roots[0].id
tags = {
Name = each.key
ManagedBy = "OpenTofu"
}
}The parent_id references the organization root. For nested OUs, you'd reference another OU's ID instead.
Account Vending
Creating new AWS accounts through Organizations is called "account vending". Here's the configuration:
resource "aws_organizations_account" "accounts" {
for_each = var.accounts_to_create
name = each.key
email = each.value.email
role_name = "OrganizationAccountAccessRole"
iam_user_access_to_billing = "DENY"
close_on_deletion = false
parent_id = aws_organizations_organizational_unit.ous[each.value.organizational_unit].id
lifecycle {
prevent_destroy = true
}
}Key points:
email: Each AWS account needs a unique email address. We use email aliases like aws-website-prod@example.com.
role_name: Organizations automatically creates this IAM role in the new account, allowing the management account to assume it. This is how cross-account management works initially.
iam_user_access_to_billing = "DENY": Prevents IAM users from accessing billing. Only the root user (and Identity Center users with billing permissions) can see costs.
prevent_destroy = true: Deleting an AWS account is a significant action. This lifecycle rule requires manual intervention.
Importing Existing Accounts
The DNS account existed before Organizations was set up. It was imported into the organization rather than created fresh:
# Import existing account
import {
to = aws_organizations_account.imported["dns"]
id = "111111111111" # Example existing account ID
}Importing accounts is straightforward, but there's a catch: you can't change the email address or account name after import. Plan your naming carefully.
What the Management Account Should (and Shouldn't) Do
The management account has special powers:
| Item | Keep in management account? | Why |
|---|---|---|
| AWS Organizations configuration | Yes | This is the organization control plane |
| IAM Identity Center (or delegated admin setup) | Yes | Identity control belongs at org level |
| Organization CloudTrail trail | Yes | Central audit ownership |
| Terraform state backend | Yes | Shared control-plane dependency |
| CI/CD cross-account roles | Yes | Common trust anchor for deployments |
| Production workloads | No | Management account is not covered by SCPs |
| Development resources | No | Reduces attack surface and operational noise |
| Anything internet-facing or high-risk | No | Compromise impact is organization-wide |
The management account can't have SCPs applied to it - that's an AWS protection. If someone compromises this account, they own your entire organization. Keep it locked down.
Consolidated Billing
Organizations automatically consolidates billing across all member accounts. You get a single invoice, and each account's costs appear as a separate line item - so you can see exactly what the website production account's Amplify hosting costs versus the DNS account's Route 53 charges.
You also get volume discounts from aggregated usage, tag-based cost allocation, and Reserved Instance sharing across accounts. There's nothing to enable - it's automatic once accounts join the organisation.
Common Mistakes
Running workloads in the management account: Don't. Create a separate account even for personal projects. See AWS's multi-account best practices for more guidance.
Too many OUs too early: Start simple. You can always add OUs later. Refactoring OU structure is easier than maintaining unnecessary complexity.
Forgetting email uniqueness: Each account needs a unique email. Set up a convention (aws-{purpose}@yourdomain.com) before you need it.
Not enabling all features: If you only enable consolidated billing, you can't use SCPs or delegated administration later without migrating.
What's Next
With Organizations in place, the next step is establishing security foundations: CloudTrail for audit logging, AWS Config for compliance monitoring, and IAM password policies.
These services are set up in the bootstrap repository and give the whole organisation its security baseline.