aws-cloudformation / aws-cloudformation/cloudformation-coverage-roadmap
[AWS::ApiGateway::Resource] - [BUG] - REVERT_DRIFT change sets resolve !Ref to RestApiId (primaryIdentifier[0]) instead of ResourceId (the documented Ref return value)
- Dominant language
- No language data
- Stars
- 1.1k
- Forks
- 62
- PR merge metrics
- No merged PRs in 30d
Description
### Name of the resource
AWS::ApiGateway::Resource
### Resource Name
_No response_
### Issue Description
CloudFormation drift-aware change sets (`--deployment-mode REVERT_DRIFT`, launched November 2025) incorrectly evaluate `!Ref` (and the equivalent `Fn::Sub "${LogicalId}"` form) for `AWS::ApiGateway::Resource`. The static intrinsic-function evaluator returns the resource's parent **RestApiId** instead of the resource's own **ResourceId**, contradicting the documented Ref behavior at https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-resource.html:
> When you pass the logical ID of this resource to the intrinsic Ref function, Ref returns the resource ID, such as `abc123`.
The resource's CloudFormation Registry schema declares a composite `primaryIdentifier` of `[/properties/RestApiId, /properties/ResourceId]`. The observed behavior is consistent with REVERT_DRIFT's evaluator returning `primaryIdentifier[0]` (RestApiId) for `Ref` instead of the documented Ref return value (ResourceId, the schema's read-only property). NORMAL deployment mode resolves the same `!Ref` correctly against the same template.
The same template, against the same stack at the same moment, produces:
- An **empty change set** under NORMAL deployment mode (correct).
- **N phantom `Replacement=True` change set entries** under REVERT_DRIFT mode, one for every Method with `ResourceId: !Ref XxxResource` and every nested Resource with `ParentId: !Ref XxxResource` (incorrect).
In our production stack this is 19 phantom replacements. In the minimal clean-room repro it is 2 (one Resource and one Method). Approving such a change set would delete and recreate the affected API Gateway Resources and Methods to byte-identical configurations, with the production API briefly returning 404s during the recreate.
`AWS::ApiGateway::Resource` is in the supported list for drift-aware change sets per https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/drift-aware-change-sets.html (it is not listed under "Resource type support limitations").
### Expected Behavior
A REVERT_DRIFT change set against an unchanged template should be empty when the live stack is in sync with the template, identical to the NORMAL change set against the same template. `!Ref` on an `AWS::ApiGateway::Resource` should return the resource's own ResourceId (e.g. `p9hz7d`), matching the documented Ref behavior and matching how NORMAL mode resolves it.
### Observed Behavior
REVERT_DRIFT statically evaluates `!Ref MyApiGwResource` (and `Fn::Sub "${MyApiGwResource}"`) from the template and returns the parent RestApi's physical ID, not the resource's own ResourceId. This produces phantom `Modify` / `Replacement=True` entries on every `Method.ResourceId` and every child `Resource.ParentId` that references such a resource.
Smoking gun, captured from `aws cloudformation describe-change-set --include-property-values` against the clean-room stack (RestApi `MyApi` physical ID `3dj6odcmya`, parent `MyResource` ResourceId `uh1wyj`, child `MyChildResource` ResourceId `p9hz7d`):
```text
=== MyChildResource (AWS::ApiGateway::Resource) action=Modify replacement=True ===
attr=ParentId BeforeValue='uh1wyj' AfterValue='3dj6odcmya'
BeforeValueFrom=ACTUAL_STATE AfterValueFrom=TEMPLATE
Evaluation=Static ChangeSource=ResourceReference CausingEntity=MyResource
BeforeContext.Properties = {"ParentId": "uh1wyj", "PathPart": "{id}", "RestApiId": "3dj6odcmya"}
AfterContext.Properties = {"ParentId": "3dj6odcmya", "PathPart": "{id}", "RestApiId": "3dj6odcmya"}
```
Two things to note in `AfterContext.Properties`:
1. `ParentId` collapses from the correct `uh1wyj` (MyResource's ResourceId, ACTUAL_STATE) to `3dj6odcmya` (the RestApiId).
2. `ParentId` and `RestApiId` end up with the same value, suggesting `!Ref MyResource` and `!Ref MyApi` both resolve to the RestApiId.
`AfterValueFrom: TEMPLATE` with `Evaluation: Static` and `ChangeSource: ResourceReference` shows REVERT_DRIFT statically resolved `!Ref MyResource` from the template to the wrong physical ID. The same `!Ref` resolves correctly under NORMAL mode (which is why the NORMAL change set against the same template is empty).
`Fn::Sub "${MyResource}"` exhibits the identical wrong-value behavior (verified, see "Verified narrowing" below), so the bug is not in the `Ref` opcode handler in isolation; it is in the shared logical-id-to-physical-id resolution that backs both `Ref` and `Fn::Sub` variable substitution under REVERT_DRIFT.
### Test Cases
Clean-room reproduction. All commands below were executed against a fresh stack named `revert-drift-repro` in `us-east-2` on 2026-05-06 and produced the output shown.
**Minimal template** (`template.yaml`):
```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: REVERT_DRIFT phantom-change repro for AWS::ApiGateway::Resource
Resources:
MyApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: revert-drift-repro
MyResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref MyApi
ParentId: !GetAtt MyApi.RootResourceId
PathPart: things
MyChildResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref MyApi
ParentId: !Ref MyResource
PathPart: '{id}'
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref MyApi
ResourceId: !Ref MyChildResource
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: MOCK
IntegrationResponses:
- StatusCode: 200
RequestTemplates:
application/json: '{"statusCode":200}'
MethodResponses:
- StatusCode: 200
```
**Steps:**
1. Create stack:
```bash
aws cloudformation create-stack \
--stack-name revert-drift-repro \
--template-body file://template.yaml \
--capabilities CAPABILITY_IAM
aws cloudformation wait stack-create-complete --stack-name revert-drift-repro
```
2. Confirm IN_SYNC:
```bash
aws cloudformation detect-stack-drift --stack-name revert-drift-repro
```
`aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id ` returns `StackDriftStatus: IN_SYNC`, `DriftedStackResourceCount: 0`.
3. NORMAL change set against the unchanged template:
```bash
aws cloudformation create-change-set \
--stack-name revert-drift-repro \
--change-set-name normal-empty \
--template-body file://template.yaml \
--capabilities CAPABILITY_IAM
```
Result (correct):
```text
Status: FAILED
StatusReason: The submitted information didn't contain changes. Submit different information to create a change set.
```
4. REVERT_DRIFT change set against the SAME unchanged template:
```bash
aws cloudformation create-change-set \
--stack-name revert-drift-repro \
--change-set-name revert-drift-phantom \
--template-body file://template.yaml \
--deployment-mode REVERT_DRIFT \
--capabilities CAPABILITY_IAM
```
Result (incorrect): `Status: CREATE_COMPLETE`, with two phantom replacements:
```bash
aws cloudformation describe-change-set \
--stack-name revert-drift-repro \
--change-set-name revert-drift-phantom \
--query 'Changes[].ResourceChange.[LogicalResourceId,Action,Replacement]' \
--output table
```
```text
---------------------------------------
| DescribeChangeSet |
+------------------+----------+-------+
| MyChildResource | Modify | True |
| MyMethod | Modify | True |
+------------------+----------+-------+
```
5. Inspect diagnostic detail:
```bash
aws cloudformation describe-change-set \
--stack-name revert-drift-repro \
--change-set-name revert-drift-phantom \
--include-property-values \
--query 'Changes[].ResourceChange.{Logical:LogicalResourceId,Details:Details,After:AfterContext}'
```
See "Observed Behavior" above for the captured output. `BeforeValue` is the resource's own ResourceId (correct). `AfterValue` is the RestApiId (wrong), with `AfterValueFrom: TEMPLATE` and `Evaluation: Static`.
### Other Details
**Region / scope:** Reproduced 2026-05-05 in `us-east-2` against a production stack and 2026-05-06 in `us-east-2` against a clean-room stack. Region-independent in principle since the bug is in the change-set intrinsic-function evaluator.
**Impact:** Customers adopting drift-aware change sets on stacks containing `AWS::ApiGateway::Resource` cannot achieve idempotent no-op deploys. Approving the phantom change set would delete and recreate N API Gateway Resources and Methods to byte-identical configurations, briefly breaking production traffic for what should be a no-op.
**Workaround:** Use `--deployment-mode NORMAL` instead of `REVERT_DRIFT` for stacks containing `AWS::ApiGateway::Resource`. This sacrifices in-deploy drift correction for those stacks.
**Verified narrowing (probes run on the clean-room stack, all change sets describe-only and then deleted):**
| Probe | Template change | NORMAL mode | REVERT_DRIFT mode | Conclusion |
| --- | --- | --- | --- | --- |
| Test 1 | `MyMethod.ResourceId: !GetAtt MyChildResource.ResourceId` (parent ParentId still `!Ref MyResource`) | MyMethod Modify (template syntax change) | MyChildResource Modify + MyMethod Modify (cascade) | `!GetAtt` doesn't fix the cascade because `!Ref MyResource` upstream is still wrong. |
| Test 2 | `MyChildResource.ParentId: !GetAtt MyResource.ResourceId` (Method still `!Ref MyChildResource`) | MyChildResource + MyMethod Modify (template syntax change) | **MyMethod only** (MyChildResource clean) | `!GetAtt` resolves correctly (`uh1wyj`). The remaining MyMethod phantom shows `!Ref MyChildResource` resolving to `3dj6odcmya` (RestApiId) instead of `p9hz7d`. **Isolates bug to `!Ref`, not `!GetAtt`.** |
| Test 4 | `MyChildResource.ParentId: !Sub "${MyResource}"`, `MyMethod.ResourceId: !Sub "${MyChildResource}"` | MyChildResource + MyMethod Modify | MyChildResource + MyMethod Modify with `AfterValue=3dj6odcmya` (same wrong-value pattern as `!Ref`) | `Fn::Sub` variable form has the **identical** wrong-value bug. The defect is in shared logical-id resolution, not in the `Ref` opcode alone. |
| Test 5 | Cloud Control `GetResource` and CFN Registry schema for `AWS::ApiGateway::Resource` | n/a | n/a | `primaryIdentifier = ["/properties/RestApiId", "/properties/ResourceId"]`; `readOnlyProperties = ["/properties/ResourceId"]`. The Cloud Control identifier format is `\|`. **Suggests REVERT_DRIFT's static `Ref` evaluator is returning `primaryIdentifier[0]` (RestApiId) instead of the documented Ref return value (ResourceId).** |
Test 2's REVERT_DRIFT diagnostic detail (the cleanest single piece of evidence, since `!GetAtt` resolves correctly while `!Ref` does not, on the same change set):
```text
=== MyMethod (AWS::ApiGateway::Method) action=Modify replacement=True ===
attr=ResourceId BeforeValue='p9hz7d' AfterValue='3dj6odcmya'
BeforeValueFrom=ACTUAL_STATE AfterValueFrom=TEMPLATE
Evaluation=Static ChangeSource=ResourceReference CausingEntity=MyChildResource
AfterContext.Properties = {... "ResourceId": "3dj6odcmya", "RestApiId": "3dj6odcmya"}
```
Note `AfterContext.Properties.ResourceId` and `AfterContext.Properties.RestApiId` collapse to the same value: `!Ref MyChildResource` and `!Ref MyApi` both produce `3dj6odcmya`.
**References:**
- `AWS::ApiGateway::Resource` Ref documented behavior:
- Drift-aware change sets, supported types:
- REVERT_DRIFT launch announcement (November 2025):
- AWS DevOps Blog post:
Contributor guide
Research direction
Start with the minimal template in template.yaml and run the listed AWS CLI commands in us-east-2, comparing NORMAL and REVERT_DRIFT change sets. Inspect describe-change-set output with --include-property-values; done means an unchanged stack produces an empty REVERT_DRIFT change set and Ref resolves to the resource's ResourceId.
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
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100