(core): (cross-stack reference to a migrated resource forces replacement of the consumer due to auto-generated export name)
- 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
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