AWS CDK Landing Zone

Organization Structure

Define OUs, accounts, and SCP attachments for your AWS Organization in organization-structure.ts.

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 OU and account IDs to SSM Parameter Store under /organization/, making them available to StackSet targets in later phases.

Built-in OUs

The starter ships with five pre-configured OUs that match common AWS multi-account patterns:

OU keyDefault name patternPurpose
LogArchiveOU<org>-log-ouHolds the log archive account that receives centralized CloudTrail logs
SecurityOU<org>-security-ouHolds the security account used as the GuardDuty and Security Hub delegated admin
DevelopmentOU<org>-workload-dev-ouWorkload accounts for non-production environments, including the sandbox
ProductionOU<org>-workload-prod-ouWorkload accounts for production environments
SuspendedOU<org>-suspended-ouReceives the lockdownSuspendedAccountsSCP to deny all actions in suspended accounts

You can rename, add, or remove OUs by editing the organizationalUnits map. The OU key (e.g. LogArchiveOU) becomes the SSM parameter suffix (/organization/LogArchiveOUId) and the TypeScript property name on orgVars; changing a key is a compile-time-checked rename.

Options

The OrganizationStructure type has a single required top-level key:

OptionTypeRequiredDescription
rootOrganizationRootStructureYesThe organization root node. Holds SCPs and the OU map.
root.serviceControlPoliciesServiceControlPolicy[]NoSCPs attached at the organization root (apply to every account). Max 10 per target.
root.organizationalUnitsRecord<string, OrganizationalUnitStructure>NoMap of OU key → OU definition.

Each OrganizationalUnitStructure entry:

OptionTypeRequiredDescription
namestringYesDisplay name of the OU in AWS Organizations.
serviceControlPoliciesServiceControlPolicy[]NoSCPs attached to this OU. Max 10 per target including inherited ones.
accountsRecord<string, AccountStructure>NoMap of account key → account definition.

Each AccountStructure entry:

OptionTypeRequiredDescription
namestringYesDisplay name of the account in AWS Organizations.
emailstringYesRoot email address for the account. Must be globally unique across all of AWS.
serviceControlPoliciesServiceControlPolicy[]NoSCPs attached to this specific account. Max 10 per target including inherited ones.

Example

const orgName = landingZoneSettings.organizationName.toLowerCase();
const mailDomain = landingZoneSettings.mailDomain.toLowerCase();

export const organizationStructure = {
  root: {
    serviceControlPolicies: [
      criticalSecurityGuardrailsSCP,
      protectTaggedCloudFormationStacksSCP,
      denyAllOutsidePrimaryAndSecondaryRegionsSCP,
    ],
    organizationalUnits: {
      LogArchiveOU: {
        name: `${orgName}-log-ou`,
        accounts: {
          LogArchiveAccount: {
            name: `${orgName}-log-acct`,
            email: `aws+log@${mailDomain}`,
          },
        },
      },
      SecurityOU: {
        name: `${orgName}-security-ou`,
        accounts: {
          SecurityAccount: {
            serviceControlPolicies: [protectSecurityHubConfigurationSCP],
            name: `${orgName}-security-acct`,
            email: `aws+security@${mailDomain}`,
          },
        },
      },
      DevelopmentOU: {
        name: `${orgName}-workload-dev-ou`,
        accounts: {
          SandboxAccount: {
            name: 'Sandbox',
            email: `sandbox@${mailDomain}`,
          },
        },
      },
      ProductionOU: {
        name: `${orgName}-workload-prod-ou`,
        accounts: {
          WorkloadAlphaAccount: {
            name: 'Workload Alpha',
            email: `aws+workload-alpha@${mailDomain}`,
          },
          WorkloadBetaAccount: {
            name: 'Workload Beta',
            email: `aws+workload-beta@${mailDomain}`,
          },
        },
      },
      SuspendedOU: {
        name: `${orgName}-suspended-ou`,
        serviceControlPolicies: [lockdownSuspendedAccountsSCP],
      },
    },
  },
} satisfies OrganizationStructure;

export type LandingZoneOrganizationStructure = typeof organizationStructure;

How it's used

The OrganizationConstruct in Phase 1 (OrganizationStack) reads this object and:

  1. Creates the AWS Organization if it does not exist
  2. Creates OUs and accounts as specified
  3. Attaches SCPs at root, OU, and account levels
  4. Publishes every OU ID and account ID to SSM under /organization/<Key>Id

Later stacks read those SSM parameters at synth time via createOrganizationVariables(). The cdk.context.json file caches the lookups. Run pnpm run management:synth after Phase 1 to populate it before deploying Phase 2 and 3.

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 per attachment point (root, OU, or account), counting inherited ones (raised from 5 to 10 in May 2026). Plan SCP layering accordingly.
  • Renaming an account or OU key (e.g. WorkloadAlphaAccountAppAlphaAccount) changes the SSM parameter name and breaks every stack that references orgVars.WorkloadAlphaAccountId. The TypeScript type LandingZoneOrganizationStructure makes 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. Move unwanted accounts to SuspendedOU instead.