aws-cdk icon indicating copy to clipboard operation
aws-cdk copied to clipboard

(core): `crossRegionReferences` generates unnecessarily long SSM parameter names leading to deployment failures

Open colifran opened this issue 9 months ago • 1 comments

Describe the bug

When using crossRegionReferences the producing stack creates SSM Parameters in the consuming region for each exported value. This is accomplished via this code https://github.com/aws/aws-cdk/blob/2779b5e7b4c962409f977eff2d28b96bc01c6c46/packages/aws-cdk-lib/core/lib/private/refs.ts#L248-L263. For users using longer stack names or nested stacks, the generated parameter name can become unnecessarily long. As a result, this can lead to deployment failures resulting from a response object that exceeds the 4096 byte limit when the export reader and writer custom resource providers are responding back to CloudFormation.

Expected Behavior

crossRegionReferences doesn't generate unnecessarily long SSM parameter names.

Current Behavior

crossRegionReferences generates SSM parameters with the name /cdk/exports/{consumingStackName}/{export-name}. For users using nested stacks or for users with longer stack names (or both), the parameter names can become unnecessarily long which can result in deployment failures.

Reproduction Steps

import * as awscdk from 'aws-cdk-lib'

const ACCOUNT_ID = '111222333444'
const STACK_PREFIX = 'testing-cdk-cross-region-references-as-example-for-github-issue'
// 8 or less works, 9 or more fails with "Response object is too long"
const NUMBER_OF_KEYS_TO_CREATE = 9

function createRootStack (app: awscdk.App, region: string): awscdk.Stack {
  const stackName = `${STACK_PREFIX}-rootstack-${region}`

  const props = {
    env: {
      account: ACCOUNT_ID,
      region: region
    },
    crossRegionReferences: true,
    synthesizer: new awscdk.LegacyStackSynthesizer()
  }

  const stack = new awscdk.Stack(app, stackName, props)

  return stack
}

function createPrimaryKeyStack (rootStack: awscdk.Stack, keyName: string): string {
  const nestedStack = new awscdk.NestedStack(rootStack, `${STACK_PREFIX}-nestedstack-${keyName}`)

  const keyProps = getKeyProps(keyName)
  const kmsCfnKey = new awscdk.aws_kms.CfnKey(nestedStack, `${STACK_PREFIX}-key-${keyName}`, keyProps)

  const aliasName = `alias/${STACK_PREFIX}-${keyName}`
  const aliasProps = getAliasProps(aliasName, kmsCfnKey.attrKeyId)
  new awscdk.aws_kms.CfnAlias(nestedStack, `${STACK_PREFIX}-alias-${keyName}`, aliasProps)

  return kmsCfnKey.attrArn
}

function createReplicaKeyStack (rootStack: awscdk.Stack, keyName: string, primaryKeyArn: string) {
  const nestedStack = new awscdk.NestedStack(rootStack, `${STACK_PREFIX}-nestedstack-${keyName}`)

  const keyProps = getReplicaKeyProps(keyName, primaryKeyArn)
  const kmsCfnKey = new awscdk.aws_kms.CfnReplicaKey(nestedStack, `${STACK_PREFIX}-key-${keyName}`, keyProps)

  const aliasName = `alias/${STACK_PREFIX}-${keyName}`
  const aliasProps = getAliasProps(aliasName, kmsCfnKey.attrKeyId)
  new awscdk.aws_kms.CfnAlias(nestedStack, `${STACK_PREFIX}-alias-${keyName}`, aliasProps)
}

function getKeyPolicy (): awscdk.aws_iam.PolicyDocument {
  const thisPrincipal = new awscdk.aws_iam.AccountPrincipal(ACCOUNT_ID)

  const policyStatement = new awscdk.aws_iam.PolicyStatement({
    sid: 'Allow all key access',
    effect: awscdk.aws_iam.Effect.ALLOW,
    actions: [
      'kms:*'
    ],
    resources: ['*'],
    principals: [thisPrincipal]
  })

  const policyDocument = new awscdk.aws_iam.PolicyDocument({ statements: [policyStatement] })

  return policyDocument
}

function getKeyProps (keyName: string): awscdk.aws_kms.CfnKeyProps {
  const policyDocument = getKeyPolicy()

  const keyProps: awscdk.aws_kms.CfnKeyProps = {
    description: `Testing cross-region references in CDK - ${keyName}`,
    keyPolicy: policyDocument,
    multiRegion: true,
    enableKeyRotation: false,
    enabled: true,
    keyUsage: 'ENCRYPT_DECRYPT',
    pendingWindowInDays: 7
  }

  return keyProps
}

function getReplicaKeyProps (keyName: string, primaryKeyArn: string): awscdk.aws_kms.CfnReplicaKeyProps {
  const policyDocument = getKeyPolicy()

  const keyProps: awscdk.aws_kms.CfnReplicaKeyProps = {
    description: `Testing cross-region references in CDK - ${keyName}`,
    primaryKeyArn: primaryKeyArn,
    keyPolicy: policyDocument,
    enabled: true,
    pendingWindowInDays: 7
  }

  return keyProps
}

function getAliasProps (aliasName:string, targetKeyId: string): awscdk.aws_kms.CfnAliasProps {
  const aliasProps: awscdk.aws_kms.CfnAliasProps = {
    aliasName: aliasName,
    targetKeyId: targetKeyId
  }

  return aliasProps
}

function main (args: Array<string> | undefined = undefined) {
  const app = new awscdk.App()

  const rootStackEast = createRootStack(app, 'us-east-1')
  const rootStackWest = createRootStack(app, 'us-west-2')

  for (let i = 1; i <= NUMBER_OF_KEYS_TO_CREATE; i++) {
    const primaryKeyArn = createPrimaryKeyStack(rootStackEast, i.toString())
    createReplicaKeyStack(rootStackWest, i.toString(), primaryKeyArn)
  }
}

if (require.main === module) {
  main()
}

Possible Solution

crossRegionReferences is an experimental feature. We may want to consider updating the SSM parameter names being generated to be limited to a certain length. This would break existing customers, though. Ideally, we could find a way to give users a choice to optionally limit the parameter name length. While this doesn't help us get around the 4096 byte limitation for custom resource provider response objects, it would reduce the chances of a deployment failure by limiting the response body size.

Additional Information/Context

No response

CDK CLI Version

2.136.0

Framework Version

No response

Node.js Version

20.11.0

OS

MacOS

Language

TypeScript

Language Version

No response

Other information

No response

colifran avatar May 09 '24 01:05 colifran

Thanks @colifran for reference I don't use nested stacks, and my stack prefixes are only 4 letters long, and I was still running into this issue due do the number of regions AWS has available.

renschler avatar May 09 '24 01:05 renschler