Recently, I ran into a head‑scratcher while deploying a CDK stack. Everything used to work fine, but once my organization introduced strict SCP rules based on tags, my cdk deploy
started failing with:
AccessDenied: action cloudformation:CreateChangeSet is not authorized
At first glance, it didn’t make sense. I was already tagging everything in my CDK code. I even had a loop that pulled tags from props.Tags
and attached them with cdk.Tags.of(this).add(...)
. These tags used to flow down nicely to all resources — including the CloudFormation stack itself.
So why did it suddenly stop working? 🤔
What Changed? SCPs and Request‑Time Enforcement
The key is where SCP rules get evaluated.
An SCP can restrict not only what resources exist, but also what API calls are allowed. In my case, the org had a policy like this:(dummy example)
{
"Effect": "Deny",
"Action": "cloudformation:CreateChangeSet",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestTag/Org": "ABC"
}
}
}
This means: if the CreateChangeSet
request doesn’t already include the required tag, the call is blocked right away.
CDK Tagging: Two Different Worlds
This is where CDK behavior matters:
StackProps.tags
When you pass tags into thesuper(scope, id, props)
constructor, CDK includes those tags in theCreateChangeSet
API call. These show up asRequestTags
. That’s what SCPs check.cdk.Tags.of(resource).add(...)
This method attaches tags to resources inside the CloudFormation template. They are applied after the stack is already created.
So my old approach of looping through props.Tags
and calling cdk.Tags.of(this).add(...)
worked fine in the past, but now fails because the SCP never lets the stack get created in the first place. The required tags simply aren’t present yet at request time.
Fix: Pass Tags via StackProps
The solution was simple once I understood the difference. Previously my props had Tags property which I replaced with tags which props:cdk.StackProps expects and uses to initialize CDK’s in-memory construct tree (the Stack object inside your app).
When you run cdk deploy, the CLI uses the CloudFormation SDK to call:
CreateChangeSet (or UpdateChangeSet) → this is the first API call to AWS.
This is where stack-level tags (props.tags) are injected into the request payload
export class MyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
for (const [k, v] of Object.entries(props.tags)) {
cdk.Tags.of(this).add(k, v);
}
}
}
new MyStack(app, "MyTaggedStack", {
tags: {
Org: "ABC",
Owner: "TeamA"
}
});
Now:
-
StackProps.tags → go straight into the
CreateChangeSet
request (SCP passes ✅). - cdk.Tags.of(this).add(...) → still ensures all resources get the same tags after creation.
Takeaway
If your organization enforces strict SCP rules on cloudformation:CreateChangeSet
, you can’t rely on cdk.Tags.of(...)
alone. Those tags arrive too late. You need to use StackProps.tags
so the tags are present in the request itself.
It’s a subtle but important difference — and once I understood it, the “AccessDenied” error finally made sense.
Top comments (0)