aws-cloudformation / aws-cloudformation/cloudformation-coverage-roadmap
[Resource Type] - [BUG] - REVERT_DRIFT change sets report phantom changes for Fn::GetStackOutput properties on IN_SYNC stacks
- Dominant language
- No language data
- Stars
- 1.1k
- Forks
- 62
- PR merge metrics
- No merged PRs in 30d
Description
### Name of the resource
AWS::Lambda::EventSourceMapping
### Resource Name
_No response_
### Issue Description
Observed 2026-09-02, region eu-central-1. No CDK involved in the reproduction: plain CloudFormation templates and the AWS CLI only.
When a stack property references another stack's output via `Fn::GetStackOutput`, a drift-aware change set (`create-change-set --deployment-mode REVERT_DRIFT`) against the *unchanged, previously deployed template* reports a `Modify` for every resource whose properties contain `Fn::GetStackOutput`. The `AfterValue` of the affected property is the unresolved token `{{changeSet:KNOWN_AFTER_APPLY}}`. The same template against the same stack produces an **empty change set** under the default (NORMAL) deployment mode. `DetectStackDrift` on the same stack returns `StackDriftStatus: IN_SYNC` with `DriftedStackResourceCount: 0`, and the drift-aware change set itself reports `StackDriftStatus: IN_SYNC`. The change-set engine leaves the `Fn::GetStackOutput` value unresolved and includes the resource as a change anyway, while the drift-detection engine resolves the same intrinsic and finds no drift.
For `AWS::Lambda::EventSourceMapping` the entry is reported with `Replacement: True` and `PolicyAction: ReplaceAndDelete` (because `EventSourceArn` is in the change scope). A reviewer cannot tell this apart from a real replacement, yet executing the change set does not replace the mapping.
Impact: any stack using `Fn::GetStackOutput` (per https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getstackoutput.html, "the referenced value is resolved at stack create or update time") gets non-empty drift-aware change sets on every deployment. This defeats change review, and deployment tooling that gates on change-set contents cannot use REVERT_DRIFT with such stacks. This is not limited to hand-written templates: AWS CDK emits `Fn::GetStackOutput` for cross-stack references when `@aws-cdk/core:defaultCrossStackReferences` is `"weak"` (its recommended value), so every drift-aware deployment of such an app is affected.
### Expected Behavior
Drift-aware change set creation resolves `Fn::GetStackOutput`, like drift detection already does (DetectStackDrift compares the resolved value against actual state and reports IN_SYNC), and produces an empty change set when the stack is IN_SYNC and the template is unchanged, matching the NORMAL mode result for the same template. Alternatively, the behavior for unresolvable intrinsics in drift-aware change sets should be documented, and `Replacement=True` should not be reported for a value that cannot be compared.
If the phantom entries are intended as conservative handling of statically unresolvable intrinsics, that cannot explain `Replacement: True` with `PolicyAction: ReplaceAndDelete` being reported for a value the engine itself marks as unresolvable (`{{changeSet:KNOWN_AFTER_APPLY}}`): a replacement cannot be claimed for a value that was never compared. Resolving cross-stack references at change-set creation is also demonstrably possible — the change-set engine already resolves `Fn::ImportValue`, the older cross-stack intrinsic, at creation time.
### Observed Behavior
Variant 1, `AWS::Lambda::Function` whose `Environment.Variables.TopicName` is `Fn::GetStackOutput` of a producer stack's output (summarized from `describe-change-set`; all field names are as returned by the API):
```
--- Modify ReproFunction AWS::Lambda::Function Replacement: False ResourceDriftStatus: IN_SYNC
Target: /Properties/Environment/Variables/TopicName
BeforeValue: revertdrift--producer-Topic-xxxxxxxx
AfterValue: {{changeSet:KNOWN_AFTER_APPLY}}
BeforeValueFrom: ACTUAL_STATE AfterValueFrom: TEMPLATE
Evaluation: Static ChangeSource: DirectModification
StackDriftStatus: IN_SYNC DeploymentMode: REVERT_DRIFT
```
Variant 2, `AWS::Lambda::EventSourceMapping` whose `EventSourceArn` is `Fn::GetStackOutput` (producer = Kinesis stream):
```
--- Modify Mapping AWS::Lambda::EventSourceMapping Replacement: True PolicyAction: ReplaceAndDelete ResourceDriftStatus: UNSUPPORTED
Target: /Properties/EventSourceArn
BeforeValue: arn:aws:kinesis:...:stream/...
AfterValue: {{changeSet:KNOWN_AFTER_APPLY}}
BeforeValueFrom: PREVIOUS_DEPLOYMENT_STATE Evaluation: Static
```
In both variants:
- `DetectStackDrift` reports the stack as `IN_SYNC` (`DriftedStackResourceCount: 0`) immediately before the change set is created.
- A plain change set (`--use-previous-template`, no `--deployment-mode`) against the same unchanged template fails with "The submitted information didn't contain changes."
- Executing the drift-aware change set (`execute-change-set`, then `wait stack-update-complete`) finishes in 7-9 seconds. `describe-stack-events` shows only stack-level UPDATE events and no resource-level events, the Lambda's `Environment` is bit-identical before and after, and the event source mapping's physical ID is unchanged. Execution therefore resolves `Fn::GetStackOutput` correctly and applies no changes: the `Modify` / `Replacement=True` entries at change-set creation time were phantom.
### Test Cases
Templates for variant 1 (SNS producer / Lambda consumer):
Producer:
```json
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "REVERT_DRIFT raw-CFN repro: producer stack",
"Resources": {
"Topic": {"Type": "AWS::SNS::Topic"}
},
"Outputs": {
"TopicName": {"Value": {"Fn::GetAtt": ["Topic", "TopicName"]}}
}
}
```
Consumer:
```json
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "REVERT_DRIFT raw-CFN repro: consumer stack (Lambda reading producer output via Fn::GetStackOutput)",
"Resources": {
"LambdaRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }
]
}
}
},
"ReproFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Runtime": "nodejs22.x",
"Handler": "index.handler",
"Code": { "ZipFile": "exports.handler = async (event) => ({ topic: process.env.TopicName });\n" },
"Role": { "Fn::GetAtt": ["LambdaRole", "Arn"] },
"Environment": {
"Variables": {
"TopicName": {
"Fn::GetStackOutput": {
"StackName": "",
"Region": "eu-central-1",
"OutputName": "TopicName"
}
}
}
}
}
}
}
}
```
Templates for variant 2 (Kinesis producer / Lambda + EventSourceMapping consumer):
Producer:
```json
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "REVERT_DRIFT raw-CFN repro 2: producer stack with Kinesis stream",
"Resources": {
"Stream": {"Type": "AWS::Kinesis::Stream", "Properties": {"ShardCount": 1}}
},
"Outputs": {
"StreamArn": {"Value": {"Fn::GetAtt": ["Stream", "Arn"]}}
}
}
```
Consumer:
```json
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "REVERT_DRIFT raw-CFN repro 2: consumer with Lambda EventSourceMapping via Fn::GetStackOutput",
"Resources": {
"LambdaRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }
]
},
"Policies": [
{
"PolicyName": "kinesis-read",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kinesis:DescribeStream",
"kinesis:DescribeStreamSummary",
"kinesis:GetRecords",
"kinesis:GetShardIterator",
"kinesis:ListShards",
"kinesis:ListStreams"
],
"Resource": "*"
}
]
}
}
]
}
},
"ReproFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Runtime": "nodejs22.x",
"Handler": "index.handler",
"Code": { "ZipFile": "exports.handler = async (event) => ({ ok: true });\n" },
"Role": { "Fn::GetAtt": ["LambdaRole", "Arn"] },
"Environment": {
"Variables": {
"StreamArn": {
"Fn::GetStackOutput": {
"StackName": "",
"Region": "eu-central-1",
"OutputName": "StreamArn"
}
}
}
}
}
},
"Mapping": {
"Type": "AWS::Lambda::EventSourceMapping",
"Properties": {
"EventSourceArn": {
"Fn::GetStackOutput": {
"StackName": "",
"Region": "eu-central-1",
"OutputName": "StreamArn"
}
},
"FunctionName": { "Fn::GetAtt": ["ReproFunction", "Arn"] },
"StartingPosition": "TRIM_HORIZON",
"BatchSize": 100
}
}
}
}
```
CLI steps (same for both variants; stack names and template files differ):
```
aws cloudformation create-stack --stack-name --template-body file://producer.json
aws cloudformation wait stack-create-complete --stack-name
aws cloudformation create-stack --stack-name --template-body file://consumer.json --capabilities CAPABILITY_IAM
aws cloudformation wait stack-create-complete --stack-name
aws cloudformation detect-stack-drift --stack-name
# note the StackDriftDetectionId, then poll until DetectionStatus is DETECTION_COMPLETE:
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id
# → StackDriftStatus: IN_SYNC, DriftedStackResourceCount: 0
aws cloudformation create-change-set --stack-name --change-set-name plain --use-previous-template --capabilities CAPABILITY_IAM
sleep 5
aws cloudformation describe-change-set --stack-name --change-set-name plain
# → Status: FAILED, StatusReason: "The submitted information didn't contain changes..."
aws cloudformation create-change-set --stack-name --change-set-name drift --use-previous-template --deployment-mode REVERT_DRIFT --capabilities CAPABILITY_IAM
aws cloudformation wait change-set-create-complete --stack-name --change-set-name drift
aws cloudformation describe-change-set --stack-name --change-set-name drift
# → CREATE_COMPLETE with phantom Modify entries as shown under Observed Behavior
```
Both drift-aware change sets can be executed to observe the no-op execution described under Observed Behavior:
```
aws cloudformation execute-change-set --stack-name --change-set-name drift
aws cloudformation wait stack-update-complete --stack-name
aws cloudformation describe-stack-events --stack-name --max-items 10
# → only stack-level UPDATE events, no resource-level events
```
Cleanup (repeat for the variant 2 stacks; deleting a stack also removes its change sets):
```
aws cloudformation delete-stack --stack-name
aws cloudformation wait stack-delete-complete --stack-name
aws cloudformation delete-stack --stack-name
aws cloudformation wait stack-delete-complete --stack-name
```
### Other Details
Possibly related: #2505 (REVERT_DRIFT change sets also mis-evaluate `!Ref` on unchanged, IN_SYNC stacks; same static-evaluation engine, different intrinsic).
Contributor guide
Research direction
No repository files or tests are identified; begin with the provided AWS CLI reproduction using the producer and consumer templates. Compare NORMAL and REVERT_DRIFT change sets, drift detection, and execution results, then confirm that the phantom entries disappear or determine the documented behavior needed for unresolved intrinsics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws
- Domain
- cloud, infrastructure
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100