A new account lands in the Production OU on a Friday afternoon. By Monday it still has no alerting topic, no encryption defaults and none of the audit settings every other account carries. The baseline lives in an assume-role loop that someone runs by hand, and nobody ran it.
AWS StackSets (AWS CloudFormation StackSets) deploy one CloudFormation template as stacks in many accounts and Regions from a single administrator account. With service-managed permissions, a StackSet targets AWS Organizations organizational units (OUs) and, with automatic deployment on, deploys to accounts that join them later.
If you own that loop, you need three answers: which permission model to pick, what happens when an account moves between OUs, and how to keep one bad change from breaking 40 accounts at once. The limits and a CDK example you can synthesize follow.
How AWS StackSets Reach Every Account and Region
Three StackSets concepts carry the rest of this guide:
- Administrator account: the account where you create the StackSet. For service-managed StackSets, that is the management account or a delegated administrator.
- Target account: an account that receives a stack from the StackSet.
- Stack instance: a reference to a stack in one target account and one Region. It can exist without its stack, which is why a failed create still shows up as an instance.
A CloudFormation StackSet is itself a Regional resource, visible only in the Region where you created it. Every instance comes from its one template; parameter overrides change values per account or Region. Deleting instances with retain leaves their stacks running on their own, and a StackSet can only be deleted once it has no instances left.
Self-Managed or Service-Managed: Which Permission Model Fits?
The permission model decides who creates the IAM roles and which accounts you can reach. PermissionModel can't change while a StackSet has stack instances, so choose once.
| Question | Self-managed permissions | Service-managed permissions |
|---|---|---|
| Who creates the IAM roles | You: AWSCloudFormationStackSetAdministrationRole in the administrator account, AWSCloudFormationStackSetExecutionRole in every target | StackSets, through service-linked roles in the management and member accounts |
| What you target | Account IDs, in or outside your organization | OU IDs or the organization root |
| New accounts deployed automatically | No | Yes, with automatic deployment on |
| Reaches the management account | Yes | Never, even when you target the root |
| Prerequisites | Both roles, plus iam:PassRole on the administration role for the caller | All features enabled in Organizations, trusted access activated from the management account |
| Template limits | None beyond standard CloudFormation | No nested stacks, macros or transforms |
| Pick it when | You need the management account, a single dedicated account or accounts outside the org | The baseline belongs to every account in an OU, now and later |
My rule: self-managed for the management account or accounts outside your organization, service-managed for everything that lives in OUs. The remaining service-managed restrictions are under creating StackSets with service-managed permissions.
Running StackSets from a delegated administrator
You don't have to work in the management account day to day. Register a member account as the StackSets delegated administrator:
aws organizations register-delegated-administrator \
--service-principal=member.org.stacksets.cloudformation.amazonaws.com \
--account-id=111122223333
An organization can have up to five delegated administrators, each with "full permissions to deploy to accounts in your organization" that the management account can't narrow. Service-managed StackSets are still created in the management account, including those a delegated administrator creates; the delegated administrator manages them by passing CallAs: DELEGATED_ADMIN. Guard that account like the management account when you set up a delegated administrator.
What Happens When an Account Joins, Moves or Leaves an OU
Automatic deployment is the service-managed feature that fixes the Friday account. You set it per StackSet; it can't vary by OU, account or Region. Once it is on, AWS Organizations membership drives the StackSet:
- An account joins a target OU: StackSets creates stack instances in every Region the StackSet covers.
- An account leaves: StackSets deletes its instances, unless
RetainStacksOnAccountRemovalis true. Then the resources stay in the account, no longer managed by the StackSet. - An account moves between OUs: when each OU has its own StackSet, moving an account from Production to Workloads runs a delete operation for the Production StackSet and queues a create operation for the Workloads one.
Two details catch teams later. New accounts receive the StackSet's default parameter values, not your overrides. Automatic deployment also ignores account-level targeting filters, so targeting a few accounts with it enabled still deploys to accounts added later.
This call turns on automatic deployment for an existing StackSet. Don't pass deployment targets or Regions in the same call, and add --call-as DELEGATED_ADMIN from a delegated administrator:
aws cloudformation update-stack-set \
--stack-set-name my-stackset \
--use-previous-template \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=true
Since November 2025, AutoDeployment.DependsOn lists StackSets that must finish first, up to 10 per StackSet and 100 per account, so when automatic deployment runs, a StackSet waits for the ones that create what it needs.
On retain, I decide by what breaks: on for anything a running workload depends on, such as log delivery and access roles; off for resources that only make sense under that OU's guardrails. The OU design guide covers where accounts should sit.
How Do You Control a StackSet Rollout?
Operation preferences decide how an update spreads: how many accounts at once, how many failures to tolerate and in which Region order. Every field has a documented default, and those defaults shape every rollout you don't tune.
The defaults roll out one account at a time
Unset, maximum concurrent accounts is 1, failure tolerance is 0, the concurrency mode is STRICT_FAILURE_TOLERANCE and Regions run sequentially. Take 40 accounts in two Regions. That is 80 stack instance operations, run one account at a time, Region by Region. The first failure stops that Region, and CloudFormation doesn't attempt any later Region.
Set maximum concurrent accounts to 25% and failure tolerance to 10%, and the same rollout asks for 10 accounts per batch and tolerates four failed accounts per Region; the fifth stops that Region. Three rules apply:
- Maximum concurrent accounts is a ceiling. Service throttling can lower it.
- Failure tolerance counts per Region.
- Percentages round down, and concurrency never drops below 1.
RegionOrder puts your lowest-impact Region first; PARALLEL runs every Region at once. AWS's best practices start at 1 and 0 in one Region and test template updates on a few accounts before the rest.
Strict or soft failure tolerance
Under the default strict mode, concurrency never exceeds failure tolerance plus one, and CloudFormation lowers it further as failures occur. The 25% and 10% settings above would run at most five accounts at a time, not 10. Soft mode decouples the two: operations run at the configured concurrency regardless of failures. The tolerance still stops the Region, but only after accounts already in progress finish, so failures can overshoot it.
I use strict for a change that hasn't run anywhere yet, and soft once it has passed a test OU. The sequential, parallel and progressive patterns in AWS blog posts are combinations of these settings, not a separate mode.
New accounts ignore your rollout settings
"StackSetOperationPreferences don't apply to AutoDeployment, even if it's enabled," per the API reference. Your carefully tuned 25% and 10% govern updates you push, not the account that joins on Friday.
AWS recommends managed execution with automatic deployment. With ManagedExecution active, CloudFormation runs non-conflicting operations concurrently and queues conflicting ones; without it, a StackSet performs one operation at a time. You can't change it while operations run or wait, so turn it on early.
Drift, Quotas and the Limits You Will Hit
StackSet drift detection runs through DetectStackSetDrift. It checks the stack behind every instance against the template and parameters, overrides included. One drifted resource marks the stack, the instance and the StackSet as DRIFTED. The other statuses are IN_SYNC and NOT_CHECKED. Changes you make to one stack through CloudFormation directly don't count as drift, and only one drift operation can run per StackSet at a time.
StackSets has no native drift schedule that I could find, so schedule it yourself: an EventBridge Scheduler or Lambda job that calls DetectStackSetDrift and alerts on DRIFTED.
The CloudFormation quotas that matter most:
- 1,000 StackSets per administrator account
- 100,000 stack instances per StackSet
- 10,000 stack instance operations running at the same time per Region, across all StackSets in an administrator account
- 10,000 queued operations per StackSet
- 51,200 bytes for a template body in the request, 1 MB for a template stored in S3
The first two can be raised through Service Quotas. Global resources, such as IAM roles and S3 bucket names, collide when the same stack lands in two Regions of one account. Make the names Region-unique, or give global resources a StackSet with a single Region.
Define AWS StackSets in CDK and Review the Change
aws-cdk-lib 2.276.0 has no stable L2 construct for StackSets, so the CDK StackSet you write today is the L1 CfnStackSet. The cdklabs cdk-stacksets package offers an L2, but it is experimental, still at version 0.0.x and open to breaking changes.
The L1 does no asset handling. Pass the template as a templateBody string (51,200 bytes at most), or upload it to S3 yourself and pass templateUrl. Because the inner template carries no assets, nothing in the target accounts depends on CDK bootstrap; only the administrator account that deploys this stack does.
import { App, Stack, type StackProps } from 'aws-cdk-lib';
import { CfnStackSet } from 'aws-cdk-lib/aws-cloudformation';
import type { Construct } from 'constructs';
// Regional resources only, so two Regions per account don't collide.
const baselineTemplate = {
AWSTemplateFormatVersion: '2010-09-09',
Resources: {
BaselineVersion: {
Type: 'AWS::SSM::Parameter',
Properties: {
Name: '/baseline/version',
Type: 'String',
Value: '1.0.0',
},
},
LocalAlerts: {
Type: 'AWS::SNS::Topic',
Properties: { TopicName: 'baseline-alerts' },
},
},
};
class BaselineStackSetStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
new CfnStackSet(this, 'Baseline', {
stackSetName: 'workload-baseline',
permissionModel: 'SERVICE_MANAGED',
// Run from a delegated administrator; use 'SELF' from the management account.
callAs: 'DELEGATED_ADMIN',
templateBody: JSON.stringify(baselineTemplate),
autoDeployment: { enabled: true, retainStacksOnAccountRemoval: true },
managedExecution: { Active: true },
// Governs operations this resource starts (creates and updates).
// It does not throttle auto-deployment to new accounts.
operationPreferences: {
regionConcurrencyType: 'SEQUENTIAL',
regionOrder: ['eu-west-1', 'eu-central-1'],
maxConcurrentPercentage: 25,
failureTolerancePercentage: 10,
concurrencyMode: 'STRICT_FAILURE_TOLERANCE',
},
stackInstancesGroup: [
{
deploymentTargets: {
// Pass -c workloadsOuId=ou-xxxx-xxxxxxxx to every cdk command.
organizationalUnitIds: [this.node.getContext('workloadsOuId')],
},
regions: ['eu-west-1', 'eu-central-1'],
},
],
});
}
}
const app = new App();
new BaselineStackSetStack(app, 'BaselineStackSetStack');
Note the capital A in managedExecution: { Active: true }. CDK types that property as any and passes keys through unchanged; synthesizing { active: true } printed:
WARNING ManagedExecution.active: Additional properties are not allowed ('active' was unexpected. Did you mean 'Active'?) (CloudFormation Validate)
Deploy it with cdk deploy -c workloadsOuId=ou-xxxx-xxxxxxxx. Then bump the baseline version to 1.1.0 and run cdk diff -c workloadsOuId=ou-xxxx-xxxxxxxx against the deployed stack:
Stack BaselineStackSetStack
Resources
[~] AWS::CloudFormation::StackSet Baseline Baseline
└─ [~] TemplateBody
├─ [-] {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"BaselineVersion":{"Type":"AWS::SSM::Parameter","Properties":{"Name":"/baseline/version","Type":"String","Value":"1.0.0"}},"LocalAlerts":{"Type":"AWS::SNS::Topic","Properties":{"TopicName":"baseline-alerts"}}}}
└─ [+] {"AWSTemplateFormatVersion":"2010-09-09","Resources":{"BaselineVersion":{"Type":"AWS::SSM::Parameter","Properties":{"Name":"/baseline/version","Type":"String","Value":"1.1.0"}},"LocalAlerts":{"Type":"AWS::SNS::Topic","Properties":{"TopicName":"baseline-alerts"}}}}
That is a template-level diff of the single AWS::CloudFormation::StackSet resource. One changed value shows as the whole string replaced, with nothing about which accounts or Regions receive it, so keep the inner template readable in source where the pull request's line diff shows the change.
In CloudFormation, a template or parameter change marks every stack instance OUTDATED before the targeted accounts and Regions update. Point a change at a test OU or test StackSet first, then widen. To look up every property of AWS::CloudFormation::StackSet, use the CloudFormation resource properties tool.
When Another Tool Fits Better
Your existing stack decides this more than any feature list.
For application stacks with stages, CDK Pipelines deploys the stages you list in code, each with an explicit account and Region, and every target must be bootstrapped. That suits promotion with tests between environments, not a baseline that must follow OU membership. In CDK, authoring and rollout are separate decisions.
Terraform teams can use the AWS provider's aws_cloudformation_stack_set resource, which supports SERVICE_MANAGED permissions, an auto_deployment block, call_as and managed_execution, so they can drive StackSets without leaving Terraform. The CDK vs Terraform guide covers the wider choice.
If you want AWS to run account setup and controls from a managed console, look at AWS Control Tower. Compare Control Tower with a code-owned landing zone, or read the broader list of Control Tower alternatives.
How a Landing Zone Builds Its Baseline on StackSets
The AWS CDK Landing Zone uses both permission models. One landing zone account, registered as the StackSets delegated administrator, manages every baseline StackSet. Member-account baselines are service-managed and target OUs, so a new account in a targeted OU receives them with no loop to run. StackSets for a single account, the management account included, are self-managed.
Retargeting is one target argument: fromOrganizationalUnits for service-managed, fromAccounts for self-managed, with excludeAccounts to skip accounts inside a targeted OU. Each change arrives as a pull request with its stack and StackSet diff. See the StackSets reference and how to customize targets, Regions and rollout. The landing zone is AWS CDK in TypeScript, so it suits teams that already review infrastructure changes as pull requests.
What to keep:
- Use service-managed AWS StackSets with automatic deployment for OU-wide baselines, self-managed for the management account.
- Unset operation preferences mean one account at a time and a stop at the first failure.
- Operation preferences don't govern new accounts; AWS recommends managed execution alongside automatic deployment.
- Keep the StackSet in code and review both diffs.
Your next step: create one service-managed StackSet for one OU in one Region, with managed execution on, and widen once its first instances are healthy. Then read how accounts get created and closed to see where it fits in the account lifecycle.
Next step