hero-image

Dynamic CDK Dashboards

10 Minutes

09/24/2021

One of the CDK's greatest strengths is the ability to create a library of components to be shared across your company, or shared with the world on constructs.dev. But how do you create a construct based on other constructs in the same stack without forcing the developer to call a method for each one? That's exactly what aspects allow you to do.

You can find the final source for this tutorial at https://github.com/theBenForce/dynamo-dashboard

What are Aspects?

The CDK uses a four step process to convert your code into a CloudFormation template: construct, prepare, validate, and synthesize. Since aspects are used during the second step, you need to understand how the first two steps are related.

When you run a CDK command, it starts by executing your code. Usually this is some file in the project's bin/ directory where your App is created. This will create all of the constructs that you've added to your project, keeping only the level 1, or Cfn* constructs for the next step.

The prepare step is where aspects are used. Each scope has a set of aspects registered to it during the construct phase. The CDK travels down the construct tree and if it finds a registered aspect it will pass in all of the constructs in the current scope and any child scopes.

Creating the Dashboard Construct

If you don't have the CDK installed, you can find instructions in the getting started guide.

Now that you've seen aspects work behind the scenes, let's create one of our own. We'll create a CloudWatch dashboard construct that dynamically adds widgets for every DynamoDB table in the construct's scope. Start by creating a skeleton project by running the following commands.

mkdir dynamo-dashboard
cd dynamo-dashboard
cdk init app --language typescript

Create the Construct

First, create a new construct called DynamoDashboard. Create the class in lib/dynamoDashboard.ts and make sure it implements IAspect.

import * as cdk from "@aws-cdk/core";
import * as cw from "@aws-cdk/aws-cloudwatch";
import * as cw from "@aws-cdk/aws-dynamodb";

export class DynamoDashboard extends cdk.Construct implements cdk.IAspect {
	visit(node: cdk.IConstruct): void {
	}
}

Next, you'll need to add some properties to store the dashboard's state between calls to its visit method. The first property is a reference to the CloudWatch dashboard that will be updated. The second property will store all of the widgets that you create.

private dashboard: cw.CfnDashboard;
widgets: Array<Widget> = [];

Finally, add a constructor. It should register itself as an Aspect in its scope, and create the dashboard construct.

constructor(scope: cdk.Construct, id: string) {
	super(scope, id);
	
	// Register this as an aspect
  cdk.Aspects.of(scope).add(this);

	this.dashboard = new cw.CfnDashboard(this, `Dashboard`, {
    dashboardBody: ""
  });
}

Implementing visit()

Adding a widget to the dashboard requires three steps: find a free spot, add a widget object to the widgets array, and update the dashboard body definition. Use following method to figure out where the next available spot is. You can change the width/height values, but keep in mind that the dashboard is 24 units wide.

getNextPosition(): {x: number; y: number; width: number; height: number;} {
	let x = 0;
  let y = 0;

  if (this.widgets.length > 0) {
    const lastWidget = this.widgets[this.widgets.length - 1];
    x = lastWidget.x + lastWidget.width;
    y = lastWidget.y;

    if (x >= DASHBOARD_WIDTH - 1) {
      x = 0;
      y += this.props.widgetHeight + 1;
    }
  }

	return {x, y, width: 12, height: 6};
}

Now you can implement the actual visit method. The first thing you need to do is make sure the node is a DynamoDB table. Since every construct passed into the visit method will be level 1, you will be checking if the node is a CfnTable instance.

visit(node: cdk.IConstruct): void {
	if(node instanceof dynamodb.CfnTable) {
	}
}

If the node is a table instance, you can create a new widget and update the dashboard.

visit(node: cdk.IConstruct): void {
  if (node instanceof dynamo.CfnTable) {
    const position = this.getNextPosition();

    this.widgets.push({
		  ...position,
		  type: "metric",
		  properties: {
		    metrics: [
		      [
		        "AWS/DynamoDB",
		        "ConsumedReadCapacityUnits",
		        "TableName",
		        node.ref,
		        { label: "Read (max)" },
		      ],
		      [".", "ConsumedWriteCapacityUnits", ".", ".", { label: "Write (max)" }],
		    ],
		    view: "timeSeries",
		    stacked: false,
		    region: node.stack.region,
		    title: node.logicalId,
		    stat: "Maximum",
		    period: 300,
		    liveData: true,
		  },
		});

    this.dashboard.dashboardBody = JSON.stringify({ widgets: this.widgets });
  }
}

Testing It

Now comes the fun part, it's time to test your component. Go to your project's default stack and create an instance of the component. Next, add a couple DynamoDB tables. Even though your visit method is looking for CfnTable instances you can use the Table class since it creates a CfnTable instance in its constructor.

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

    new DynamoDashboard(this, `DynamoDashboard`);

    new dynamo.Table(this, `FirstTable`, {
      partitionKey: {
        name: "pk",
        type: dynamo.AttributeType.STRING,
      },
    });

    new dynamo.Table(this, `SecondTable`, {
      partitionKey: {
        name: "pk",
        type: dynamo.AttributeType.STRING,
      },
    });
  }
}

Everything should be good to go at this point, run cdk deploy to send your stack to the cloud. Once it's done deploying, open up CloudWatch and find your dashboard. Now enjoy the fact that you don't have to manually add every table to it!

Your generated dashboard

Conclusion

In this article you've seen what CDK aspects are and how to use them to dynamically update resources. I haven't seen many other examples of aspects, but hopefully you'll be inspired to create something awesome and share it!