aws / aws/aws-cdk

(core): (cross-stack reference to a migrated resource forces replacement of the consumer due to auto-generated export name)

Open
#38,492 1 comment 0 reactions 0 assignees View on GitHub
@aws-cdk/core effort/medium feature-request p2
Dominant language
TypeScript
Stars
12.9k
Forks
4.6k
Avg merge
2d 3h
Merged PRs (30d)
83

Description

### Describe the feature

When one CDK stack references a resource attribute from another stack (a cross-stack reference), CDK automatically creates an export on the producer stack and rewrites the consumer to import it via `Fn::ImportValue`. The generated export name follows the format `:ExportsOutputRef`.

If a stack already exports a value under a specific export name that other stacks import, there is currently no way to make a cross-stack reference **reuse that existing export name**. CDK always mints its own generated name instead.

This becomes a problem when the imported value feeds an **immutable / create-only property** (e.g. `VpcId` on `AWS::EC2::SecurityGroup`). CloudFormation determines replacement by diffing the **unresolved intrinsic expression**, not the resolved value. So when the consumer's `Fn::ImportValue` target changes from one export name to another — for example from `Fn::ImportValue: ""` to `Fn::ImportValue: ":ExportsOutputRef..."` CloudFormation reports `Replacement: True`, **even though both names resolve to the exact same value (same resource, same ID)**. (Note: inline `` names must be wrapped in backticks so GitHub does not strip them as HTML tags.)

The result is an avoidable resource replacement, driven purely by the export **name** changing rather than by any change in the underlying value.

### Use Case

Incremental raw CFN → CDK migrations where a resource must not be replaced security groups, ENIs, subnets, or anything with immutable cross-stack references. Today the export rename is only discovered after a change set flags a replacement, then must be manually pinned back. A mechanism would maintain deployment idempotency during cross-stack refactors and prevent avoidable replacement of downstream resources for example, a security group replacement that forces re-association of every ENI and instance referencing it.

### Proposed Solution

Provide an ergonomic, discoverable way to tell CDK **"reuse this existing export name for this cross-stack reference"** so the synthesized consumer keeps the original `Fn::ImportValue` string. For example:

- a prop/helper to **adopt an existing export name** on the producer such that cross-stack refs resolve to it instead of minting a new one, e.g. `stack.exportValue(vpc.vpcId, { name: "" })` that both creates the output **and** makes downstream cross-stack refs import that name; or

- a documented, "migration mode" for cross-stack references that preserves existing export names.

### Other Information

### Reproduction / supporting evidence

**Producer stack** : (L2 `Vpc`, no explicit `CfnOutput` CDK auto-generates the export because the consumer references the VPC across the stack boundary):

```java
this.vpc = Vpc.Builder.create(this, "Ec2Vpc")
.ipAddresses(IpAddresses.cidr("10.11.0.0/16"))
.build();
```

**Consumer stack** (L2 `SecurityGroup` taking the cross-stack construct; logical ID pinned so the change set isolates the `VpcId` change):

```java
SecurityGroup sg = SecurityGroup.Builder.create(this, "DefaultEc2SecurityGroup")
.vpc(vpc) // cross-stack construct reference -> auto export
.description("Default security group for testing")
.allowAllOutbound(true)
.build();
((CfnSecurityGroup) sg.getNode().getDefaultChild())
.overrideLogicalId("DefaultEc2SecurityGroup");
```

**Synthesized producer** : export CDK injected (never declared by the author):

```json
"Outputs": {
"ExportsOutputRefEc2Vpc5537D173": {
"Value": { "Ref": "Ec2Vpc5537D173" },
"Export": { "Name": "NetworkStack:ExportsOutputRefEc2Vpc5537D173" }
}
}
```

**Synthesized consumer** : import rewritten to the generated name (no longer the existing ``):

```json
"VpcId": { "Fn::ImportValue": "NetworkStack:ExportsOutputRefEc2Vpc5537D173" }
```

**Change set** against the deployed consumer stack (which imports the original ``):

```json
[
{ "Logical": "CDKMetadata", "Action": "Add", "Replacement": null, "Props": [] },
{
"Logical": "DefaultEc2SecurityGroup",
"Action": "Modify",
"Replacement": "True",
"Props": ["VpcId", "SecurityGroupEgress"]
}
]
```

`Replacement: True` is driven by the `VpcId` change — caused solely by the export-name rename, since the resolved VPC ID is unchanged. (`VpcId` is create-only; that alone forces the replacement. `SecurityGroupEgress` is an incidental L2 diff from `allowAllOutbound(true)` and is mutable.)

### Current workaround

Pin the existing export name on the producer and import it explicitly on the consumer, so the synthesized `VpcId` expression stays byte-for-byte identical to what is already deployed:

```java
// Producer — re-export under the existing name instead of relying on the auto-generated one:
new CfnOutput(this, "Ec2VpcIdLegacyExport", CfnOutputProps.builder()
.value(vpc.getVpcId())
.exportName("")
.build());

// Consumer — import the fixed name explicitly (L1, so the intrinsic is emitted verbatim):
String vpcId = Fn.importValue("");
CfnSecurityGroup.Builder.create(this, "DefaultEc2SecurityGroup")
.groupDescription("Default security group for testing")
.vpcId(vpcId)
.build();
```

With this approach the synthesized template continues to emit `"VpcId": { "Fn::ImportValue": "" }` — identical to the deployed template. `cdk diff` and the change set report **no changes** on the security group → **no replacement** (verified: change set returns *"The submitted information didn't contain changes"*).

### Acknowledgements

- [ ] I may be able to implement this feature request
- [ ] This feature might incur a breaking change

### AWS CDK Library version (aws-cdk-lib)

2.263.0

### AWS CDK CLI version

2.1135.0 (build 0fa27ff)

### Environment details (OS name and version, etc.)

macOS 26.5.2 (build 25F84), Apple Silicon (arm64); Language: Java (Maven)

Contributor guide

Open the contributing guide

Research direction

Start by tracing CDK's cross-stack reference synthesis and the exportValue and CfnOutput entry points described in the issue. Compare the current generated export/import names with the explicit existing-name workaround. Done means a supported producer API or migration mode preserves the existing Fn::ImportValue string for downstream consumers without forcing replacement.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, typescript
Domain
cloud, infrastructure
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.