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

  1. Use service-managed permissions with AWS Organizations—much simpler than self-managed
  2. Deploy to OUs, not account lists—new accounts get resources automatically
  3. Start small—test with one account before deploying to hundreds
  4. Use operation preferences—control concurrency and failure tolerance
  5. Monitor operations—StackSet deployments can silently fail in some accounts
  6. 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.