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)

Open
#2,505 0 comments 1 reaction 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.