CDK Toolkit diff

Last year (May 2025) AWS released the AWS CDK Toolkit Library as generally available. With this they exposed programmatic access to some of the core AWS CDK functionality. When this came out I was primarily interested in using the diff tool to determine how many stack disruptions would be present in a change. At ${dayjob} we had something get stuck in preproduction because of a destructive change and a co-worker said it would be nice to have a tool letting you know how many resources would be replaced or destroyed. I started to work on something and then never went back to it… until now!

Let’s take a very simple stack with just a Nodejs function. It does nothing but this stack will deploy.

import { App, Stack, StackProps } from "aws-cdk-lib";
import { Code } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export class TestStack extends Stack {
  constructor(scope: App, id: string, props: StackProps) {
    super(scope, id, props);

    new NodejsFunction(this, "TestDiffFunction", {
      code: Code.fromInline("console.log(123)"),
      handler: "handler",
    });
  }
}

If you deploy this, comment out the function, and run npx cdk diff you’ll get an output that looks like this

~cdk-diff % npx cdk diff
start: Building StackOne Template
success: Built StackOne Template
start: Publishing StackOne Template (xxxx-us-east-1-xxxx)
success: Published StackOne Template (xxxx-us-east-1-xxxx)
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)

Stack StackOne
IAM Statement Changes
┌───┬────────────────┬────────┬────────────────┬──────────────────┬───────────┐
│   │ Resource       │ Effect │ Action         │ Principal        │ Condition │
├───┼────────────────┼────────┼────────────────┼──────────────────┼───────────┤
│ - │ ${TestDiffFunc │ Allow  │ sts:AssumeRole │ Service:lambda.a │           │
│   │ tion/ServiceRo │        │                │ mazonaws.com     │           │
│   │ le.Arn}        │        │                │                  │           │
└───┴────────────────┴────────┴────────────────┴──────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────┬────────────────────────────────────┐
│   │ Resource                           │ Managed Policy ARN                 │
├───┼────────────────────────────────────┼────────────────────────────────────┤
│ - │ ${TestDiffFunction/ServiceRole}    │ arn:${AWS::Partition}:iam::aws:pol │
│   │                                    │ icy/service-role/AWSLambdaBasicExe │
│   │                                    │ cutionRole                         │
└───┴────────────────────────────────────┴────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::IAM::Role TestDiffFunction/ServiceRole TestDiffFunctionServiceRoleE5C10C59 destroy
[-] AWS::Lambda::Function TestDiffFunction TestDiffFunctionC0528C8B destroy

This output is a bit of a mess to parse programmatically. If you had other changes beyond something so trivial it would all be mixed together making it even more difficult. You could dump it to an LLM to check every time but with the CDK Toolkit we have a “simpler” option.

The CDK Toolkit gives us access to the diff function and its output in a structured, typed way. Here is a simple version with no frills:

import { DiffMethod, Toolkit } from "@aws-cdk/toolkit-lib";
import { ResourceImpact } from "@aws-cdk/cloudformation-diff";
import * as path from "path";

const cdkOutDir = path.join(__dirname, "..", "cdk.out");

void main();

interface Report {
  stackName: string;
  diffCount: number;
  logicalIds: string[];
}

async function main() {
  const toolkit = new Toolkit({
    color: false,
    emojis: false,

    // disables stdout so that we don't get the noisy default diff
    ioHost: {
      notify: async () => {},
      requestResponse: async (msg) => msg.defaultResponse,
    },
  });

  const assembly = await toolkit.fromAssemblyDirectory(cdkOutDir);

  const diff = await toolkit.diff(assembly, {
    method: DiffMethod.ChangeSet({
      fallbackToTemplate: false,
    }),
  });

  const report: Report[] = [];
  for (const [stackName, stackDiff] of Object.entries(diff)) {
    const removals = stackDiff.resources.filter(
      (diff) =>
        diff?.changeImpact === ResourceImpact.WILL_REPLACE ||
        diff?.changeImpact === ResourceImpact.WILL_DESTROY,
    );

    if (removals.differenceCount < 1) continue;

    report.push({
      stackName,
      diffCount: removals.differenceCount,
      logicalIds: removals.logicalIds,
    });
  }

  console.log(report);
}

This will produce an output like

{
  stackName: 'StackOne',
  diffCount: 2,
  logicalIds: [ 'TestDiffFunctionServiceRoleE5C10C59', 'TestDiffFunctionC0528C8B' ]
}
{
  stackName: 'StackTwo',
  diffCount: 2,
  logicalIds: [ 'TestDiffFunctionServiceRoleE5C10C59', 'TestDiffFunctionC0528C8B' ]
}

Why would I use this?

If you compare just the simple console.log(report) to the default output above it isn’t really any better by itself. The main win is that we can now action on this. Maybe you just want to know how many replacements or destroys you have in a given PR, or specific resource types like databases, or maybe you want to block a PR if it has X% of destructive changes compared to total resources. I’ve wired this up at ${dayjob} so that we get a comment on PRs for any resource destruction or replacement. Over time we might improve it or table it but either way it was nice to finally finish this Saturday morning task.

Example

I’ve put together a sample for reference.

References