Skip to content

Conversation

@lousydropout
Copy link

@lousydropout lousydropout commented Nov 26, 2025

Issue

Partially addresses #17377 by introducing synthesis-time template transforms, fulfilling the request for a way to inspect and modify the final CloudFormation template before it is written. Deploy-time hooks remain out of scope.

Reason for this change

Right now, CDK has ways to customize constructs before synthesis and ways to validate synthesized output through metadata checks. But there is no way to inspect or update the final CloudFormation template right before it’s saved.

This capability enables use cases such as:

  • checking the final template for policy or security issues
  • adding organization-required metadata
  • catching problems that only appear after all tokens are resolved
  • making small adjustments to the template before deployment

Description of changes

This PR introduces Template Transforms, a simple feature that lets users register functions on the App that run right before each stack’s template is written out.

New APIs:

  • ITemplateTransform -- interface with transformTemplate(stack, template)
  • TemplateTransforms -- stores the transforms
  • App.addTemplateTransform() -- adds a transform to the app

Modified APIs:
None. This PR does not modify any existing API.

How it works:

  • Runs after all values are resolved
  • Runs before the template is written to cdk.out
  • Transforms run in the order they were added
  • A transform may:
    • change the template directly
    • return a replacement template
    • throw an error itself to fail synthesis
    • or add annotations (e.g. addError) for validation that allows templates to be written but causes the CLI to fail after synthesis
  • Does nothing unless a transform is added

This feature is fully opt-in and non-breaking. If a user does not register any template transforms, no objects or symbols are created unless the user registers a transform and no extra code paths run during synthesis. This is enforced by a TemplateTransforms.hasAny() check in Stack._toCloudFormation(), which completely skips the transform logic unless transforms have been explicitly added. Applications that do not use this API see zero behavioral or performance change.

Files changed:

  • template-transform.ts - new file defining the transform interfaces and registry
  • app.ts - added the addTemplateTransform() method
  • stack.ts - added an opt-in branch to invoke template transforms during synthesis
  • index.ts - added export * from "./template-transform"
  • Documentation updates

Describe any new or updated permissions being added

N/A - This feature does not introduce any new IAM permissions.

Description of how you validated changes

  • 22 unit tests checking registration, hasAny() opt-in behavior, ordering, mutation, errors, and nested stacks

  • Integration test verifying output changes

  • Manual testing using a sample CDK app with a local build of aws-cdk-lib linked into a local CDK CLI:

    • confirmed transforms run at the right time
    • confirmed template updates show up in cdk.out
    • confirmed errors and annotations behave correctly
  • yarn build and yarn rosetta:extract --strict (from within packages/aws-cdk-lib) pass

Usage example

This example demonstrates how template transforms can enforce guardrails during synthesis. The transforms perform:

  • Public ingress detection: Adds an error annotation if any Security Group allows 0.0.0.0/0 or ::/0, causing the CLI to fail after synthesis.
  • Hard-coded IAM ARN detection: Adds an error annotation if any IAM policy contains literal "arn:" values instead of token-based references.
  • Automatic execution on every stack during synthesis, using the fully resolved template.
import * as cdk from "aws-cdk-lib";
import { AppStack } from "../lib/app-stack";

const app = new cdk.App();

// Prevent security group being opened to everyone
class NoPublicIngress implements cdk.ITemplateTransform {
  transformTemplate(stack: cdk.Stack, template: any): void {
    console.log("[NoPublicIngress] Reviewing security group practices")
    const resources = template.Resources ?? {};
  
    for (const logicalId of Object.keys(resources)) {
      const resource: any = resources[logicalId];
  
      if (resource.Type !== "AWS::EC2::SecurityGroup") continue;
  
      const ingress = resource.Properties?.SecurityGroupIngress ?? [];
      
      for (const rule of ingress) {
        if (rule.CidrIp === "0.0.0.0/0" || rule.CidrIpv6 === "::/0") {
          // Add an error annotation: this will allow the template to be written,
          // but the CLI will fail after synthesis and block deployment
          cdk.Annotations.of(stack).addError(`Security Group ${logicalId} in stack ${stack.stackName} allows public ingress (${cidr4 ?? cidr6}).`);
        }
      }
    }
  }
}

// Prevent unsafe IAM policies that use hard-coded ARNs instead of dynamic references
class BlockHardCodedIamArns implements cdk.ITemplateTransform {
  public transformTemplate(stack: cdk.Stack, template: any): void {
    console.log("[BlockHardCodedIamArns] Reviewing IAM references")
    const resources = template.Resources || {};

    for (const logicalId of Object.keys(resources)) {
      const resource = resources[logicalId];

      if (resource.Type === "AWS::IAM::Policy") {
        const document = resource.Properties?.PolicyDocument;
        if (document) {
          // Scan policy for hard-coded ARN values.
          // Tokens are resolved at this point, so any string starting with "arn:" is literal.
          const json = JSON.stringify(document);
          if (json.indexOf("arn:") !== -1) {
            // Add an error annotation: this will allow the template to be written,
            // but the CLI will fail after synthesis and block deployment
            cdk.Annotations.of(stack).addError(`IAM Policy ${logicalId} contains a hard-coded ARN. Use Fn.importValue, Ref, or GetAtt instead.`);
          }
        }
      }
    }
  }
}


// Add the compliance checks
app.addTemplateTransform(new NoPublicIngress());
app.addTemplateTransform(new BlockHardCodedIamArns());

new AppStack(app, "AppStack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region:  process.env.CDK_DEFAULT_REGION,
  },
});

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.

…odification

Add an opt-in API that allows users to inspect or modify the final
CloudFormation template during synthesis, after token resolution but
before the template is written to disk.

- Add ITemplateTransform interface and TemplateTransforms class
- Add App.addTemplateTransform() convenience method
- Invoke transforms in Stack._toCloudFormation() after token resolution
- Export from core module

Transforms can validate templates, inject metadata, modify resources,
or fail synthesis by throwing errors. Multiple transforms execute in
registration order (FIFO).
Add TemplateTransforms.hasAny() static method that checks if any
transforms exist without creating the singleton. Update stack synthesis
to skip transform processing entirely when no transforms are registered.

Previously, every stack synthesis would create a TemplateTransforms
singleton and iterate over its array, even when empty. Now there is
zero overhead for users who don't use the feature.
- Add Template Transforms section to aws-cdk-lib README with examples
- Simplify JSDoc examples in app.ts to be jsii-compatible (use classes)
- Add ITemplateTransform to rosetta default.ts-fixture for example compilation
- Add integration test for template transform feature
…shots

- Replace simple MetadataInjector example with BlockHardCodedIamArns
- Example demonstrates practical security use case: blocking hard-coded ARNs
- Uses Annotations.of(stack).addError() for Rosetta/jsii compatibility
- Add missing integration test snapshot files
@aws-cdk-automation aws-cdk-automation requested a review from a team November 26, 2025 02:23
@github-actions github-actions bot added beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK effort/large Large work item – several weeks of effort feature-request A feature should be added or improved. p1 labels Nov 26, 2025
@lousydropout lousydropout marked this pull request as ready for review November 26, 2025 04:17
@lousydropout lousydropout requested a review from a team as a code owner November 26, 2025 04:17
@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Nov 26, 2025
@lousydropout lousydropout changed the title feat(core): add Template Transform API for post-resolution template modification Feature: Template Transform API for post-resolution template modification Nov 26, 2025
@lousydropout lousydropout changed the title Feature: Template Transform API for post-resolution template modification feat(core): Template Transform API for post-resolution template modification Nov 26, 2025
Copy link
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

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

(This review is outdated)

@aws-cdk-automation aws-cdk-automation removed the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Nov 26, 2025
@lousydropout lousydropout changed the title feat(core): Template Transform API for post-resolution template modification feat(core): add Template Transform API for post-resolution template modification Nov 26, 2025
@aws-cdk-automation aws-cdk-automation dismissed their stale review November 26, 2025 15:20

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Nov 26, 2025
Update nested stacks test to verify specific stacks are processed
with correct nested property values, rather than just checking that
some stacks exist with those values.
@aws-cdk-automation aws-cdk-automation added the pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. label Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK effort/large Large work item – several weeks of effort feature-request A feature should be added or improved. p1 pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. pr/needs-maintainer-review This PR needs a review from a Core Team Member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants