AWS Account Vending in Under 30 Minutes with AFT
Vending a new AWS account used to mean filing a ticket, waiting for someone to click through the console, manually configuring IAM roles, and hoping they remembered to enable CloudTrail. Now it’s a Terraform file and a git push.
Here’s how I set up Account Factory for Terraform (AFT) to vend fully configured AWS accounts in under 30 minutes — with guardrails, CI roles, and state buckets included.
What You Get
When I push a new account request, this happens automatically:
- AFT CodePipeline picks up the change
- Control Tower creates the account with all guardrails
- Global customizations deploy (IAM roles, security baselines)
- Per-OU customizations apply (environment-specific config)
- State bucket gets bootstrapped for future Terraform deployments
30 minutes later, the account is ready to receive infrastructure.
The Account Request
Each account is a single Terraform file:
module "portfolio_prod_0" {
source = "./modules/aft-account-request"
control_tower_parameters = {
AccountEmail = "you+portfolio-prod-0@example.com"
AccountName = "portfolio-prod-0"
ManagedOrganizationalUnit = "Production (ou-xxxx-xxxxxxxx)"
SSOUserEmail = "you@example.com"
SSOUserFirstName = "Your"
SSOUserLastName = "Name"
}
account_tags = {
Environment = "workloads-prod"
Owner = "you@example.com"
}
account_customizations_name = "workloads-prod"
}
Commit, push, done. The pipeline handles everything else.
Global Customizations
Every account gets a baseline via aft-global-customizations:
# Block all S3 public access
resource "aws_s3_account_public_access_block" "this" {
block_public_acls = true
ignore_public_acls = true
block_public_policy = true
restrict_public_buckets = true
}
# IAM password policy (for IAM users, not SSO — SSO is managed by
# Identity Center and has its own auth policies)
resource "aws_iam_account_password_policy" "this" {
minimum_password_length = 24
require_symbols = true
require_numbers = true
require_uppercase_characters = true
require_lowercase_characters = true
allow_users_to_change_password = true
password_reuse_prevention = 12
}
# GitHubCIRole for CI/CD deployments
resource "aws_iam_role" "github_ci" {
name = "GitHubCIRole"
assume_role_policy = data.aws_iam_policy_document.trust.json
}
This means every account is born secure and CI-ready. No manual setup.
Note: I intentionally omit max_password_age — NIST 800-63B recommends against mandatory password rotation, and AWS follows this guidance for SSO-managed accounts. Forced rotation leads to weaker passwords, not stronger ones.
The Naming Convention
I use {purpose}-{env}-{number}:
portfolio-prod-0— first production account for the portfolio projectportfolio-dev-0— first development accountlineup-prod-0— production for a different project
The number suffix allows multiple accounts per purpose without naming collisions. If you need to replace an account, increment the number.
OU Structure
Root
├── Core — Security, logging
├── Infrastructure — Shared tooling (billing dashboards, monitoring)
├── Workloads
│ ├── Production
│ └── Development
└── Sandbox — Short-lived experiments
SCPs are attached at the OU level:
- All OUs: Deny leaving the org, deny root API access, deny disabling security services
- All non-Core: Deny regions outside us-east-1 and us-west-2
- Sandbox: Limit EC2 to small instance types
Gotchas I Hit
Customization pipelines don’t auto-trigger. Pushing to the aft-global-customizations repo doesn’t automatically run the pipeline. You need to manually start it via aws codepipeline start-pipeline-execution. This tripped me up — the account was vended and active, but the CI role was missing because customizations hadn’t run. I spent an hour debugging OIDC assume failures before realizing the role simply didn’t exist yet.
The pipeline lives in the AFT account, not management. The CodePipeline for account requests is in your AFT management account, not the org management account. You need to assume into the AFT account to trigger or monitor it. First time I tried to find the pipeline, I was looking in the wrong account.
State bucket bootstrap is separate. AFT creates the account but doesn’t create the Terraform state bucket. You need a separate bootstrap workflow that creates the S3 bucket + DynamoDB lock table. I have a reusable GitHub Actions workflow for this — trigger it with the new account ID and it chains into the account to create the state infrastructure.
STS session limits with role chaining. When Terraform operations take longer than an hour (ACM certificate validation waiting on DNS propagation), your assumed role session expires mid-apply. I hit this deploying CloudFront — the ACM cert took 40 minutes to validate, and the session expired before the Route 53 records could be written. Solution: split long-running resources into separate applies, or use aws_acm_certificate_validation with a higher timeout and fresh credentials.
Account Decommissioning
Vending is half the lifecycle. When an account is no longer needed:
- Remove all deployed infrastructure (
terraform destroy) - Remove the account request
.tffile and push - Close the account via AWS Organizations (AFT doesn’t handle closure)
AWS retains closed accounts for 90 days before permanent deletion. During that window, the account name is reserved — hence the number suffix in the naming convention.
The Workflow
For a new project:
- Create account request
.tffiles (one per environment) - Push to
aft-account-request→ pipeline vends accounts - Run bootstrap workflow → state buckets created
- Deploy project infrastructure via CI/CD
From “I need a new account” to “infrastructure is deploying” in about 30 minutes. No tickets, no console clicking, no missed configurations.
Why This Matters
Without AFT, every new account is a liability. Someone forgets to enable CloudTrail. Someone leaves S3 public access on. Someone creates an IAM user with static credentials.
With AFT, every account is born with the same baseline. Security is the default, not an afterthought. And when you need to update that baseline — say, adding a new security control — you update one file and it propagates to every account.
That’s the difference between infrastructure that scales and infrastructure that breaks.