AWS account structure for a startup after Series A

Same story as the GCP version of this note, different cloud: startup closes Series A, and the AWS side is still one root user, one account, prod and the sandbox living next to each other. Cloud governance is the thing that breaks first once you leave MVP and start scaling, and “we’ll sort it later” always turns into “the security auditor is asking for it and we have three days”.

Here’s what I set up in the first weeks on a new AWS platform.

Multi-account structure

Everything under AWS Organizations, split by purpose: DEV, Stage, Prod, OPS, Networking. Prod never shares an account with a sandbox, full stop.

aws organizations create-account \
  --email [email protected] \
  --account-name "acme-prod"

aws organizations create-account \
  --email [email protected] \
  --account-name "acme-networking"

Group them into OUs so SCPs and Config rules can target “prod” or “dev” as a unit instead of per account:

aws organizations create-organizational-unit \
  --parent-id r-xxxx \
  --name Prod

Control Tower and Identity Center

Control Tower lays down the landing zone, Identity Center (formerly SSO) replaces IAM users scattered across every account with one login:

aws sso-admin create-permission-set \
  --instance-arn arn:aws:sso:::instance/ssoins-xxxxxxxx \
  --name DeveloperAccess \
  --session-duration PT8H

Attach a managed policy to the permission set and assign it per account/group, not per user:

aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn arn:aws:sso:::instance/ssoins-xxxxxxxx \
  --permission-set-arn arn:aws:sso:::permissionSet/ssoins-xxxxxxxx/ps-xxxxxxxx \
  --managed-policy-arn arn:aws:iam::aws:policy/PowerUserAccess

Least privilege from the first assignment, not retrofitted later.

Networking baseline

Transit Gateway as the hub, one VPC per account as the spokes:

aws ec2 create-transit-gateway \
  --description "acme-hub-tgw" \
  --options AmazonSideAsn=64512

aws ec2 create-transit-gateway-vpc-attachment \
  --transit-gateway-id tgw-xxxxxxxx \
  --vpc-id vpc-xxxxxxxx \
  --subnet-ids subnet-xxxxxxxx subnet-yyyyyyyy

One Client VPN endpoint, authenticated against Identity Center, so developers connect with the same SSO credentials instead of a shared PSK or a static key pair:

aws ec2 create-client-vpn-endpoint \
  --client-cidr-block 10.100.0.0/22 \
  --server-certificate-arn arn:aws:acm:eu-west-1:111111111111:certificate/xxxxxxxx \
  --authentication-options Type=federated-authentication,FederatedAuthentication={SAMLProviderArn=arn:aws:iam::111111111111:saml-provider/AcmeSSO} \
  --connection-log-options Enabled=true

Account-level guardrails

SCPs block the dangerous actions before anyone in prod gets the chance to run them:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail"
      ],
      "Resource": "*"
    }
  ]
}
aws organizations create-policy \
  --name deny-disable-cloudtrail \
  --type SERVICE_CONTROL_POLICY \
  --content file://deny-disable-cloudtrail.json

aws organizations attach-policy \
  --policy-id p-xxxxxxxx \
  --target-id ou-xxxx-xxxxxxxx

AWS Config catches what the SCP didn’t think to block:

aws configservice put-config-rule \
  --config-rule '{
    "ConfigRuleName": "s3-bucket-public-read-prohibited",
    "Source": {
      "Owner": "AWS",
      "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED"
    }
  }'

Budget alerting

Forecast-based alerts at 80% and 100%, per account, before the bill lands rather than after:

aws budgets create-budget \
  --account-id 111111111111 \
  --budget '{
    "BudgetName": "acme-prod-monthly",
    "BudgetLimit": {"Amount": "5000", "Unit": "USD"},
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST"
  }' \
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "FORECASTED",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80
      },
      "Subscribers": [{"SubscriptionType": "EMAIL", "Address": "[email protected]"}]
    }
  ]'

No one enjoys explaining a $40k surprise line item at the end of the month.

Terraform comes after all of this exists, not before — same as on the GCP side, you’re encoding a structure you already decided on, not discovering it as you write .tf files.