Skip to content

Instantly share code, notes, and snippets.

@yukiarrr
Created March 14, 2021 19:04
Show Gist options
  • Save yukiarrr/ca846d599d166c750bff5e8c8ac986f8 to your computer and use it in GitHub Desktop.
Save yukiarrr/ca846d599d166c750bff5e8c8ac986f8 to your computer and use it in GitHub Desktop.
Check NestedStack diffs
#!/bin/bash -e
cdk synth > /dev/null
color=${1:-'--color'}
for stack_name in ${YOUR_PARENT_STACK_NAMES}; do
resources=$(aws cloudformation list-stack-resources --stack-name ${stack_name})
length=$(echo ${resources} | jq '.StackResourceSummaries | length')
for i in $(seq 0 $(expr ${length} - 1)); do
summaries=$(echo ${resources} | jq ".StackResourceSummaries[${i}]")
[ "$(echo ${summaries} | jq -r '.ResourceType')" != 'AWS::CloudFormation::Stack' ] && continue
aws cloudformation get-template --stack-name $(echo ${summaries} | jq -r '.PhysicalResourceId') | jq '.TemplateBody' > template.json
resource_id=$(echo ${summaries} | jq -r '.LogicalResourceId')
stack_id=$(echo ${resource_id} | sed -e 's/NestedStack.*//')
file=$(find . -name "${stack_name}${stack_id}*.nested.template.json")
diff=$(json-diff ${color} template.json ${file} || true)
[ "${diff}" ] || continue
echo '------------------------------------------------------------------'
echo ${resource_id}
echo '------------------------------------------------------------------'
echo -e "${diff}"
done
done
rm -f template.json
@yukiarrr
Copy link
Author

Note

  • Install jq and json-diff
  • Set YOUR_PARENT_STACK_NAMES to the parent stack names

Example

Diff

...

 export class CdkWorkshopStack extends cdk.Stack {
   constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
     super(scope, id, props);

     new SqsStack(this, "CdkWorkshopSqsStack");
     new SnsStack(this, "CdkWorkshopSnsStack");
   }
 }

 class SqsStack extends cdk.NestedStack {
   constructor(scope: cdk.Construct, id: string, props?: cdk.NestedStackProps) {
     super(scope, id, props);

     new sqs.Queue(this, "CdkWorkshopQueue", {
-      visibilityTimeout: cdk.Duration.seconds(300),
+      visibilityTimeout: cdk.Duration.seconds(100),
     });
   }
 }

...

Check

 $ YOUR_PARENT_STACK_NAMES='CdkWorkshopStack' ./nested-stack-diff.sh
 ------------------------------------------------------------------
 CdkWorkshopSqsStack...
 ------------------------------------------------------------------
 {
   Resources: {
     CdkWorkshopQueue50D9D426: {
       Properties: {
-        VisibilityTimeout: 300
+        VisibilityTimeout: 100
       }
     }
   }
 }

@damonmcminn
Copy link

Many thanks @yukiarrr

For anyone wanting the above translated into JavaScript - I use it as an NPM script with my project.

const { CloudFormation } = require("aws-sdk");
const glob = require("glob");
const path = require("path");
const fs = require("fs");
const jsonDiff = require("json-diff");

const ROOT_STACK_NAME = "foo";

(async () => {
  const cfn = new CloudFormation();

  const synthed = glob.sync(
    path.join(__dirname, "cdk.out", `${ROOT_STACK_NAME}*.nested.template.json`)
  );

  const summaries = await cfn
    .listStackResources({ StackName: ROOT_STACK_NAME })
    .promise()
    .then(r => r.StackResourceSummaries);

  const sleep = seconds =>
    new Promise(resolve => {
      setTimeout(resolve, seconds * 1000);
    });

  const nestedStacks = await Promise.all(
    summaries
      .filter(summary => summary.ResourceType === "AWS::CloudFormation::Stack")
      .map(async (stack, i) => {
        // prevent throttling
        await sleep(i / 2);

        const name = stack.LogicalResourceId.split("NestedStack").shift();
        const pending = synthed.find(t =>
          t.split(ROOT_STACK_NAME).pop().startsWith(name)
        );

        return {
          name,
          template: {
            pending: fs.readFileSync(pending).toString(),
            deployed: await cfn
              .getTemplate({ StackName: stack.PhysicalResourceId })
              .promise()
              .then(r => r.TemplateBody),
          },
          diff() {
            const pending = JSON.parse(this.template.pending);
            const deployed = JSON.parse(this.template.deployed);

            return jsonDiff.diffString(deployed, pending);
          },
        };
      })
  );

  for (const stack of nestedStacks) {
    const diff = stack.diff();

    if (diff.length > 0) {
      console.log("---------------------------------------------");
      console.log("Stack:", stack.name);
      console.log("---------------------------------------------");
      console.log(diff);
    }
  }
})().catch(console.error);

@jesperalmstrom
Copy link

Thanks for the inspiration! Did have a need for a Python version.

import boto3 
import json
import glob
import sys
from subprocess import run
import difflib

try:
    from colorama import Fore, Back, Style, init
    init()
except ImportError:  # fallback so that the imported classes always exist
    class ColorFallback():
        __getattr__ = lambda self, name: ''
    Fore = Back = Style = ColorFallback()

def color_diff(diff):
    for line in diff:
        if line.startswith('+'):
            yield Fore.GREEN + line + Fore.RESET
        elif line.startswith('-'):
            yield Fore.RED + line + Fore.RESET
        elif line.startswith('^'):
            yield Fore.BLUE + line + Fore.RESET
        else:
            yield line

def confirm(msg="Do you want to continue?"):
    return input(f"{msg} ").lower() in ('yes', 'y')

def cdk_diff_nested_stack(stack_name):
    """
    Colorfull diff between local changes to CDK and what is deployed
    """
    cf_client = boto3.client("cloudformation")
    responce = cf_client.list_stack_resources(StackName=stack_name)
    resource_summeries = responce['StackResourceSummaries']
    for resource in resource_summeries:
        if 'AWS::CloudFormation::Stack' in resource['ResourceType']:
            nested_stack_name_arn = resource['PhysicalResourceId']
            nested_response = cf_client.get_template(StackName=nested_stack_name_arn)
            template = nested_response['TemplateBody']
            nested_stack_name = resource['LogicalResourceId'].split('NestedStack')[0]
            print_stack_name = f'###  {nested_stack_name}  ###'
            print(print_stack_name)
            print('#'*len(print_stack_name))
            nested_files = glob.glob(f'./cdk.out/*{nested_stack_name}*.nested.template.json')
            nested_file = [ns for ns in nested_files if 'Provider' not in ns][0]
            template_json = json.dumps(template, indent=2)
            json_file = open(nested_file)
            txt = json_file.read()
            json_file.close()
            #print(txt)
            diff = difflib.ndiff(template_json.split('\n'), txt.split('\n'))
            diff = color_diff(diff)
            print('\n'.join(diff))
            print('---')
            if not confirm(msg='Continue with next stack diff?'):
                sys.exit(1)
            print('---')


if __name__ == "__main__":
    root_stack_name = "RootCdkStackName"
    run(['cdk', 'synth', '--quiet'])
    cdk_diff_nested_stack(root_stack_name)

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