Skip to content

Conversation

@pahud
Copy link
Contributor

@pahud pahud commented Jun 18, 2025

Issue # (if applicable)

Closes #15301.

Reason for this change

CloudFront PublicKey constructs currently use node.addr as the caller reference, which changes when the construct tree structure is modified (e.g., moving constructs, renaming, or refactoring). This causes CloudFormation deployment failures with the error "Invalid request provided: AWS::CloudFront::PublicKey" because CloudFront treats caller reference changes as attempts to create new resources rather than updates to existing ones.

This is a critical issue for users who need to refactor their CDK code or update their public keys, as any structural changes to the construct tree break subsequent deployments.

Description of changes

Core Changes:

  • Added feature flag @aws-cdk/aws-cloudfront:stablePublicKeyCallerReference in cx-api/lib/features.ts with recommendedValue: true
  • Modified PublicKey class in aws-cloudfront/lib/public-key.ts to:
    • Check the feature flag using FeatureFlags.of(this).isEnabled()
    • Use a stable hash-based caller reference when flag is enabled
    • Fall back to this.node.addr when flag is disabled (backward compatibility)
    • Respect CloudFront's 128-character limit for caller references

Stable Caller Reference Implementation:

The new stable caller reference is generated using:

  • Stack name: Ensures uniqueness across different stacks
  • Construct ID: Uses this.node.id (not the full path) for stability
  • Account/Region: Ensures uniqueness across environments
  • Hash suffix: Provides collision resistance and length management

Example: MyStack-MyPublicKey-a1b2c3d4 (instead of path-based node.addr)

Why this approach solves the issue:

  • Stable across refactoring: Uses this.node.id instead of this.node.addr, so moving constructs in the tree doesn't change the caller reference
  • Globally unique: Includes stack name, account, and region to prevent collisions
  • Backward compatible: Behind a feature flag, existing deployments continue working
  • CloudFront compliant: Respects the 128-character caller reference limit

Comparison of approaches:

  • this.node.addr (Old/Unstable): Changes when you refactor code structure, even if the logical hierarchy is the same
  • Stable caller reference (New/Stable): Remains constant when you refactor code, but correctly changes when you alter the logical construct hierarchy

Describe any new or updated permissions being added

No new or updated IAM permissions are required. This change only affects the caller reference field in CloudFormation templates, which is a metadata field and doesn't impact AWS API permissions.

Description of how you validated changes

Unit Tests:

Added comprehensive test suite in aws-cloudfront/test/public-key.test.ts covering:

  • Feature flag disabled behavior: Backward compatibility with node.addr
  • Feature flag enabled behavior: Stable caller reference generation
  • Construct tree stability: Same caller reference when construct is moved in tree
  • Cross-stack uniqueness: Different caller references for different stacks
  • Cross-environment uniqueness: Different caller references for different accounts/regions
  • CloudFront length limits: Proper truncation when names exceed 128 characters
  • Original issue reproduction: Demonstrates that issue (CloudFront): Initial Create Succeeds, Subsequent Updates Fail with Invalid request provided: AWS::CloudFront::PublicKey  #15301 is resolved
  • Migration scenarios: Shows node.addr changes but stable caller reference doesn't

Integration Tests:

  • Created integ.public-key-stable-caller-reference.ts demonstrating real-world usage
  • Verified CloudFormation template generation with stable caller references
  • Tested both flag states to ensure proper behavior

Key Test Results:

// Before: node.addr changes when construct is moved
// Direct placement: c872d91ae0d2943aad25d4b31f1304d0a62c658ace
// Nested placement:  c815b84c38db03df5d30a639ab6b4db592ff0ef851

// After: stable caller reference remains the same
// Direct placement: TestStack-MyPublicKey-0cc31391
// Nested placement:  TestStack-MyPublicKey-0cc31391 ✅ Same!

Checklist

- Introduced a feature flag `@aws-cdk/aws-cloudfront:stablePublicKeyCallerReference` to enable stable caller references for CloudFront PublicKey constructs.
- Updated the PublicKey class to use a stable caller reference when the feature flag is enabled, preventing update failures due to changes in the construct tree.
- Enhanced tests to verify the behavior of the stable caller reference under different scenarios, including feature flag toggling and construct tree changes.
- Updated documentation to reflect the new feature and its usage.
@aws-cdk-automation aws-cdk-automation requested a review from a team June 18, 2025 20:12
@github-actions github-actions bot added bug This issue is a bug. effort/small Small work item – less than a day of effort p2 labels Jun 18, 2025
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Jun 18, 2025
@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv
  • Commit ID: ce43784
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

Enhance the generateStableCallerReference() method to use a more intelligent
truncation strategy when
@pahud pahud marked this pull request as ready for review August 14, 2025 16:12
@leonmk-aws leonmk-aws self-assigned this Aug 21, 2025
Copy link
Contributor

@leonmk-aws leonmk-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, generating the caller reference with Names.uniqueId does not solve the problem as this function generates a unique id based on the path of the construct in the tree, which means it changes when the PublicKey is moved in the tree (e.g during a refactoring) see:

const components = node.scopes.slice(1).map(c => Node.of(c).id);

@pahud
Copy link
Contributor Author

pahud commented Aug 26, 2025

Hi @leonmk-aws

Thank you for the excellent feedback! You're absolutely correct about Names.uniqueId() being path-based, and I'm happy to clarify that our revised implementation actually does not use Names.uniqueId() for the stable caller reference generation.

Current Implementation Details

Our implementation uses this.node.id (the immediate construct ID) rather than Names.uniqueId() (the path-based approach). Here's the key difference:

What we DON'T use (path-based, unstable):

// This would be unstable - changes when construct is moved
const unstableId = Names.uniqueId(this); // Uses full construct path

What we DO use (stable across tree changes):

private generateStableCallerReference(): string {
  const stack = Stack.of(this);
  const constructId = this.node.id; // ← Only the immediate construct ID, not the full path
  const stackName = stack.stackName;

  const stableComponents = [
    stackName,
    constructId,           // ← This is stable when construct is moved
    stack.account || 'unknown-account',
    stack.region || 'unknown-region',
  ];

  // Create hash for uniqueness and length management
  const hash = crypto.createHash('sha256')
    .update(stableComponents.join('-'))
    .digest('hex')
    .substring(0, 8);

  return `${stackName}-${constructId}-${hash}`;
}

Demonstration of Stability

Here's a concrete example showing that our implementation is stable across construct tree changes:

// Test 1: PublicKey directly in stack
new PublicKey(stack, 'MyPublicKey', { encodedKey: publicKey });
// Generated caller reference: "TestStack-MyPublicKey-0cc31391"

// Test 2: Same PublicKey moved to nested construct (same construct ID)
const wrapper = new WrapperConstruct(stack, 'Wrapper');
const deepWrapper = new DeepWrapper(wrapper, 'DeepWrapper');
new PublicKey(deepWrapper, 'MyPublicKey', { encodedKey: publicKey });
// Generated caller reference: "TestStack-MyPublicKey-0cc31391" ← Same!

Key Differences from Names.uniqueId():

Approach Uses Stability Example
Names.uniqueId() Full construct path ❌ Changes when moved StackWrapperDeepWrapperMyPublicKey12AB34CD
Our implementation this.node.id only ✅ Stable when moved TestStack-MyPublicKey-0cc31391

Test Coverage

We've added comprehensive tests that specifically validate this stability:

test('stable caller reference remains same when construct is moved in tree', () => {
  // Test shows identical caller references despite different tree positions
  expect(callerReference1).toEqual(callerReference2); // ✅ Passes
});

test('node.addr changes but stable caller reference does not when construct is moved', () => {
  // Demonstrates the problem with node.addr and how we solve it
  expect(publicKey1.node.addr).not.toEqual(publicKey2.node.addr); // Different paths
  expect(stableCallerReference1).toEqual(stableCallerReference2); // Same caller ref ✅
});

Why This Approach Works

  1. this.node.id is the immediate construct identifier (e.g., "MyPublicKey") - it doesn't change when the construct is moved in the tree
  2. Stack name provides uniqueness across different stacks
  3. Account/region ensures uniqueness across environments
  4. Hash suffix handles edge cases and length limits

The caller reference remains stable during refactoring because it's based on the construct's identity (its ID within the stack) rather than its location (its path in the tree).

Let me know if there's any other concern not addressed.

@pahud pahud requested a review from leonmk-aws August 26, 2025 18:24
Copy link
Contributor

@leonmk-aws leonmk-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately having the feature flag on makes this stack (that deploys fine with the FF off) impossible to deploy :

Invalid request provided: AWS::CloudFront::PublicKey: The caller reference that you are using to create a CloudFront public key is associated with another public key in your account.

    const key = new cloudfront.PublicKey(this, 'TestPublicKey1', {
        encodedKey: publicKey,
        publicKeyName: 'test-public-key-1',
        comment: 'Test public key for reproduction'
      });
    console.log("FF Enabled:", FeatureFlags.of(this).isEnabled(cdk.cx_api.CLOUDFRONT_STABLE_PUBLIC_KEY_CALLER_REFERENCE));

    const wrapper = new Construct(this, 'Wrapper'); 
    const key2 = new cloudfront.PublicKey(wrapper, 'TestPublicKey1', {
        encodedKey: publicKey,
        publicKeyName: 'test-public-key-2',
        comment: 'Test public key for reproduction'
    });

This is because of the way the caller reference is being built in the implementation: all the stableComponents are the same for key and key2. On possible way to move forward would be to use a combination of publicKeyName and the node id. This is because the key name has to be unique in Cloudfront. Unfortunately this would only work when the key name is set by the user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug This issue is a bug. contribution/core This is a PR that came from AWS. effort/small Small work item – less than a day of effort p2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(CloudFront): Initial Create Succeeds, Subsequent Updates Fail with Invalid request provided: AWS::CloudFront::PublicKey

3 participants