src/config/organization-structure.ts is where you define your entire AWS Organization hierarchy: which Organizational Units exist, which accounts belong to each OU, and which SCPs attach at the root, OU, or account level.
The OrganizationConstruct in the foundation package reads this object and creates or updates OUs, accounts, and SCP attachments on every deploy. It also publishes the resolved organization identifiers as CloudFormation stack outputs so later stacks can consume them as StackSet targets. OU IDs are published by default; account IDs are opt-in per account with publishAccountIdOutput.
Built-in OUs
The starter ships with six top-level OUs that match common AWS multi-account patterns:
| OU key | Default name | Purpose |
|---|---|---|
SecurityOU | security-ou | Holds the security account, the delegated administrator for the security services (see Delegated Administrators), and the observability account, which runs the organization CloudTrail trail and keeps its logs |
FinOpsOU | finops-ou | Holds the FinOps account, the delegated administrator for AWS Compute Optimizer and AWS Cost Optimization Hub |
InfrastructureOU | infrastructure-ou | Holds the landing zone account that deploys the landing zone StackSets as the StackSets delegated admin |
DevelopmentOU | workload-dev-ou | Workload accounts for non-production environments, including the sandbox |
ProductionOU | workload-prod-ou | Workload accounts for production environments, with a combined monthly budget of 500 USD in the starter |
SuspendedOU | suspended-ou | Receives the lockdownSuspendedAccountsSCP and nests the CloseAccountOU |
SuspendedOU nests a CloseAccountOU (close-account-ou), which makes seven OUs in total. The two model different offboarding states, covered in Suspended vs. close-account OU below.
There is no log archive OU or account. The ObservabilityAccount in the Security OU runs the organization trail (it is the CloudTrail delegated administrator), keeps the log buckets, and receives the CIS alarm notifications on one security-alerts topic. See Delegated Administrators.
You can rename, add, or remove OUs by editing the organizationalUnits map. The OU key (e.g. SecurityOU) becomes the stack output name (SecurityOUId) and the TypeScript property name on orgVars; changing a key is a compile-time-checked rename.
The SecurityAccount, ObservabilityAccount, FinOpsAccount, and LandingZoneAccount keys are referenced by other stacks and delegated-admin config, so they set publishAccountIdOutput: true. Renaming those keys is a compile-time-checked change you will see reflected in delegated-admins.ts and the StackSet targeting.
Options
The OrganizationStructure type has a single required top-level key:
| Option | Type | Required | Description |
|---|---|---|---|
root | OrganizationRootStructure | Yes | The organization root node. Holds SCPs, budgets, and the OU map. |
Each OrganizationRootStructure:
| Option | Type | Required | Description |
|---|---|---|---|
serviceControlPolicies | ServiceControlPolicy[] | No | SCPs attached at the organization root (apply to every account). Max 9 configured here when FullAWSAccess is attached; 10 total attachments. |
accounts | Record<string, AccountStructure> | No | Accounts attached directly to the organization root. |
organizationalUnits | Record<string, OrganizationalUnitStructure> | No | Map of OU key → OU definition. |
budget | BudgetStructure | No | Monthly cost budget for the whole organization, including the management account and accounts the structure does not define. Default: no organization-wide budget. |
budgetAlertRecipients | string[] | No | Email addresses (1 to 10) that receive every budget in the organization that sets no emailAddresses, unless an OU above the budget sets its own. |
Each OrganizationalUnitStructure entry:
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name of the OU in AWS Organizations. |
serviceControlPolicies | ServiceControlPolicy[] | No | SCPs attached to this OU. Max 9 configured here when FullAWSAccess is attached; 10 total attachments. |
accounts | Record<string, AccountStructure> | No | Map of account key → account definition. |
organizationalUnits | Record<string, OrganizationalUnitStructure> | No | Nested child OUs. Nested OUs must stay within AWS Organizations depth limits. |
budget | BudgetStructure | No | Monthly cost budget for the combined cost of every account under this OU, nested OUs included. Default: no budget. |
budgetAlertRecipients | string[] | No | Email addresses (1 to 10) for every budget at or under this OU that sets no emailAddresses. Replaces the list set on a parent OU or the root. |
Each AccountStructure entry:
| Option | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name of the account in AWS Organizations. |
email | string | Yes | Root email address for the account. Must be globally unique across all of AWS. |
publishAccountIdOutput | boolean | No | Publish this account's ID as a stack output and typed orgVars.<Key>Id variable. Set it for accounts other stacks reference. Default false. |
serviceControlPolicies | ServiceControlPolicy[] | No | SCPs attached to this specific account. Max 9 configured here when FullAWSAccess is attached; 10 total attachments. |
budget | BudgetStructure | No | Monthly cost budget for this account. Default: no budget. |
Each BudgetStructure:
| Option | Type | Required | Description |
|---|---|---|---|
monthlyLimit | number | Yes | Monthly cost limit in US dollars. Must be greater than 0. |
emailAddresses | string[] | No | Email addresses (1 to 10) alerted when the limit is exceeded. Default: the nearest budgetAlertRecipients above the budget, else the email of each account it covers. |
Example
const mailDomain = landingZoneSettings.mailDomain.toLowerCase();
export const organizationStructure = {
root: {
budgetAlertRecipients: [`aws+finops@${mailDomain}`],
serviceControlPolicies: [
criticalSecurityGuardrailsSCP,
protectTaggedCloudFormationStacksSCP,
denyAllOutsidePrimaryAndSecondaryRegionsSCP,
],
organizationalUnits: {
SecurityOU: {
name: 'security-ou',
accounts: {
SecurityAccount: {
serviceControlPolicies: [protectSecurityHubConfigurationSCP],
name: 'security-account',
email: `aws+security@${mailDomain}`,
publishAccountIdOutput: true,
},
// Runs the organization trail and keeps its logs.
ObservabilityAccount: {
name: 'observability-account',
email: `aws+observability@${mailDomain}`,
publishAccountIdOutput: true,
},
},
},
FinOpsOU: {
name: 'finops-ou',
accounts: {
FinOpsAccount: {
name: 'finops-account',
email: `aws+finops@${mailDomain}`,
publishAccountIdOutput: true,
},
},
},
InfrastructureOU: {
name: 'infrastructure-ou',
accounts: {
LandingZoneAccount: {
name: 'landingzone-account',
email: `aws+landingzone@${mailDomain}`,
publishAccountIdOutput: true,
},
},
},
DevelopmentOU: {
name: 'workload-dev-ou',
accounts: {
SandboxAccount: {
name: 'Sandbox',
email: `sandbox@${mailDomain}`,
},
},
},
ProductionOU: {
name: 'workload-prod-ou',
// One budget for the combined cost of every account in this OU.
budget: { monthlyLimit: 500 },
accounts: {
WorkloadAlphaAccount: {
name: 'Workload Alpha',
email: `aws+workload-alpha@${mailDomain}`,
},
// Account emails don't have to use the organization mail domain;
// any unique, valid address works.
WorkloadBetaAccount: {
name: 'Workload Beta',
email: 'team-beta+aws@example.org',
},
},
},
// Quarantines accounts for investigation: the lockdown SCP locks out their users
// and workload roles while administrators and landing-zone automation keep
// working, and security services stay on. Child OUs inherit the SCP.
SuspendedOU: {
name: 'suspended-ou',
serviceControlPolicies: [lockdownSuspendedAccountsSCP],
organizationalUnits: {
// Moving an account here triggers the close-account automation, which closes
// the account permanently. Security Hub CSPM, GuardDuty and Inspector are turned
// off here, and Macie stops managing these accounts.
CloseAccountOU: {
name: 'close-account-ou',
},
},
},
},
},
} satisfies OrganizationStructure;
export type LandingZoneOrganizationStructure = typeof organizationStructure;
The OU and account name fields are the display names shown in the AWS Organizations console; they are plain strings, not derived from organizationName. The map keys (SecurityOU, SecurityAccount) are what become the typed orgVars.<Key>Id outputs.
Budgets
An optional budget on the root, an OU, or an account creates a monthly AWS Budgets cost budget in the management account, which sees the cost of every member account. On an account it tracks that account. On an OU it tracks the combined cost of every account under it, nested OUs included. On the root it tracks the whole organization, including the management account and accounts the structure does not define. An OU or account without its own budget is only tracked by a budget above it.
Recipients get an email when actual or forecasted cost for the month exceeds the limit. A budget picks its recipients from the first of these that is set:
- The budget's own
emailAddresses. - The nearest
budgetAlertRecipientson an OU above it (an OU budget includes its own OU's list). budgetAlertRecipientson the root.
Without any of these, an account budget alerts the account's email, and an OU or root budget alerts the email of each account the structure defines under it, up to 10. Each list holds 1 to 10 addresses, the maximum AWS Budgets allows per notification (Adding email recipients to a budget notification). The starter sets budgetAlertRecipients on the root to the FinOps account email and one 500 USD budget on ProductionOU.
Suspended vs. close-account OU
The structure distinguishes two offboarding states, and the CloseAccountOU nests inside SuspendedOU on purpose.
SuspendedOUquarantines an account while keeping it, so administrators can investigate. ThelockdownSuspendedAccountsSCPdenies every principal except administrators, the StackSet execution roles, and the landing zone's own automation. Existing resources keep running, but the account's own users and workload roles can no longer call AWS. Landing zone StackSets that target the whole organization keep deploying, and Security Hub CSPM, GuardDuty, Inspector, and Macie stay on, so the security baseline and detection continue during the investigation. StackSets that target specific OUs are removed from the account when it leaves those OUs:CdkBootstrapStackSet(Development and Production OUs) deletes the CDK bootstrap, including its asset bucket, so workloads can't deploy until the account moves back and is bootstrapped again.ServiceQuotasStackSetremoves its stack, but the raised quotas stay.CloseAccountOUpermanently closes an account. When this OU exists in the structure, the organization phase (LandingZoneOrganizationStack, in the management account) closes its active accounts every 8 hours. It inherits the lockdown SCP, turns Security Hub CSPM, GuardDuty, and Inspector off, and stops managing Macie. Accounts waiting for closure remain locked down; failed closures are retried on the next run.
Closed accounts remain visible in the OU with SUSPENDED status for the 90-day post-closure period. AWS can still bill resources left in a closed account, so clean up costly resources before moving an account here. If you do not want the automation at all, remove the CloseAccountOU from the structure; suspended accounts then simply stay isolated.
How it's used
The OrganizationConstruct in the organization phase (LandingZoneOrganizationStack) reads this object and:
- Creates the AWS Organization if it does not exist
- Creates OUs and accounts as specified
- Attaches SCPs at root, OU, and account levels
- Creates the monthly budgets
- Publishes every OU ID as a
<Key>Idstack output, plus an account<Key>Idoutput for each account markedpublishAccountIdOutput: true
The landing zone stacks run from the landing zone account and read those outputs cross-account at deploy time, exposed to them as typed orgVars properties. LandingZoneOrganizationStack just has to be deployed before the stacks that consume its outputs.
Things to know
- Keep the
FinOpsOU→FinOpsAccountentry.FinOpsAccountis registered as the delegated administrator for Compute Optimizer, Cost Optimization Hub, and Resource Explorer in delegated-admins.ts, and the FinOps StackSet targets it by its published account ID. - Keep the
SecurityOUaccounts. The security account administers the security services, and the observability account runs the organization trail. Never suspend or close the management, landing zone, security, or observability accounts. - Account email addresses must be globally unique across all of AWS. The
aws+<alias>@<domain>pattern is a reliable way to generate unique addresses under a single domain. - AWS Organizations limits SCPs to 10 directly attached per attachment point (root, OU, or account). Inherited SCPs do not count against a child's own quota.
OrganizationConstructvalidates the structure at synth time: it fails when a node configures more than 10 SCPs or a policy document exceeds 10,240 characters, and warns when a node or policy approaches the limit. This check excludesFullAWSAccessand other policies attached outside the configuration. WithFullAWSAccessattached, configure at most 9 additional SCPs at that attachment point, and check existing attachments before deploying. - Account IDs are opt-in. OU IDs are always published, but an account only gets a
<Key>Idoutput (and anorgVars.<Key>Idproperty) when you setpublishAccountIdOutput: true. Reserve it for accounts other stacks reference by ID, such as the security, observability, FinOps, and landing zone accounts. A StackSet that targets a whole OU never needs one. Keep published outputs within the CloudFormation per-stack output limit. - Renaming an account or OU key (e.g.
WorkloadAlphaAccount→AppAlphaAccount) changes the stack output name and breaks every stack that referencesorgVars.WorkloadAlphaAccountId. The TypeScript typeLandingZoneOrganizationStructuremakes these renames compile-time errors, so you'll catch them before deploy. - Declared accounts move only through this file. To suspend or close an account declared here, move its entry under
SuspendedOUorCloseAccountOUand deploy the organization phase. Don't move a declared account in the AWS Organizations console: the next update to that account's resource, such as a tag change, moves it back to its configured parent. Accounts this file doesn't declare can be moved in the console. - Removing an account from the structure never closes or moves it. AWS Organizations does not allow programmatic account deletion, and the structure only manages membership. Once an account is closed, remove its entry.
- Budgets. AWS Budgets sends each new recipient address a verification email, and alerts start once it is confirmed; the status shows on the budget's page in the Budgets console. Changing a budget's recipients recreates the budget, and removing the
budgetfrom the structure deletes it. An OU budget covers the accounts the structure places under the OU; an account moved in outside the structure is not included. A root budget fails synthesis when the structure defines no account and no recipients are set.