Nicollas Braga
Nicollas Braga

Reputation: 821

AWS CDK parameter error: Value of property Parameters must be an object with String (or simple type) properties

Update

The accepted answer bellow by Victor is the correct workaround.

I get in contact with AWS Support that confirmed the bug. They are aware of the issue and linked me to those 2 links to follow the case.

... During my investigation and replication of the issue, I noticed that this issue seems to be with how CDK handles the VPCEndpoint resource as its parameters include a list of DNS entries with them.
This is a known issue with the resource:
https://github.com/aws/aws-cdk/issues/5897
https://github.com/aws/aws-cdk/issues/9488

Until then if you face this problem, the solution is here.


Original question

I was expecting to be able to use Fn.select and Fn.split out of the box, but the bellow code have some very weird behavior, wondering if I'm doing something unexpected.

I expected the output to contains the declared functions fn::Select [1, fn:split [ fn:select [0, getAttr [ something ] ] ] ]

But actually what I see is just getAttr [something], what leads to Value of property Parameters must be an object with String (or simple type) properties during deployment

Any help is appreciated

Here's the code to reproduce the error

#!/usr/bin/env node
import 'source-map-support/register';
import {App, aws_ec2, aws_lambda, Fn, NestedStack, NestedStackProps, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

const app = new App();

export class MainStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);
    new Nested1(this, 'first', {})
  }
}
export class Nested1 extends NestedStack {
  constructor(scope: Construct, id: string, props: NestedStackProps) {
    super(scope, id, props);

    const vpc = new aws_ec2.Vpc(this, 'vpc', {
      subnetConfiguration: [{
        cidrMask: 28,
        name: 'private-test-bug',
        subnetType: aws_ec2.SubnetType.PRIVATE_ISOLATED,
      }]
    })
    const apiEndpoint = new aws_ec2.InterfaceVpcEndpoint(this, `apiEndpoint`, {
      service: {
        name: 'com.amazonaws.eu-central-1.execute-api',
        port: 443,
      },
      vpc: vpc,
      subnets: {
        subnets: vpc.isolatedSubnets,
        onePerAz: true
      },
      open: true,
      privateDnsEnabled: false,
    });
    //  BUG IS HERE
    new Nested2(this, 'nested2', { VpcEndpoint: apiEndpoint }) // CDK should handle and parse the reference, but it dont
  }
}

export interface Nested2StackProps extends NestedStackProps { VpcEndpoint: aws_ec2.InterfaceVpcEndpoint; }
export class Nested2 extends NestedStack {
  constructor(scope: Construct, id: string, props: Nested2StackProps) {
    super(scope, id, props);

    const lambda = new aws_lambda.Function(this, 'lambda-function', {
      code: aws_lambda.Code.fromInline(`exports.handler = (event, context) => { console.log(process.env.dns_name) }`),
      handler: 'index.handler',
      runtime: aws_lambda.Runtime.NODEJS_16_X,
      environment: { dns_name: Fn.select(1, Fn.split(':', Fn.select(0, props.VpcEndpoint.vpcEndpointDnsEntries))) } // USAGE IS HERE
    })
  }
}

new MainStack (app, 'TestStack', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

Upvotes: 1

Views: 2269

Answers (1)

Victor Smirnov
Victor Smirnov

Reputation: 3780

In a nutshell, it looks like a bug in the CDK. The CDK tries to send an array as a parameter to the nested template. Instead, it should join the array and pass it as a string.

I suggest a workaround for the issue. I split my code into two parts. First, I explain the idea and show some code snippets. Finally, I show the complete solution.

We define parameter in the nested stack like this:

  const stack = new NestedStack(scope, id, {
    parameters: {
      VpcEndpointDnsEntries: props.VpcEndpointDnsEntries
    },
  })

In the code, we create an object for this parameter and use it to get the parameter value.

  const endpoints = new CfnParameter(stack, 'VpcEndpointDnsEntries', {
    type: 'List<String>',
    description: 'List of entries.'
  })

// Read the value:

const strings = endpoints.valueAsList

In the final step, we pass parameters to the nested stack almost as usual, except that we join the string.

  createNestedStack(stack, 'NestedStack',
    { VpcEndpointDnsEntries: Fn.join(',', apiEndpoint.vpcEndpointDnsEntries) })

Note that the lambda function is not working, it throws a runtime exception. Otherwise, the stack and nested stack deploys normally.

Please find the complete code below.

Sorry, I am really in a hurry now. I plan to fix grammar and update the answer this evening.

import { App, CfnParameter, Fn, NestedStack, Stack } from 'aws-cdk-lib'
import { env } from 'process'
import { InterfaceVpcEndpoint, Vpc } from 'aws-cdk-lib/aws-ec2'
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'

function createMainStack (scope, id, props) {
  const stack = new Stack(scope, id, props)

  const vpc = Vpc.fromLookup(stack, 'Vpc', { vpcName: 'WarehousePrinters' })

  const apiEndpoint = new InterfaceVpcEndpoint(stack, `ApiEndpoint`, {
    service: {
      name: 'com.amazonaws.eu-west-1.execute-api',
      port: 443,
    },
    vpc,
    subnets: { subnets: vpc.privateSubnets, onePerAz: true },
    open: true,
    privateDnsEnabled: false,
  })

  createNestedStack(stack, 'NestedStack', { VpcEndpointDnsEntries: Fn.join(',', apiEndpoint.vpcEndpointDnsEntries) })

  return stack
}

function createNestedStack (scope, id, props) {
  const stack = new NestedStack(scope, id, {
    parameters: {
      VpcEndpointDnsEntries: props.VpcEndpointDnsEntries
    },
  })

  const endpoints = new CfnParameter(stack, 'VpcEndpointDnsEntries', {
    type: 'List<String>',
    description: 'List of entries.'
  })

  new Function(stack, 'LambdaFunction', {
    code: Code.fromInline(`export function handler = () => console.log(env.dns_name)`),
    handler: 'index.handler',
    runtime: Runtime.NODEJS_16_X,
    environment: {
      dns_name: Fn.select(1, Fn.split(':', Fn.select(0, endpoints.valueAsList)))
    }
  })

  return stack
}

const app = new App()
createMainStack(app, 'MainStack', {
  env: { account: env.CDK_DEFAULT_ACCOUNT, region: env.CDK_DEFAULT_REGION }
})

Upvotes: 1

Related Questions