CloudWatch Dashboards as Code (the Right Way) Using AWS CDK
CloudWatch dashboards are very handy when it comes to having an overview of your AWS infrastructure in real time. Like most AWS services, we typically start creating CloudWatch dashboards by hand. This approach is good for experimenting, but when it comes to delivering infrastructure in production, you’ll often want to do things in a repeatable, peer-reviewed manner using infrastructure as code. If you’re thinking of infrastructure as code in the AWS world, you’re probably thinking of CloudFormation.
CloudWatch Dashboards in CloudFormation: Not the Prettiest Thing
CloudWatch Dashboards are supported in CloudFormation, which is a good start. Not all AWS services are this lucky (poor Elastic Transcoder). However, when you open up the docs for CloudWatch dashboards in CloudFormation, you are in for an unpleasant surprise.
Consider for a moment the syntax for a CloudWatch Dashboard in CloudFormation:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: String
DashboardBody: String
Here, DashboardName
is inoffensive. However, DashboardBody
is problematic:
DashboardBodyA JSON string that defines the widgets contained in the dashboard and their location. For information about how to format this string, see Dashboard Body Structure and Syntax.Required: YesType: String
You read that right. The DashboardBody
property is a JSON string that has to contain all the widgets in your dashboard. Here’s an example in YAML (taken from the AWS docs):
BasicDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: Dashboard1
DashboardBody: '{“widgets”:[{“type”:”metric”,”x”:0,”y”:0,”width”:12,”height”:6,”properties”:{“metrics”:[[“AWS/EC2”,”CPUUtilization”,”InstanceId”,”i-012345"]],”period”:300,”stat”:”Average”,”region”:”us-east-1",”title”:”EC2 Instance CPU”}},{“type”:”text”,”x”:0,”y”:7,”width”:3,”height”:3,”properties”:{“markdown”:”Hello world”}}]}'
Note how you have to declare the X and Y coordinates for every widget, as well as the width and height, and put all this in a string. If you are using JSON in CloudFormation, then you are out of luck, since you’ll have to escape every double quote in there:
{
"BasicDashboard": {
"Type": "AWS::CloudWatch::Dashboard",
"Properties": {
"DashboardName": "Dashboard1",
"DashboardBody": "{\"widgets\":[{\"type\":\"metric\",\"x\":0,\"y\":0,\"width\":12,\"height\":6,\"properties\":{\"metrics\":[[\"AWS/EC2\",\"CPUUtilization\",\"InstanceId\",\"i-012345\"]],\"period\":300,\"stat\":\"Average\",\"region\":\"us-east-1\",\"title\":\"EC2 Instance CPU\"}},{\"type\":\"text\",\"x\":0,\"y\":7,\"width\":3,\"height\":3,\"properties\":{\"markdown\":\"Hello world\"}}]}"
}
}
}
Defining your CloudWatch dashboards this way, by hand, has a few downsides:
- It’s hard to make a mental image of the dashboard simply by looking at coordinates and dimensions.
- Anytime you want to resize or move a widget, you will need to take out your pixel ruler and recalculate a bunch of X, Y, width and height values.
- If you’re using JSON templates, all this definition is going to be on a single line, making code reviews even harder.
Now, you can export your CloudWatch Dashboards' source code using the AWS console. You could in practice do the modifications to your dashboard by hand and once you’re satisfied with it, export the dashboard’s config from the console, and paste that config in your CloudFormation template. This approach still has a few shortcomings:
- Still very hard to code review. Your peers will have to open up the CloudWatch console to look at your dashboard to understand what’s going on.
- Does not respect infrastructure as code best practices: in time, someone will forget to update the dashboard source in your version control, and your versioned CloudFormation template will have drifted from the actual infrastructure.
Fortunately, there’s another solution.
Our Friend the AWS CDK
The AWS Cloud Development Kit (AWS CDK) is a new tool that helps you provision your AWS infrastructure using “normal” programming languages. There are similar tools out there, most notably Troposphere. The CDK differs from existing offerings in a few key manners:
- You can code your infrastructure using a variety of programming languages. At the moment, Java, .NET, JS and TypeScript are supported, with Python and Ruby in the works.
- It provides high-level abstractions that hide away a lot of complexity. For example, you can generate CloudFormation code for a whole VPC using very few code.
- It also includes tooling to provision your CloudFormation stacks and deploy code bundles, such as Lambda function packages or Docker images.
Recently, AWS have started including their CDK documentation in their main website (outside of Github). This is a testament to the trust AWS has in this project. It seems like in time, the CDK will be treated as a first-class citizen in the AWS tooling world, like AWS SAM does.
Now, back to the CloudWatch Dashboard situation. AWS CDK has a CloudWatch package that contains a layout system to create your dashboards. I find this layout feature to be analogous to grid systems we find in frontend frameworks such as Bootstrap.
Using the Layout System
Here at Poka, we have a Redshift cluster that we use for various analytics needs. To add visibility to the Redshift cluster, I wanted to create a dashboard as displayed in this project: https://github.com/awslabs/amazon-redshift-monitoring
As you can see, there are over 10 different widgets in this dashboard. I did not want to have to set every widget’s positions and dimensions by hand. I decided to try AWS CDK’s CloudWatch module to help me.
Using the CDK’s layout system, you can define CloudWatch dashboards using high-level abstractions such as Columns
or Rows
. Using a Dashboard
object, each successive call to Dashboard.add(widget, widget, …)
will create a row of widgets in your dashboard. The number of columns on that row is adjusted dynamically based on the number of widgets in the .add()
call
In the following example, I’ll be using TypeScript, as it seems to be the project’s authors language of choice. Let’s have a look at the resulting code:
import cdk = require('@aws-cdk/cdk');
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import {GraphWidget, Metric} from "@aws-cdk/aws-cloudwatch";export class CdkAppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props); const dashboard = new cloudwatch.Dashboard(this, 'RedshiftMonitoringDashboard', {dashboardName: 'RedshiftMonitoringDashboard'}); this.node.requireContext('env');
const env = this.node.getContext('env'); dashboard.add(
this.buildRedshiftWidget(env, 'AverageQueryTime'),
this.buildRedshiftWidget(env, 'AvgSkewRatio'),
this.buildRedshiftWidget(env, 'MaxSkewRatio'),
this.buildRedshiftWidget(env, 'ColumnsNotCompressed'),
); dashboard.add(
this.buildRedshiftWidget(env, 'AvgCommitQueueTime'),
this.buildRedshiftWidget(env, 'AvgSkewSortRatio'),
this.buildRedshiftWidget(env, 'MaxSkewSortRatio'),
this.buildRedshiftWidget(env, 'DiskBasedQueries'),
); dashboard.add(
this.buildRedshiftWidget(env, 'MaxUnsorted'),
this.buildRedshiftWidget(env, 'MaxVarcharSize'),
this.buildRedshiftWidget(env, 'TotalAlerts'),
this.buildRedshiftWidget(env, 'QueriesScanNoSort'),
); dashboard.add(
this.buildRedshiftWidget(env, 'Tables'),
this.buildRedshiftWidget(env, 'TablesNotCompressed'),
this.buildRedshiftWidget(env, 'TablesStatsOff'),
this.buildRedshiftWidget(env, 'Rows'),
); dashboard.add(
this.buildRedshiftWidget(env, 'QueriesWithHighTraffic'),
this.buildRedshiftWidget(env, 'Packets'),
this.buildRedshiftWidget(env, 'TotalWLMQueueTime'),
);
} buildRedshiftWidget(env: string, metricName: string, statistic: string = 'avg'): GraphWidget {
return new GraphWidget({
title: metricName,
left: [new Metric({
namespace: 'Redshift',
metricName: metricName,
dimensions: {
ClusterIdentifier: cdk.Fn.importValue(`${env}-PokaAnalyticsRedshift:ClusterName`)
},
statistic: statistic
})]
})
}
}
In the code above, you can see 5 calls to dashboard.add()
. Each of these 5 calls creates a single row in the dashboard. For each widget instance in the .add()
call, a column is created in the dashboard. We can easily see that the dashboard will:
- Contain 5 rows
- The first 4 rows will contain 4 widgets each
- The last row will contain only 3 widgets
This information would have been hard to uncover using CloudFormation’s native implementation of a Dashboard
.
I also added a buildRedshiftWidget()
function that further helps me reuse some code and hide away the details about the Redshift cluster.
Changing the layout is also simple. To reorder rows, simply change the order of dashboard.add(widget, widget, …)
calls. To change column ordering, you’ll need to move the widget
arguments in the dashboard.add(widget, widget, …)
calls. Pretty intuitive.
Below, you can see a short excerpt of the CloudFormation code that was generated by the CDK:
Resources:
RedshiftMonitoringDashboard1499EBB5:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardBody:
Fn::Join:
- ""
- - '{"widgets":[{"type":"metric","width":6,"height":6,"x":0,"y":0,"properties":{"view":"timeSeries","title":"AverageQueryTime","region":"'
- Ref: AWS::Region
- '","metrics":[["Redshift","AverageQueryTime","ClusterIdentifier","'
- Fn::ImportValue: prod-PokaAnalyticsRedshift:ClusterName
- '",{"yAxis":"left","period":300,"stat":"Average"}]],"annotations":{"horizontal":[]},"yAxis":{"left":{"min":0},"right":{"min":0}}}},{"type":"metric","width":6,"height":6,"x":6,"y":0,"properties":{"view":"timeSeries","title":"AvgSkewRatio","region":"'
- Ref: AWS::Region
- '","metrics":[["Redshift","AvgSkewRatio","ClusterIdentifier","'
- Fn::ImportValue: prod-PokaAnalyticsRedshift:ClusterName
- '",{"yAxis":"left","period":300,"stat":"Average"}]],"annotations":{"horizontal":[]},"yAxis":{"left":{"min":0},"right":{"min":0}}}},{"type":"metric","width":6,"height":6,"x":12,"y":0,"properties":{"view":"timeSeries","title":"MaxSkewRatio","region":"'// Continued ...
As you can see, the CDK took care for me to generate the X, Y coordinates and dimensions for every widget in the dashboard. All this time, the pixel ruler stayed in the drawer.
Conclusion
In this article, I showed you how the CloudWatch dashboard implementation in CloudFormation is hard to use, and how the AWS CDK brings an alternative solution to laying out your dashboards. If you’re interested in some other nuggets that come with the CDK, here are a few:
- Define Step Function state machines using code
- Auto-validate ACM certificates using DNS
- Easily bind Lambda functions to various event sources
Thanks to Caroline Maltais for the awesome graphics :)