CloudFormation StackSets: Multi-Account Deployments
Deploy and manage infrastructure across multiple AWS accounts and regions using CloudFormation StackSets with proper IAM setup and deployment strategies.
Managing infrastructure across dozens of AWS accounts is a nightmare without the right tools. CloudFormation StackSets let you deploy and update stacks across multiple accounts and regions from a single template. Here’s how to do it properly.
StackSets Overview
A StackSet is a CloudFormation template that creates “stack instances” in target accounts and regions:
StackSet (Management Account)
│
├── Stack Instance → Account A, us-east-1
├── Stack Instance → Account A, eu-west-1
├── Stack Instance → Account B, us-east-1
├── Stack Instance → Account B, eu-west-1
└── Stack Instance → Account C, us-east-1
Permission Models
Service-Managed Permissions (AWS Organizations)
The recommended approach for Organizations:
# stackset-admin-role.yaml (deployed to management account)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Enable service-managed StackSets'
Resources:
# AWS Organizations automatically handles permissions
# Just enable trusted access in Organizations settings
# This is done via CLI:
# aws organizations enable-aws-service-access \
# --service-principal stacksets.cloudformation.amazonaws.com
# Enable trusted access for StackSets
aws organizations enable-aws-service-access \
--service-principal stacksets.cloudformation.amazonaws.com
# Enable delegated administrator (optional, allows non-management accounts to manage StackSets)
aws organizations register-delegated-administrator \
--account-id 123456789012 \
--service-principal stacksets.cloudformation.amazonaws.com
Self-Managed Permissions
For non-Organizations accounts, set up IAM roles manually:
# admin-role.yaml - Deploy to management account
AWSTemplateFormatVersion: '2010-09-09'
Description: 'StackSet Administrator Role'
Parameters:
TargetAccountIds:
Type: CommaDelimitedList
Description: 'List of target account IDs'
Resources:
StackSetAdminRole:
Type: AWS::IAM::Role
Properties:
RoleName: AWSCloudFormationStackSetAdministrationRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: StackSetAdminPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Resource:
- !Sub 'arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole'
# execution-role.yaml - Deploy to EACH target account
AWSTemplateFormatVersion: '2010-09-09'
Description: 'StackSet Execution Role'
Parameters:
AdministratorAccountId:
Type: String
Description: 'Account ID of the administrator account'
Resources:
StackSetExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: AWSCloudFormationStackSetExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AdministratorAccountId}:role/AWSCloudFormationStackSetAdministrationRole'
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess # Scope down in production
Creating a StackSet
Basic Example: CloudTrail in All Accounts
# cloudtrail-stackset.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Enable CloudTrail in all accounts'
Parameters:
OrganizationId:
Type: String
Description: 'AWS Organization ID'
LoggingBucketName:
Type: String
Description: 'Centralized logging bucket name'
LoggingAccountId:
Type: String
Description: 'Account ID hosting the logging bucket'
Resources:
CloudTrail:
Type: AWS::CloudTrail::Trail
Properties:
TrailName: organization-trail
S3BucketName: !Ref LoggingBucketName
IsMultiRegionTrail: true
EnableLogFileValidation: true
IncludeGlobalServiceEvents: true
IsLogging: true
# SNS topic for CloudTrail notifications
CloudTrailTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: cloudtrail-notifications
Outputs:
TrailArn:
Value: !GetAtt CloudTrail.Arn
Export:
Name: !Sub '${AWS::StackName}-TrailArn'
Deploy the StackSet
# Create StackSet with service-managed permissions
aws cloudformation create-stack-set \
--stack-set-name cloudtrail-all-accounts \
--template-body file://cloudtrail-stackset.yaml \
--parameters \
ParameterKey=OrganizationId,ParameterValue=o-xxxxxxxxxx \
ParameterKey=LoggingBucketName,ParameterValue=org-cloudtrail-logs \
ParameterKey=LoggingAccountId,ParameterValue=111111111111 \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
--capabilities CAPABILITY_NAMED_IAM
# Create stack instances in all accounts
aws cloudformation create-stack-instances \
--stack-set-name cloudtrail-all-accounts \
--deployment-targets OrganizationalUnitIds=ou-xxxx-xxxxxxxx \
--regions us-east-1 eu-west-1 ap-southeast-1 \
--operation-preferences \
RegionConcurrencyType=PARALLEL,MaxConcurrentPercentage=25
Deployment Strategies
Target Organizational Units (OUs)
# Deploy to specific OUs
aws cloudformation create-stack-instances \
--stack-set-name security-baseline \
--deployment-targets OrganizationalUnitIds=ou-prod-xxxxx,ou-dev-xxxxx \
--regions us-east-1 \
--operation-preferences MaxConcurrentPercentage=10
Target Specific Accounts
# Deploy to specific accounts (self-managed or override)
aws cloudformation create-stack-instances \
--stack-set-name custom-vpc \
--accounts 111111111111 222222222222 333333333333 \
--regions us-east-1 us-west-2
Exclude Accounts
# Deploy to OU but exclude certain accounts
aws cloudformation create-stack-instances \
--stack-set-name monitoring \
--deployment-targets \
OrganizationalUnitIds=ou-root-xxxxx \
AccountFilterType=DIFFERENCE \
Accounts=999999999999 # Exclude this account
--regions us-east-1
Advanced: Security Baseline StackSet
A real-world example deploying security controls:
# security-baseline.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Organization Security Baseline'
Parameters:
SecurityAccountId:
Type: String
Description: 'Central security account ID'
GuardDutyEnabled:
Type: String
Default: 'true'
AllowedValues: ['true', 'false']
Conditions:
EnableGuardDuty: !Equals [!Ref GuardDutyEnabled, 'true']
Resources:
# Enable Security Hub
SecurityHub:
Type: AWS::SecurityHub::Hub
Properties:
Tags:
Environment: production
# Enable GuardDuty (member account)
GuardDutyDetector:
Type: AWS::GuardDuty::Detector
Condition: EnableGuardDuty
Properties:
Enable: true
FindingPublishingFrequency: FIFTEEN_MINUTES
# Config Recorder
ConfigRecorder:
Type: AWS::Config::ConfigurationRecorder
Properties:
Name: default
RecordingGroup:
AllSupported: true
IncludeGlobalResourceTypes: true
RoleARN: !GetAtt ConfigRole.Arn
ConfigRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: config.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWS_ConfigRole
# Delivery channel for Config
ConfigDeliveryChannel:
Type: AWS::Config::DeliveryChannel
DependsOn: ConfigRecorder
Properties:
S3BucketName: !Sub 'config-logs-${SecurityAccountId}'
ConfigSnapshotDeliveryProperties:
DeliveryFrequency: TwentyFour_Hours
# Password policy
IAMPasswordPolicy:
Type: AWS::IAM::AccountPasswordPolicy
Properties:
MinimumPasswordLength: 14
RequireLowercaseCharacters: true
RequireUppercaseCharacters: true
RequireNumbers: true
RequireSymbols: true
AllowUsersToChangePassword: true
MaxPasswordAge: 90
PasswordReusePrevention: 24
# Block public S3 buckets at account level
S3BlockPublicAccess:
Type: AWS::S3::AccountPublicAccessBlock
Properties:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
# EBS encryption default
EBSEncryptionByDefault:
Type: AWS::EC2::EncryptionByDefault
Properties:
Enabled: true
Outputs:
SecurityHubArn:
Value: !Ref SecurityHub
GuardDutyDetectorId:
Condition: EnableGuardDuty
Value: !Ref GuardDutyDetector
StackSet Operations
Update a StackSet
# Update the template
aws cloudformation update-stack-set \
--stack-set-name security-baseline \
--template-body file://security-baseline-v2.yaml \
--parameters \
ParameterKey=SecurityAccountId,ParameterValue=111111111111 \
--operation-preferences \
RegionConcurrencyType=SEQUENTIAL,MaxConcurrentPercentage=25,FailureTolerancePercentage=10
# Check operation status
aws cloudformation describe-stack-set-operation \
--stack-set-name security-baseline \
--operation-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Drift Detection
# Detect drift across all instances
aws cloudformation detect-stack-set-drift \
--stack-set-name security-baseline
# Check drift status
aws cloudformation describe-stack-set-drift-status \
--stack-set-name security-baseline
Delete Stack Instances
# Remove from specific accounts/regions
aws cloudformation delete-stack-instances \
--stack-set-name custom-vpc \
--accounts 111111111111 \
--regions us-west-2 \
--no-retain-stacks # Actually delete the resources
# Delete entire StackSet (must delete all instances first)
aws cloudformation delete-stack-set \
--stack-set-name custom-vpc
CI/CD Integration
GitHub Actions
# .github/workflows/stackset-deploy.yml
name: Deploy StackSet
on:
push:
branches: [main]
paths:
- 'stacksets/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111111111111:role/StackSetDeployRole
aws-region: us-east-1
- name: Validate Template
run: |
aws cloudformation validate-template \
--template-body file://stacksets/security-baseline.yaml
- name: Update StackSet
run: |
aws cloudformation update-stack-set \
--stack-set-name security-baseline \
--template-body file://stacksets/security-baseline.yaml \
--parameters file://stacksets/parameters.json \
--operation-preferences \
RegionConcurrencyType=PARALLEL,MaxConcurrentPercentage=25
- name: Wait for Operation
run: |
OPERATION_ID=$(aws cloudformation list-stack-set-operations \
--stack-set-name security-baseline \
--query 'Summaries[0].OperationId' --output text)
while true; do
STATUS=$(aws cloudformation describe-stack-set-operation \
--stack-set-name security-baseline \
--operation-id $OPERATION_ID \
--query 'StackSetOperation.Status' --output text)
echo "Operation status: $STATUS"
if [[ "$STATUS" == "SUCCEEDED" ]]; then
exit 0
elif [[ "$STATUS" == "FAILED" || "$STATUS" == "STOPPED" ]]; then
exit 1
fi
sleep 30
done
Parameter Overrides
Different values per account or region:
# Create with overrides
aws cloudformation create-stack-instances \
--stack-set-name vpc-stackset \
--deployment-targets OrganizationalUnitIds=ou-prod-xxxxx \
--regions us-east-1 us-west-2 \
--parameter-overrides \
ParameterKey=VpcCidr,ParameterValue=10.0.0.0/16 \
ParameterKey=Environment,ParameterValue=prod
# parameters-overrides.json for complex scenarios
[
{
"Account": "111111111111",
"Region": "us-east-1",
"ParameterOverrides": [
{"ParameterKey": "VpcCidr", "ParameterValue": "10.1.0.0/16"},
{"ParameterKey": "AZCount", "ParameterValue": "3"}
]
},
{
"Account": "222222222222",
"Region": "us-east-1",
"ParameterOverrides": [
{"ParameterKey": "VpcCidr", "ParameterValue": "10.2.0.0/16"},
{"ParameterKey": "AZCount", "ParameterValue": "2"}
]
}
]
Troubleshooting
Common Issues
# View failed operations
aws cloudformation list-stack-set-operation-results \
--stack-set-name security-baseline \
--operation-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--query 'Summaries[?Status==`FAILED`]'
# Common failures:
# 1. Missing execution role in target account
# 2. Template validation errors
# 3. Resource limit exceeded
# 4. Insufficient permissions
Monitoring
# CloudWatch alarms for StackSet operations
StackSetFailureAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: StackSet-Operation-Failed
MetricName: StackSetOperationFailed
Namespace: AWS/CloudFormation
Statistic: Sum
Period: 300
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
AlarmActions:
- !Ref AlertTopic
When NOT to Use StackSets
StackSets aren’t always the answer:
- Single account/region: Use regular CloudFormation
- Complex dependencies: StackSets don’t handle cross-stack dependencies well
- Rapid iteration: Updates are slow across many accounts
- Account-specific resources: Use regular stacks with CI/CD
Key Takeaways
- Use service-managed permissions with AWS Organizations—much simpler than self-managed
- Deploy to OUs, not account lists—new accounts get resources automatically
- Start small—test with one account before deploying to hundreds
- Use operation preferences—control concurrency and failure tolerance
- Monitor operations—StackSet deployments can silently fail in some accounts
- Enable auto-deployment—new accounts in OUs get stack instances automatically
StackSets are essential for multi-account AWS governance. Once set up properly, they ensure consistent security and compliance baselines across your entire organization.