Nikolas Knickrehm

3 min read

CloudWatch Monitoring Dashboards in Serverless

Infrastructure as Code

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.

CloudWatch Monitoring Dashboards in 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:

  1. 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.
  2. 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!

Next up

Want to stay up to date?