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 pre-configured OUs that match common AWS multi-account patterns:
| OU key | Default name | Purpose |
|---|---|---|
LogArchiveOU | log-ou | Holds the log archive account that receives the centralized organization CloudTrail logs |
SecurityOU | security-ou | Holds the security account, the delegated administrator for CloudTrail, GuardDuty, Security Hub, Inspector, and Macie |
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 |
SuspendedOU | suspended-ou | Receives the lockdownSuspendedAccountsSCP and nests the CloseAccountOU |
SuspendedOU nests a CloseAccountOU (close-account-ou). The two model different offboarding states, covered in Suspended vs. close-account OU below.
You can rename, add, or remove OUs by editing the organizationalUnits map. The OU key (e.g. LogArchiveOU) becomes the stack output name (LogArchiveOUId) and the TypeScript property name on orgVars; changing a key is a compile-time-checked rename.
The SecurityAccount, LandingZoneAccount, and LogArchiveAccount 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 and the OU map. |
root.serviceControlPolicies | ServiceControlPolicy[] | No | SCPs attached at the organization root (apply to every account). Max 10 per target. |
root.organizationalUnits | Record<string, OrganizationalUnitStructure> | No | Map of OU key → OU definition. |
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 10 directly attached per target. |
accounts | Record<string, AccountStructure> | No | Map of account key → account definition. |
organizationalUnits | Record<string, OrganizationalUnitStructure> | No | Nested child OUs. The structure is recursive, so OUs can nest to any depth. |
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 10 directly attached per target. |
Example
const mailDomain = landingZoneSettings.mailDomain.toLowerCase();
export const organizationStructure = {
root: {
serviceControlPolicies: [
criticalSecurityGuardrailsSCP,
protectTaggedCloudFormationStacksSCP,
denyAllOutsidePrimaryAndSecondaryRegionsSCP,
],
organizationalUnits: {
LogArchiveOU: {
name: 'log-ou',
accounts: {
LogArchiveAccount: {
name: 'logarchive-account',
email: `aws+log@${mailDomain}`,
publishAccountIdOutput: true,
},
},
},
SecurityOU: {
name: 'security-ou',
accounts: {
SecurityAccount: {
serviceControlPolicies: [protectSecurityHubConfigurationSCP],
name: 'security-account',
email: `aws+security@${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',
accounts: {
WorkloadAlphaAccount: {
name: 'Workload Alpha',
email: `aws+workload-alpha@${mailDomain}`,
},
WorkloadBetaAccount: {
name: 'Workload Beta',
// Account emails don't have to use the organization mail domain;
// any unique, valid address works.
email: 'team-beta+aws@example.org',
},
},
},
SuspendedOU: {
name: 'suspended-ou',
serviceControlPolicies: [lockdownSuspendedAccountsSCP],
organizationalUnits: {
// Moving an account here triggers the close-account automation.
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 (LogArchiveOU, SecurityAccount) are what become the typed orgVars.<Key>Id outputs.
Suspended vs. close-account OU
The structure distinguishes two offboarding states, and the CloseAccountOU nests inside SuspendedOU on purpose.
SuspendedOUisolates an account while keeping it. ThelockdownSuspendedAccountsSCPdenies every action except an allowlist of roles, and the OU is excluded from Security Hub CSPM, GuardDuty, and Macie member management. Use it to quarantine an account without deleting anything.CloseAccountOUpermanently closes an account. When this OU exists in the structure, its ID drives a scheduled reconcile in the organization phase (LandingZoneOrganizationStack, in the management account) that runs every 8 hours, lists the accounts in the OU, and closes any that are still active. Because it is a child ofSuspendedOU, it inherits the lockdown SCP and the security-service exclusion, so an account waiting to close (or one whose closure fails against the Organizations quota of 10% of members per rolling 30 days) stays locked down and retries on the next run.
Closed accounts remain visible in the OU with SUSPENDED status for the 90-day post-closure window. 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
- 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. Because the references resolve during deployment rather than at synth, there is nothing to cache first. LandingZoneOrganizationStack just has to be deployed before the stacks that consume its outputs.
Things to know
- 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), raised from 5 to 10 in May 2026. Inherited SCPs do not count against a child's own quota.
OrganizationConstructvalidates the structure at synth time: it fails when a node exceeds 10 attached SCPs or a policy document exceeds 10,240 characters, and warns once you reach 8 attachments or roughly 90% of the document size, so quota problems surface before a CloudFormation rollback mid-deploy. - 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 log archive and security accounts. A StackSet that targets a whole OU never needs one. This keeps large organizations under the CloudFormation per-stack output limit; synthesis warns as you approach it and fails before it is exceeded. - 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. - Removing an account from the structure does not close it. AWS Organizations does not allow programmatic account deletion, and the structure only manages membership. To decommission an account, move it to
SuspendedOUto quarantine it, or toCloseAccountOUto have the close-account automation shut it down permanently.