Skip to content

W9007: distinct Fn::GetStackOutput values are treated as duplicates #247

Description

@pahud

W9007 reports duplicate values for arrays of distinct Fn::GetStackOutput values. This is the same class of problem as #52 (fixed for Fn::ImportValue in #83), but the fix was never extended to Fn::GetStackOutput.

Snippet:

{
  "Resources": {
    "ALB": {
      "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
      "Properties": {
        "Type": "application",
        "Subnets": [
          { "Fn::GetStackOutput": { "StackName": "VpcStack", "Region": "ap-northeast-1", "OutputName": "PublishOutputRefVpcPublicSubnet1Subnet5C2D37C4F5EE3F2D" } },
          { "Fn::GetStackOutput": { "StackName": "VpcStack", "Region": "ap-northeast-1", "OutputName": "PublishOutputRefVpcPublicSubnet2Subnet691E08A328131BFA" } },
          { "Fn::GetStackOutput": { "StackName": "VpcStack", "Region": "ap-northeast-1", "OutputName": "PublishOutputRefVpcPublicSubnet3SubnetBE12F0B6AC1253B4" } }
        ]
      }
    }
  }
}

Warning:

WARNING Subnets: Array property 'Subnets' contains duplicate values (CloudFormation Validate)
   AlbStack/ALB/Resource (ALBAEE750D2) aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer
   Acknowledge with 'CloudFormation-Validate::W9007'

The three entries reference different outputs, so they are distinct unknowable deploy-time values.

Scope

I probed RegoEngine.validateDetailed() (engine 1.5.1, as bundled in aws-cdk-lib 2.262.1) with the same resource shape, varying only the Subnets entries:

Subnets contents W9007
distinct Fn::GetStackOutput (different OutputName) fires
distinct Fn::GetStackOutput (different StackName) fires
distinct Fn::ImportValue clean
distinct Ref clean
distinct Fn::GetAtt clean
distinct literals clean
duplicate literals fires (correct)
one Fn::GetStackOutput + one literal clean

So the misfire is exclusive to Fn::GetStackOutput, and even a differing StackName is not enough to make two values distinct.

Cause

In src/template-model/src/resolver.rs on main, the ImportValue arm folds the export name into the resolved value's reason, which is what keeps two different imports distinct:

IntrinsicFn::ImportValue(arg) => {
    let reason = match self.resolve_node(*arg) {
        ResolvedValue::Concrete { value } => match value.as_str() {
            Some(export) => format!("cross-stack import: {export}"),
            None => "cross-stack import".into(),
        },
        _ => "cross-stack import".into(),
    };
    ResolvedValue::TypedDynamic { reason, param_type: PARAM_TYPE_STRING.into() }
}

The GetStackOutput arm resolves its arguments only to maintain current_path, discards the results, and returns a constant reason:

IntrinsicFn::GetStackOutput(args) => {
    let saved = self.current_path.clone();
    for (key, arg) in args {
        self.current_path = format!("{}.{}", saved, key);
        let _ = self.resolve_node(*arg);
    }
    self.current_path = saved;
    ResolvedValue::Dynamic { reason: "cross-stack output".into() }
}

StackName, Region, and OutputName never reach the resolved value, so every Fn::GetStackOutput in a template resolves identically and W9007's duplicate detection sees N identical entries.

Suggested fix

Mirror the ImportValue arm: carry the distinguishing detail (StackName + Region + OutputName) into the resolved value's reason when the arguments resolve to concrete values, and consider returning TypedDynamic with PARAM_TYPE_STRING for parity.

Impact

CDK renders cross-stack references as Fn::GetStackOutput when the @aws-cdk/core:defaultCrossStackReferences feature flag is set to weak, so this fires on common patterns whose subnet lists come from another stack — ALB Subnets, AWS::RDS::DBSubnetGroup, and AWS::ElastiCache::SubnetGroup. Reported downstream as aws/aws-cdk#38424.

Engine version tested: 1.5.1 (bundled in aws-cdk-lib 2.262.1). The code above is still present on main as of 2026-07-28.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions