Authenticating...
Skip to main content

Creating a New AWS Account with CDK

Overview​

This runbook covers provisioning a new AWS account in the org — a workload environment, a shared infrastructure account, or a personal sandbox — and wiring it into SSO so people can actually sign in. Accounts are created exclusively through CDK pull requests, never through the AWS Console (Control Tower Account Factory / Service Catalog). Creating one by hand causes drift against our IaC and has caused root-email collisions in the past.

If you just want a personal sandbox, see Setting up a Sandbox AWS Account for the abbreviated, convention-locked version of the same two steps below. This runbook is the general procedure both build on.

When to use this​

  • You need a brand-new AWS account under the org — a new product/environment workload, a shared infra account, or a sandbox — provisioned and wired into SSO.
  • Not for changing access on an account that already exists — see AWS Identity Center — Groups, Users & Permission Sets.
  • Not for in-account IAM (roles/users inside an account's own IAM) — this is about AWS Organizations account objects and Identity Center, one layer up.

Repo map​

RepoOwnsDeploys to
AdAction/aws-organization-managementOrg/OU structure, the account objects themselves, the StackSet that auto-deploys OrgAuditReadOnlyRoleManagement account 172122050326, us-east-2
AdAction/aws-ssoImporting the account into SSO, Group → PermissionSet → Account assignmentsManagement account 172122050326, us-east-2
Deploy order matters

aws-organization-management exports the new account's ID via CloudFormation (adaction-<account-name>-AccountId); aws-sso imports it with Fn.importValue. Step 1 must merge, release, and deploy before Step 2 can reference it — the export has to exist first, or the aws-sso deploy fails outright.

Prerequisites​

  • Push access to AdAction/aws-organization-management and AdAction/aws-sso, and a GitHub PAT with read:packages scope configured in ~/.npmrc — a plain npm install 404s on the @adaction/* scoped packages without it.
  • Node/npm matching each repo's .nvmrc / engines.
  • An AWS CLI profile for the management account (172122050326, profile adgem locally) if you want to run cdk diff/cdk bootstrap — actual stack deploys happen only through CI.
  • At least one approving PR reviewer (generic branch-protection rule; neither repo has a stricter CODEOWNERS gate for this today).

Conventions​

Naming​

  • Personal sandboxes: sandbox-<username>-<n>, e.g. sandbox-bgiese-1. This format is load-bearing — isSandboxAccount() in aws-sso pattern-matches on the sandbox- prefix to route the account into SandboxAssignmentStack instead of the general one.
  • Everything else: a descriptive product/purpose name, usually with an environment suffix — ad-suite-production, data-engineering-prod, servicehub-stage-env, offerwall-ui-prod. There's no single enforced pattern beyond "descriptive and environment-qualified" — check buildNewAccounts() in accounts-stack.ts and IMPORTED_ACCOUNTS in org-inventory.ts for existing names before picking one.

Root email​

aws+<account-name>@adaction.com — always the shared aws@ alias with plus-addressing, never a personal email. AWS account emails are globally unique and are the root user's recovery channel; keeping them all on the aws+ alias means accounts-stack.ts doubles as a partial inventory of addresses already used. It isn't a complete record, though — closed accounts and accounts outside our org won't show up there.

Never put cost-allocation tags directly on the account object

accounts-stack.ts deliberately strips Teams, Service, Product, Environment, and Owners off every CfnAccount (stripCostAllocationTags()). Tags on an Organizations account object are inherited by every CUR line item billed to that account — an account tagged Teams=platform silently double-attributes (or mis-attributes) all spend in it, including line items that already carry their own correct resource-level tag. See PLA-165 and PLA-399. If you need those dimensions for Datadog Cloud Cost Management, tag the resources inside the account, not the account object itself.

OU hierarchy​

Defined in organizational-structure-stack.ts. Every new account needs an OU assignment; don't invent a new top-level OU without a CDK change there first.

OU pathUsed for
SecuritySecurity tooling accounts
Workloads/ProdProduction workload accounts (ad-suite-production, adaction-docs, adgem-docs)
Workloads/SdlcNon-prod workload environments
Infrastructure/ProdShared production infra (cognito-production, mcp-server, data-archive)
Infrastructure/SDLCShared non-prod infra
SuspendedDecommissioned/closed-out accounts
DeploymentsDeployment-pipeline accounts
SandboxPersonal sandbox accounts
CredentialsCredential-related accounts

Step 1 — create the account (aws-organization-management)​

  1. Add the account to buildNewAccounts() in src/stacks/accounts-stack.ts:

    {
    name: 'my-new-account',
    email: 'aws+my-new-account@adaction.com',
    },
  2. Add the OU assignment to ACCOUNT_OU_ASSIGNMENTS in src/stacks/accounts-ou-assignment-stack.ts:

    { accountName: 'my-new-account', ouPath: 'Workloads/Prod' },
Account already exists in AWS?

If the account was created outside CDK (or predates it), it goes in IMPORTED_ACCOUNTS in src/data/org-inventory.ts instead of buildNewAccounts() — CloudFormation fails with AlreadyExists otherwise. org-inventory.ts entries are imported with a RETAIN deletion policy and referenced positionally; append only, never reorder.

  1. Nothing else to do for baseline auditability: once the account lands in any OU, a service-managed CloudFormation StackSet (stacksets-stack.ts) auto-deploys the OrgAuditReadOnlyRole to it — that's org-wide and automatic, not something you configure per account.

  2. Verify, PR, deploy:

    npm run build && npm test && npx cdk diff

    Confirm the diff shows a Create for the new CfnAccount. Open a PR with a Conventional Commits title — feat: or fix: (semantic-release only deploys on those, not chore:). On merge, deploy-iac.yml deploys via AdAction/shared-workflows and publishes the adaction-my-new-account-AccountId export that Step 2 depends on.

Step 2 — wire it into SSO (aws-sso)​

  1. Add one line to importAccounts() in src/import-utils.ts — only after Step 1 has deployed:

    this.importAccount({ name: "my-new-account" }),
  2. Accounts named sandbox-* route automatically into SandboxAssignmentStack via isSandboxAccount(). Anything else that needs sandbox-style treatment (rare — a few legacy exceptions are hardcoded in that function) needs an explicit entry there.

  3. The platform group gets every permission set on every imported account automatically — no action needed for that. If a specific team's group needs access to this new account, add it to that team's assign<TeamName>Permissions() method in updated-assignment-stack.ts. Full pattern and gotchas: AWS Identity Center — Groups, Users & Permission Sets.

Per-account managed-policy ceiling

Every group→permission-set→account assignment provisions a separate IAM role (AWSReservedSSO_<PermissionSet>_<id>) in the target account, and the permission-set catalog is capped at 10 managed policies each (repo guardrail, enforced by a Jest test — see the Identity Center runbook for the full quota picture). A brand-new account starts clean, but sandbox accounts fan out to every permission set via the platform group's automatic grant and accumulate roles the fastest of any account type.

  1. Verify, PR, deploy:

    npm run build && npm test && npx cdk diff

    PR with a feat:/fix: title, ≥1 approval, merge after Step 1's release has deployed. semantic-release cuts a release, which triggers deploy.yml (cdk deploy --all --require-approval never).

Step 3 — bootstrap the account for CDK use​

Only needed if this account will run its own CDK stacks (most workload/infra accounts will; a pure sandbox you're just poking around in might not).

  1. Log in to AWS SSO with the AWS CLI:

    aws configure sso --profile my-new-account
    SSO start URL [None]: https://adgem.awsapps.com/start
    SSO Region [None]: us-east-2

    A browser opens for the authorization prompt — click Allow. (On AWS CLI versions before 2.22 you'll instead see a device code to enter manually; force that behavior with --use-device-code if needed.)

  2. Bootstrap the account, using the account ID from Step 1's export:

    cdk bootstrap aws://<account-id>/us-east-2 --profile my-new-account

Accessing the account​

Once both stacks have deployed, the account shows up in the AWS access portal for anyone whose group carries a permission set on it (automatically platform; explicitly-wired teams per Step 2).

Troubleshooting​

  • "An AWS account with that email already exists" — the aws+<name>@ email is taken, possibly by an account outside our org or a closed account (AWS reserves closed-account emails for ~90 days). Pick a different name/suffix; accounts-stack.ts and org-inventory.ts list names we've used but can't show closed or foreign-org accounts.
  • aws-sso deploy fails on a missing export — Step 1 hasn't deployed yet. Wait for the aws-organization-management release to finish, then re-run the failed aws-sso deploy.
  • A team can see the account in the portal but with the wrong (or no) access — the platform group's automatic grant doesn't extend to other groups; someone needs to add the account to that team's method in updated-assignment-stack.ts (Step 2, part 3).
  • cdk deploy/cdk bootstrap fails for a stack in the new account — confirm Step 3 actually ran; a fresh account has no CDK bootstrap stack until someone bootstraps it.