CloudWatch Monitoring Dashboards in Serverless
CloudWatch monitoring dashboards come in really handy when you are operating serverless applications on AWS. After manually setting up a nice new monitoring dashboard in CloudWatch with a colleague we came up with the idea of automating the deployment of this dashboard through Serverless.
This would be really cool as every stage could then have its own monitoring dashboard automatically attached and our team could perform monitoring on every stage during development with little to no additional costs. We managed to achieve this after overcoming some unexpected problems that others might also encounter. So feel free to profit from our learnings!
In this article, I will explain and demonstrate ...
- ... how a monitoring dashboard can be deployed through the Serverless framework with no additional plugins installed
- ... how monitoring configurations can be stored in an external file to keep your serverless.yml short and clean
- ... how you can dynamically write resource names including (for example) the current stage name in the JSON configuration of your dashboards so you can have distinct dashboards for every deployment stage
After a quick assessment (Hey Google!) we achieved to deploy a simple CloudWatch dashboard by defining a AWS::CloudWatch::Dashboard
resource in the Resources
scope of our serverless.yml which CloudFormation can interpret when we deploy our serverless application. To ensure that we have a dedicated dashboard for every deployment stage, we included the stage name in the DashboardName
field. How the dashboard will load the metrics of the actual resources deployed on the same stage will be addressed later in this article.
resources:
SomeMonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: My-First-Dashboard-${self:provider.stage}
DashboardBody: `
{
"widgets": [
{
"type": "text",
"x": 0,
"y": 0,
"width": 6,
"height": 1,
"properties": {
"markdown": "\n# Hello World!\n"
}
}
]
}`
While this setup works even for bigger and more elaborate monitoring dashboards (like the one we have created 😎) putting the whole dashboard configuration in your serverless.yml
is not a very good idea as this would likely blow up the size of this file.
Therefore, we tried to move the JSON configuration of our beautiful dashboard into a new JSON file that is loaded into the main serverless.yml
via the ${file()}
command. This however was much harder to achieve than we expected because the DashboardBody
field expects a string value and ${file()}
will the content of a JSON file as YAML. Even wrapping the ${file()}
command in quotation marks or storing the JSON in a plain .txt
file does not prevent this from happening.
resources:
SomeMonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: My-First-Dashboard-${self:provider.stage}
DashboardBody: `${file(./dashboard.json)}`
Serverless Error ---------------------------------------
An error occurred: SomeMonitoringDashboard - Property validation failure: [Value of property {/DashboardBody} does not match type {String}].
Luckily ${file()}
has some hidden superpowers as it can reference a JavaScript file and call exported function of it. If such function returns a string Serverless will interpret it correctly. So our workaround is to store the JSON configuration in a JS file instead and export a stringified version of it through module.exports
.
const dashboard = {
widgets: [
{
type: 'text',
x: 0,
y: 0,
width: 6,
height: 1,
properties: {
markdown: '\n# Hello World!\n'
},
},
],
}
module.exports.toString = () => JSON.stringify(dashboard)
This allows us to load the external configuration for our CloudWatch monitoring dashboard into the main serverless.yml
which in return enables Serverless to create the dashboard for us through CloudFormation.
resources:
SomeMonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: My-First-Dashboard-${self:provider.stage}
DashboardBody: ${file(./dashboard.js):toString}
Lastly, our monitoring dashboard was supposed to show metrics and logs of multiple Lambdas and SQS queues, which we deploy on multiple stages. Obviously, the monitoring dashboard for a certain stage should display metrics of the infrastructure that is deployed on the same stage.
Unfortunately, we did not find a solution for accessing the resource names through CloudFormation helper functions like !Ref
or !GetAtt
so we identified two possible solutions to do it some other way:
- We could write the resource names directly in the configuration of our monitoring dashboard while only setting the stage part of them through
${self:provider.stage}
. This is possible as CloudFormation generates most resource names in a predictable way. - We could introduce new variables storing the names of each resource we will monitor on our dashboard and manually set the names of the resources in our
serverless.yml
and the configuration JSON for the monitoring dashboard.
In the end, we chose the latter of both options because this would allow us to change the names of those resources later without needing to also modify the dashboard configuration.
By the end of the day our configuration looked somewhat like this (I can not share the fancy dashboard we have created, but you will get the idea):
# serverless.yml
service: our-fancy-app
provider:
name: aws
region: eu-central-1
stage: ${opt:stage, 'dev'}
# ...
custom:
someLambdaFunctionName: ${self:service.name}-${self:provider.stage}-someLambdaFunction
# ...
functions:
someLambdaFunction:
name: ${self:custom.someLambdaFunctionName}
# ...
resources:
SomeMonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: ${self:service.name}-${self:provider.stage}
DashboardBody: ${file(./dashboard.js):toString}
// dashboard.js
const dashboard = {
widgets: [
{
type: 'metric',
x: 0,
y: 1,
width: 3,
height: 3,
properties: {
metrics: [
['AWS/Lambda', 'Errors', 'FunctionName', '${self:custom.someLambdaFunctionName}', { label: '#' }],
],
view: 'singleValue',
region: 'eu-central-1',
stat: 'Sum',
period: 3600,
stacked: true,
title: 'Errors',
setPeriodToTimeRange: true,
},
},
],
}
module.exports.toString = () => JSON.stringify(dashboard)
Thanks for reading this article and happy monitoring!