What is AWS Control Tower?
Service that sets up and governs a secure, multi-account AWS environment based on best practices. Automates the creation of a landing zone.
What It Sets Up
- AWS Organizations (account structure)
- Identity Center (SSO)
- Guardrails (preventive + detective + proactive controls)
- Log Archive account (centralized logging)
- Audit account (security/compliance)
- Account Factory (provision new accounts)
┌─────────────────────────────────────────────────────────────────┐
│ Management Account │
│ └── Organizations │
│ ├── Security OU │
│ │ ├── Log Archive Account │
│ │ └── Audit Account │
│ └── Workloads OU │
│ ├── Dev Account │
│ └── Prod Account │
└─────────────────────────────────────────────────────────────────┘
Landing Zone
A well-architected, multi-account AWS environment. It’s the architecture pattern, not a service.
Components
- Multi-account structure (Organizations + OUs)
- Identity management (IAM Identity Center)
- Centralized logging (CloudTrail, Config logs)
- Security baseline (guardrails, SCPs)
- Network architecture (shared VPCs, Transit Gateway)
- Governance (tagging, cost allocation)
Control Tower vs Landing Zone
| Control Tower | Landing Zone | |
|---|---|---|
| What | AWS service | Architecture pattern |
| Relationship | Creates and manages | The thing being created |
One Control Tower = One Landing Zone per AWS Organization.
- Cannot have multiple landing zones in same Organization
- Cannot run Control Tower multiple times in same Organization
If You Need Separate Environments
Option 1: Multiple OUs within one landing zone
One Landing Zone
├── Production OU (strict guardrails)
├── Development OU (relaxed guardrails)
└── Sandbox OU (minimal guardrails)
Option 2: Separate AWS Organizations
┌─────────────────────┐ ┌─────────────────────┐
│ Organization A │ │ Organization B │
│ Control Tower A │ │ Control Tower B │
│ Landing Zone A │ │ Landing Zone B │
└─────────────────────┘ └─────────────────────┘
You can also build a landing zone manually or with other tools (Terraform, CDK), but Control Tower is AWS’s managed solution.
Control Tower Controls (3 Types)
┌─────────────────────────────────────────────────────────────────┐
│ Preventive Controls │
│ Implementation: SCPs │
│ When: Blocks API calls before they happen │
├─────────────────────────────────────────────────────────────────┤
│ Detective Controls │
│ Implementation: AWS Config Rules │
│ When: Detects non-compliance after resource exists │
├─────────────────────────────────────────────────────────────────┤
│ Proactive Controls │
│ Implementation: CloudFormation Hooks │
│ When: Validates during CFN deployment (before creation) │
└─────────────────────────────────────────────────────────────────┘
Proactive controls only work for resources deployed via CloudFormation (not CLI/Console direct API calls).
Hooks vs Other Controls
| CloudFormation Hooks | SCPs | Config Rules | |
|---|---|---|---|
| When | During deployment | At API call | After creation |
| Action | Block/allow deployment | Block API | Detect + remediate |
| Scope | CloudFormation only | All API calls | All resources |
Audit Account vs Log Archive Account
| Account | Purpose |
|---|---|
| Audit | Security team works here, investigates, reviews |
| Log Archive | Stores logs (CloudTrail, Config), immutable storage |
Audit Account Capabilities
- SNS notifications for Config rule violations
- Cross-account IAM roles for security auditing
- Access to Security Hub (aggregated findings)
- Access to GuardDuty (delegated admin)
- CloudTrail organization trail access
- Read-only access to member accounts (cannot modify workload resources)
CloudFormation Hooks
Custom code that runs before/after CloudFormation provisions a resource. Validates or modifies resources during deployment.
CloudFormation: "I'm about to create S3 bucket"
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hook (pre-create) │
│ │
│ "Is encryption enabled?" │
│ Yes → ALLOW (continue) │
│ No → FAIL (block deployment) │
└─────────────────────────────────────────────────────────────────┘
│
▼
CloudFormation creates S3 bucket (if allowed)
Hook Types
| Type | When it runs |
|---|---|
| Pre-provision | Before resource is created/updated/deleted |
| Post-provision | After resource is created/updated/deleted |
AWS-Managed vs Custom Hooks
| Type | Description |
|---|---|
| AWS-managed | Pre-built by AWS (encryption, public access checks) |
| Custom | You build for your specific rules |
Register Custom Hook
# 1. Create hook project
cfn init # Choose "Hook", select language (Python/Java/Go)
# Project structure:
# my-hook/
# ├── src/
# │ └── handlers.py ← Your validation code
# ├── hook-role.yaml ← IAM role for hook
# └── my-hook.json ← Hook schema
# 2. Build and submit to CloudFormation registry
cfn submit --dry-run # Validate
cfn submit # Register
# 3. Activate hook
aws cloudformation set-type-configuration \
--type HOOK \
--type-name MyOrg::S3::EncryptionCheck \
--configuration '{
"CloudFormationConfiguration": {
"HookConfiguration": {
"TargetStacks": "ALL",
"FailureMode": "FAIL"
}
}
}'
Hook Configuration Options
| Setting | Options | Meaning |
|---|---|---|
| TargetStacks | ALL | Validates all stacks |
NONE | Hook disabled | |
| FailureMode | FAIL | Block deployment |
WARN | Log warning, allow deployment |
Hook Scope
Regional: Hooks must be activated in each region separately.
us-east-1: Hook activated ✓ → Validates stacks
us-west-2: Hook not activated → No validation
eu-west-1: Hook activated ✓ → Validates stacks
All stacks: Once activated, validates ALL CloudFormation stacks in that account/region. Individual stacks cannot bypass hooks.
Control Tower proactive controls: When enabled on an OU, automatically activates across all governed regions.
Stack-Level Filter
You can exclude specific stacks in hook schema:
{
"handlers": {
"preCreate": {
"targetNames": ["AWS::S3::Bucket"],
"targetFilters": {
"exclude": {
"stackNames": ["LegacyStack-*"]
}
}
}
}
}
Simple Hook Handler Example (Python)
def handler(event, context):
# Get resource properties CFN is about to create
resource_properties = event['hookContext']['targetModel']['resourceProperties']
# Your validation logic
if 'BucketEncryption' not in resource_properties:
return {
'status': 'FAILED',
'message': 'S3 bucket must have encryption enabled'
}
return {'status': 'SUCCESS'}
Account Factory Customization (AFC)
Built-in Control Tower feature that lets you customize new accounts during provisioning using Service Catalog blueprints.
Account Factory vs CfCT
| Account Factory Customization | CfCT | |
|---|---|---|
| Built into Control Tower | Yes | No (separate solution to deploy) |
| When it runs | During account creation | After account creation |
| Configuration | Service Catalog blueprints | manifest.yaml + templates |
| Use case | Account baseline at birth | Ongoing customizations |
How AFC Works
┌─────────────────────────────────────────────────────────────────┐
│ Control Tower Account Factory │
│ │
│ 1. Admin creates blueprint (CFN template) in Service Catalog │
│ 2. Admin enables blueprint in Account Factory │
│ 3. User provisions new account │
│ 4. Account Factory deploys blueprint to new account │
└─────────────────────────────────────────────────────────────────┘
Timeline:
Account creation starts
│
├── Control Tower baseline (guardrails, IAM Identity Center)
│
├── AFC blueprints deploy (your custom resources)
│
▼
Account ready with customizations
Service Catalog Concepts
| Concept | What it is |
|---|---|
| Product | A CloudFormation template wrapped for self-service |
| Portfolio | Collection of products, with access controls |
| Blueprint | Product specifically for Account Factory customization |
| Provisioned Product | Instance of a product deployed to an account |
Setting Up AFC
Step 1: Create blueprint product in Service Catalog (Hub account)
Hub account = Management account or delegated admin account
Service Catalog → Products → Create product
- Product name: "Security Baseline"
- Template: your CloudFormation template
- Version: "v1"
Step 2: Create portfolio and add product
Service Catalog → Portfolios → Create portfolio
- Name: "Account Factory Blueprints"
- Add product: "Security Baseline"
Step 3: Enable in Account Factory
Control Tower → Account Factory → Blueprints
- Select portfolio
- Enable blueprints
Step 4: Provision account with blueprint
Control Tower → Account Factory → Create account
- Account details (name, email, OU)
- Select blueprints to apply
- Create
Blueprint Template Example
AWSTemplateFormatVersion: '2010-09-09'
Description: Security baseline for new accounts
Parameters:
AccountEmail:
Type: String
Description: Account email (passed by Account Factory)
Resources:
SecurityAlertTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: security-alerts
CloudTrailBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'cloudtrail-${AWS::AccountId}'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
ConfigRecorder:
Type: AWS::Config::ConfigurationRecorder
Properties:
RoleARN: !GetAtt ConfigRole.Arn
RecordingGroup:
AllSupported: true
AFC vs Manual Service Catalog
| Aspect | AFC (via Account Factory) | Manual Service Catalog |
|---|---|---|
| Trigger | Automatic during account creation | Manual launch by user |
| Target | New accounts only | Any account |
| Integration | Built into Control Tower workflow | Standalone |
| Parameters | Account Factory passes account info | User provides all params |
Multiple Blueprints
You can apply multiple blueprints to one account:
New Account
│
├── Blueprint: Network Baseline (VPC, subnets)
├── Blueprint: Security Baseline (Config, GuardDuty)
└── Blueprint: Logging Baseline (CloudWatch, S3)
Each blueprint = separate StackSet instance in the account.
Blueprint Parameters
Account Factory automatically passes these parameters to blueprints:
| Parameter | Value |
|---|---|
AccountId | New account’s ID |
AccountEmail | New account’s email |
AccountName | New account’s name |
SSOUserEmail | SSO user’s email |
SSOUserFirstName | SSO user’s first name |
SSOUserLastName | SSO user’s last name |
Your template can use these:
Parameters:
AccountId:
Type: String
AccountEmail:
Type: String
Resources:
AlertTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Protocol: email
Endpoint: !Ref AccountEmail
When to Use AFC vs CfCT
| Scenario | Use |
|---|---|
| Resources needed at account birth | AFC |
| Resources that change over time | CfCT |
| Simple baseline (VPC, IAM roles) | AFC |
| Complex customizations with dependencies | CfCT |
| Want Control Tower native solution | AFC |
| Need SCPs per OU | CfCT |
You can use both: AFC for initial setup, CfCT for ongoing management.
Customizations for AWS Control Tower (CfCT)
AWS solution that deploys custom CloudFormation templates and SCPs to accounts managed by Control Tower. Automates customizations when new accounts are created.
Problem It Solves
Without CfCT:
- Control Tower creates new account
- You manually deploy custom resources
- Repeat for every new account
With CfCT:
- Control Tower creates new account
- CfCT automatically deploys your customizations
CfCT Is Not Built Into Control Tower
CfCT is a separate AWS Solution you deploy yourself via CloudFormation template from AWS Solutions page.
CfCT Architecture
┌─────────────────────────────────────────────────────────────────┐
│ S3/CodeCommit ──► CodePipeline ──► CodeBuild ──► Lambda ──► StackSets
│ (source) (orchestrate) (validate) (deploy)
└─────────────────────────────────────────────────────────────────┘
manifest.yaml Example
region: us-east-1
version: 2021-03-15
resources:
# Deploy CloudFormation template
- name: SecurityBaseline
resource_file: templates/security-baseline.yaml
deployment_targets:
organizational_units:
- Workloads
parameters:
- parameter_key: Environment
parameter_value: production
# Apply SCP
- name: DenyS3Public
resource_file: policies/deny-s3-public.json
deployment_targets:
organizational_units:
- Workloads
deploy_method: scp
CfCT Components
| Component | Role |
|---|---|
| S3/CodeCommit | Store manifest + templates + policies |
| CodePipeline | Orchestrate stages in order |
| CodeBuild | Validate syntax, lint templates, parse manifest |
| SCP Lambda | Create/attach SCPs via Organizations API |
| StackSet Lambda | Create/deploy StackSets via CloudFormation API |
| Lifecycle Lambda | Auto-apply customizations to new accounts |
| EventBridge Rule | Triggers Lifecycle Lambda on Control Tower events |
CfCT Component Details
CodePipeline
Orchestrates the entire workflow. Defines stages and order of execution.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Source │───►│ Build │───►│ Deploy │───►│ Deploy │
│ │ │ │ │ (SCPs) │ │(StackSets)│
│ S3 or │ │ CodeBuild│ │ Lambda │ │ Lambda │
│ CodeCommit │ validates│ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
If any stage fails → pipeline stops → you get notified
CodeBuild
Validates and prepares your customizations.
- Parse manifest.yaml (validate YAML syntax, check required fields)
- Validate CloudFormation templates (cfn-lint, verify files exist)
- Validate SCP JSON (syntax, policy structure)
- Create structured output for Lambda stages
CodeBuild doesn’t compile anything—CFN templates and JSON don’t need compilation. “Package” means organizing and structuring data for Lambda stages.
SCP Lambda
Creates/updates SCPs and attaches to target OUs.
# What SCP Lambda does
organizations.create_policy(Content=deny-s3-public.json)
organizations.attach_policy(PolicyId, TargetId=WorkloadsOU)
StackSet Lambda
Creates/updates StackSets and deploys to target accounts.
# What StackSet Lambda does
cloudformation.create_stack_set(TemplateBody=vpc.yaml)
cloudformation.create_stack_instances(
StackSetName,
DeploymentTargets={OrganizationalUnitIds}
)
EventBridge Rule
CfCT creates an EventBridge rule that listens for Control Tower lifecycle events.
{
"source": ["aws.controltower"],
"detail-type": ["AWS Service Event via CloudTrail"],
"detail": {
"eventName": ["CreateManagedAccount", "UpdateManagedAccount"]
}
}
Target: Lifecycle Lambda
Lifecycle Lambda
Triggered by EventBridge when Control Tower creates new account or moves account to different OU.
Control Tower creates account
│
▼
EventBridge receives "CreateManagedAccount" event
│
▼
Rule matches → triggers Lifecycle Lambda
│
▼
Lambda applies customizations to new account
What Lifecycle Lambda does:
def handler(event):
account_id = event['detail']['serviceEventDetails']['createManagedAccountStatus']['account']['accountId']
ou_id = get_ou_for_account(account_id)
# Read manifest to find which StackSets apply to this OU
stacksets = get_stacksets_for_ou(ou_id)
for stackset in stacksets:
# Add new account to existing StackSet
cloudformation.create_stack_instances(
StackSetName=stackset['name'],
Accounts=[account_id],
Regions=stackset['regions']
)
# SCPs: Nothing to do! Account inherits from OU automatically
“Add to StackSet instances” = Call create_stack_instances() to deploy existing StackSet template to the new account.
Why Lifecycle Lambda doesn’t handle SCPs: SCPs are attached to OUs, not accounts. When new account joins OU, it automatically inherits OU’s SCPs.
Different Triggers, Different Lambdas
| Trigger | What runs | What happens |
|---|---|---|
| S3/CodeCommit change | Full pipeline (CodeBuild → SCP Lambda → StackSet Lambda) | Update all SCPs and StackSets |
| New account created | Lifecycle Lambda only | Add account to existing StackSets |
| Account moved to different OU | Lifecycle Lambda only | Add/remove from StackSets based on new OU |
Lifecycle Lambda works independently. It calls AWS APIs directly, not other Lambdas.
StackSet Permissions
StackSets need two IAM roles to deploy across accounts.
Management Account:
StackSet Administration Role
- Trust: cloudformation.amazonaws.com
- Permission: sts:AssumeRole on execution roles
Target Account (each member account):
StackSet Execution Role
- Trust: Management Account
- Permission: Create actual resources (EC2, VPC, IAM...)
Permission Models
| Model | How it works |
|---|---|
| Self-managed | You create both roles manually |
| Service-managed | AWS Organizations creates roles automatically |
With Control Tower: Use service-managed permissions (recommended).
cloudformation.create_stack_set(
PermissionModel="SERVICE_MANAGED",
AutoDeployment={
"Enabled": True,
"RetainStacksOnAccountRemoval": False
}
)
CfCT Setup Steps
1. Deploy CfCT solution (CloudFormation template from AWS)
https://aws.amazon.com/solutions/implementations/
customizations-for-aws-control-tower/
2. CfCT creates:
- CodePipeline
- CodeBuild
- S3 bucket (or CodeCommit repo)
- Lambda functions
- EventBridge rule
3. Upload your customizations:
s3://cfct-bucket/
├── manifest.yaml
├── templates/
│ └── security-baseline.yaml
└── policies/
└── deny-s3-public.json
4. Pipeline triggers automatically on upload
Common Customizations
| Customization | Example |
|---|---|
| Networking | VPCs, Transit Gateway attachments, VPC endpoints |
| Security | IAM roles, Config rules, GuardDuty, Security Hub |
| Logging | CloudWatch log groups, Kinesis streams |
| Governance | Service Catalog portfolios, tagging policies |
| SCPs | Additional preventive controls |
Summary
| Term | What it is |
|---|---|
| Control Tower | AWS service that builds/manages landing zones |
| Landing Zone | Multi-account architecture pattern |
| Preventive Controls | SCPs (block API calls) |
| Detective Controls | Config Rules (detect after creation) |
| Proactive Controls | CloudFormation Hooks (validate during deployment) |
| CfCT | Solution to deploy custom templates/SCPs automatically |
| Audit Account | Security team investigation |
| Log Archive Account | Immutable log storage |
| Lifecycle Lambda | Auto-applies customizations to new accounts via EventBridge |