Skip to content

(aws-cdk-lib): Property overrides in CDK Aspects and CfnParameter #19447

@avanderm

Description

@avanderm

General Issue

CDK Aspects used to modify properties will lead to odd behaviour when the original value was a CfnReference

The Question

Consider the following CDK Aspect, a simplified version we use to modify names of resources according to a naming convention:

import {
  CfnResource,
  IAspect,
  Stack,
  Token,
} from 'aws-cdk-lib';
import { CfnRole } from 'aws-cdk-lib/aws-iam';
import { IConstruct } from 'constructs';

function hasOwnProperty<X extends {}, Y extends PropertyKey>
  (obj X, prop: Y): obj is X & Record<Y, unknown> {
    return obj.hasOwnProperty(prop)
  }

function isRef(name: string | object) {
  return typeof name === 'object' && this.hasOwnProperty(name, 'Ref');
}

function isJoin(name: string | object) {
  return typeof name === 'object' && this.hasOwnProperty(name, 'Fn::Join');
}

export class MyAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (CfnResource.isCfnResource(node) && node.cfnResourceType == CfnRole.CFN_RESOURCE_TYPE_NAME) {
      const role = node as CfnRole;
      const name = Stack.of(node).resolve(role.roleName) || node.logicalId;

      const newName = [ 
        'my',
        this.isRef(name) || this.isJoin(name) ? Token.asString(name) : name, 
      ].join('-');

      node.addPropertyOverride('RoleName', newName);
    }
  }
}

And the following stack we apply the aspect on:

import { CfnParameter, Fn, Stack, StackProps } from 'aws-cdk-lib';
import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class AspectsReferencesStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const parameter = new CfnParameter(this, 'MyParameter', {
      type: 'String',
    }).valueAsString;

    new Role(this, 'MyRole1', {
      assumedBy: new ServicePrincipal('codebuild.amazonaws.com'),
    });

    new Role(this, 'MyRole2', {
      assumedBy: new ServicePrincipal('codebuild.amazonaws.com'),
      roleName: 'a-role',
    });

    new Role(this, 'MyRole3', {
      assumedBy: new ServicePrincipal('codebuild.amazonaws.com'),
      roleName: parameter,
    });

    new Role(this, 'MyRole4', {
      assumedBy: new ServicePrincipal('codebuild.amazonaws.com'),
      roleName: `a-${parameter}`,
    });
  }
}

What you will see is that most results are as expected and produce valid CloudFormation. However, the third role will contain invalid code, and the RoleName property will be:

RoleName:
  Ref: MyParameter
  Fn::Join:
    - ""
    - - my-
      - Ref: MyParameter

The Ref key will always be present, which is odd considering the override of the property. I've tried various tricks, such as deletion overrides on RoleName.Ref and more, but this additional field always seems to persist and just sit alongside the Fn::Join resulting from the interpolation in the aspect. I realize this is a fringe case, and the reason why I'm even considering a CfnParameter is because we use this to in combination with the ProductStack of the service catalog module to provide input fields when provisioning.

What happens under the hood in addPropertyOverride? Does it simply do some sort of object merge?

CDK CLI Version

2.10.0 (build e5b301f)

Framework Version

No response

Node.js Version

v16.13.0

OS

Fedora release 35 (Thirty Five)

Language

Typescript

Language Version

TypeScript (3.9.7)

Other information

No response

Metadata

Metadata

Assignees

Labels

aws-cdk-libRelated to the aws-cdk-lib packagebugThis issue is a bug.p1

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions