You add an OU when you need a distinct governance boundary: a separate SCP posture, a clear cost allocation boundary, or a new team domain that shouldn't share an OU with existing workloads. This guide covers flat OUs and nested OUs, SCP attachment, and what to check for StackSet targeting after the change.
Steps
1. Add the OU in organization-structure.ts
Open src/config/organization-structure.ts and add a new key to root.organizationalUnits. The key becomes the stack output name and the TypeScript property name on orgVars. OU IDs are always published, so a new OU is immediately available as orgVars.<Key>Id. Keep it PascalCase and end it with OU by convention:
DataOU: {
name: 'data-ou',
accounts: {
DataLakeAccount: {
name: 'Data Lake',
email: 'aws+data-lake@example.com',
},
},
},
The name is the display name shown in the AWS Organizations console. You can leave accounts empty if you only need the OU structure now and plan to add accounts later.
To track the OU's spend, add an optional budget to the OU, for example budget: { monthlyLimit: 1000 }. It creates a monthly AWS Budgets cost budget in the management account for the combined cost of every account under the OU, nested OUs included, and emails alerts when actual or forecasted cost for the month exceeds the limit (AWS Budgets notifications). Set budgetAlertRecipients on the OU to choose who receives the alerts for every budget at or under it, or emailAddresses on the budget itself. See Organization Structure for the budget fields.
Nested OUs
AWS Organizations supports nested OUs, and so does the landing zone's OrganizationStructure type. Add a child OU under the parent's organizationalUnits key. A nested OU takes the same fields as a top-level one:
WorkloadsOU: {
name: 'workloads-ou',
organizationalUnits: {
FrontendOU: {
name: 'workloads-frontend-ou',
accounts: {
FrontendProdAccount: {
name: 'Frontend Production',
email: 'aws+frontend-prod@example.com',
},
},
},
BackendOU: {
name: 'workloads-backend-ou',
accounts: {},
},
},
},
Note that deep nesting adds complexity to SCP layering (every level in the chain contributes inherited restrictions that are harder to reason about) and makes StackSet targeting more involved. I'd keep nesting shallow, as two levels is almost always enough.
2. Attach SCPs to the new OU (optional)
If the new OU needs guardrails beyond those inherited from the root, import your SCP constants and add them to serviceControlPolicies on the OU definition. See Create a Custom SCP for authoring a new SCP, and Service Control Policies for the shipped guardrails.
DataOU: {
name: 'data-ou',
serviceControlPolicies: [denyS3BucketDeletionSCP],
accounts: {
DataLakeAccount: {
name: 'Data Lake',
email: 'aws+data-lake@example.com',
},
},
},
AWS Organizations enforces a hard limit of 10 directly attached SCPs per attachment point; SCPs inherited from the root or a parent OU do not count against this OU's own quota. With FullAWSAccess attached, the OU has room for at most 9 additional SCPs. Synthesis checks configured policies but does not count FullAWSAccess or other attachments outside your configuration; check the deployed attachments before adding policies. Group related guardrails into a single SCP rather than stacking many small ones.
3. Consider StackSet targeting
The member-account StackSets in LandingZoneMemberAccountsStack target OUs by ID at synth time. AccountSecurityStackSet and ResourceExplorerStackSet target orgVars.RootOUId, which covers every member account in the organization including accounts in your new OU. They include new accounts added to the new OU with no stack changes needed.
CdkBootstrapStackSet targets only the DevelopmentOUId and ProductionOUId, and ServiceQuotasStackSet targets the DevelopmentOUId, ProductionOUId, and SecurityOUId. To give accounts in your new OU the CDK toolkit resources or your quota requests, open src/stacks/landing-zone-member-accounts-stack.ts and add orgVars.DataOUId to that StackSet's organizationalUnits array. See Customize StackSets for the full targeting options.
4. Preview the change
pnpm run organization:diff
Expect to see a new AWS::Organizations::OrganizationalUnit resource and the corresponding DataOUId stack output in the LandingZoneOrganizationStack diff. If you added accounts, you'll also see AWS::Organizations::Account resources, and if you set a budget, an AWS::Budgets::Budget resource.
If you changed StackSet targets, switch to landing-zone-account credentials and run pnpm run landingzone:diff:all. The diff includes each StackSet's template, so it shows what changes in your member accounts.
5. Deploy
pnpm run organization:deploy
The organization stack creates the OU and publishes its ID as a stack output. If you changed StackSet targets, switch to landing-zone-account credentials and run pnpm run landingzone:deploy:all to apply those changes.
6. Verify
Confirm the OU exists in AWS Organizations:
aws organizations list-organizational-units-for-parent \
--parent-id $(aws organizations list-roots --query 'Roots[0].Id' --output text) \
--query 'OrganizationalUnits[?Name==`your-org-data-ou`]'
Confirm the OU ID stack output was published:
aws cloudformation describe-stacks --stack-name LandingZoneOrganizationStack \
--query 'Stacks[0].Outputs[?OutputKey==`DataOUId`].OutputValue'
What happens on deploy
LandingZoneOrganizationStack calls the AWS Organizations API to create the OU under the root (or under its parent OU for nested structures), creates any accounts defined inside it, and attaches the specified SCPs. Every new OU ID is published as a <Key>Id stack output.
If a member-account StackSet targets the new OU explicitly, CloudFormation updates the StackSet's organizational unit targets and automatically deploys stack instances into any accounts already in that OU. Accounts added to the OU after the StackSet is deployed receive their instances automatically through the service-managed targeting mechanism. That's the main reason to prefer OU-targeted StackSets over explicit account lists.
Renaming an OU key (e.g. DataOU to DataPlatformOU) changes the stack output name and breaks every reference to orgVars.DataOUId. The TypeScript type LandingZoneOrganizationStructure surfaces these as compile errors, so you'll catch them before deploy. Rename carefully and update all references in the stack files at the same time.