Dynamic CDK Dashboards
Tags: aws, devops, cdk, cloudwatch
Published: 2021-09-27 12:22:40

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](https://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](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.

![CDK Lifecycle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yuv69rkl6vv066o239pk.png)

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](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html).

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.

```bash
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`.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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 Custom Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u42c0gx002phb41zywaa.png)

## 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!