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.