Serverless Site Health Check Notification System
Tags: aws, serverless, lambda, eventdriven
Published: 2023-12-23 04:12:20

I have been spending lots of time the last few years working on projects in cloud environments. Most of my adventures involve AWS but i have spent time with all the major clouds and have tried out many of the components they provide. There are so many tools available out there that in many cases it is just a matter of piecing together the best components available to solve whatever your current problem is.

When you have deployed a new app or site in the cloud the focus usually shifts to monitoring and maintenance. One of the core parts you need to keep an eye on is just making sure your site is reachable to your users at all times.

On AWS there are many ways you can do this but one approach i like is to setup [Health Checks](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/health-checks-creating-values.html#health-checks-creating-values-endpoint) in the Route 53 area. One misconception about these checks is that you have to use Route 53 for your host DNS or you have to have everything in Route53 to use the Health Check feature. This is not the case. You can use this feature to monitor ANY endpoint (even ones you don't control) via HTTP, HTTPS, or TCP and specify the host by hostname or IP address.

This blog details how you can use some key serverless components from AWS like [Amazon Eventbridge](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html), [AWS Lambda](https://aws.amazon.com/lambda/), and [Simple Notification Service](https://aws.amazon.com/sns/) to setup a system that will monitor your site (which can be running anywhere) and send emails, text messages, slack messages, and more when the reachability status of your site changes.


## Serverless Application Model (SAM)
I'm a big fan of using an Infrastructure as Code (IaC) approach for any project. My go to tools for this are the [Servlerless Application Model](https://aws.amazon.com/serverless/sam/) (SAM) and it's associated CLI ([SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)). For more official use cases and for cross platform apps I typically use Terraform.

The setup for this project will be done using SAM and the associated project sample code found in this [Github repository](https://github.com/RDarrylR/route53-health-check-sam) uses SAM. Of course all of the components described here can be setup using the AWS console, AWS CLI, or with many other approaches.

The SAM template for this project can be found in the [Github repository](https://github.com/RDarrylR/route53-health-check-sam) but here is the template.yaml file.

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  route53-health-check-sam

  SAM Template for route53-health-check-sam
    - Route53 Health Check
    - Health Check Cloudwatch Alarm
    - Lambda Function to run when alarm state changes
    - SNS Topic to send updates on health check changes to

Globals:
  Function:
    Timeout: 3
    MemorySize: 128
    Tracing: Active
    LoggingConfig:
      LogFormat: JSON
  Api:
    TracingEnabled: true

Parameters:
  Hostname:
    Type: String
    Description: Hostname to monitor   
    Default: www.amazon.com
  SlackWebhookURL:
    Type: String
    Description: URL to publish slack messages to when health check changes state
    Default: https://hooks.slack.com/triggers/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

Resources:
  HealthCheckStateChangedFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: site_health_check/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64      
      Policies:          
        - SNSPublishMessagePolicy: 
            TopicName: !GetAtt Route53HealthCheckSNSTopic.TopicName
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref Route53HealthCheckSNSTopic
          SLACK_WEBHOOK_URL: !Ref SlackWebhookURL
      Events:
        Trigger:
          Type: EventBridgeRule
          Properties:
            Pattern:
              source:
                - aws.cloudwatch
              detail-type:
                - CloudWatch Alarm State Change
              detail:
                alarmName:
                  - wildcard: "*-HealthCheckAlarm"

  Route53HealthCheckSNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      DisplayName: "Route53 Health Check SNS Topic"
      Subscription:  
        - Endpoint: healthcheck_status@example.com
          Protocol: email          
      TopicName: "Route53HealthCheckSNSTopic"

  Route53HealthCheck: 
    Type: 'AWS::Route53::HealthCheck'
    Properties: 

      HealthCheckConfig: 
        Port: 443
        Type: HTTPS
        ResourcePath: '/'
        FullyQualifiedDomainName: !Ref Hostname
        RequestInterval: 30
        FailureThreshold: 3    

  Route53HealthCheckAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: Health Check Alarm
      AlarmName: !Join
        - ''
        - - !Ref Hostname
          - '-HealthCheckAlarm'
      Namespace: AWS/Route53
      MetricName: HealthCheckStatus
      Dimensions:
        - Name: HealthCheckId
          Value: !Ref Route53HealthCheck
      ComparisonOperator: LessThanThreshold
      EvaluationPeriods: 1
      Period: 30
      Statistic: Minimum
      Threshold: 1.0
      TreatMissingData: breaching
```

## Route53 Health Checks

The core component of this solution is the service provided by AWS called [Route53 Health Checks](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html). Route53 health checks monitor the health and performance of your web applications, web servers, and other resources.

Route 53 has health checkers in locations around the world. When you create a health check that monitors an endpoint, health checkers start to send requests to the endpoint that you specify to determine whether the endpoint is healthy. You can choose which locations you want Route 53 to use, and you can specify the interval between checks: every 10 seconds or every 30 seconds.


![Route53 Health Checks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3atk5kzq2e16qp5lmi0.png)

## Cloudwatch Alarms for Route53 Health Checks

You can use [Amazon Cloudwatch Alarms](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html) to monitor the status of your Route53 Health Checks. When setting up the alarm you specify that it should be based on the metric associated with the Route53 Health Check. When the status of the metric changes from UP to DOWN (metric goes from 1 to 0) or DOWN to UP (metric goes from 0 to 1), the status of the Cloudwatch alarm will change as well.

One of the most important components in AWS to build Event-Driven Architectures is [Amazon Eventbridge](https://aws.amazon.com/eventbridge/). It is a serverless event bus that helps you receive, filter, transform, route, and deliver events. Most changes that happen in your AWS account automatically get sent to this bus including Cloudwatch alarm state changes. We'll discuss this below.


![Route53 Health Check Cloudwatch Alarm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1kr4yermud9s2qvpvw2d.png)

## Amazon Eventbridge

Whenever the status of a Cloudwatch alarm changes, AWS automatically sends and event to the default Eventbridge event bus in your account. In order to take advantage of this and trigger actions on events like this you need to create an [Eventbridge Rule](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html) to match the events you are interesting in and define what to do when a matching event is seen.

Here is an example of the rule we will be using to match the state change of the Cloudwatch alarm for the state change of the Route53 Health Check. We have setup the alarm name to be the hostname of what we are checking with a suffix of "-HealthCheckAlarm". The rule below will match any event that has an alarm name ending with this suffix.


![Eventbridge Rule Pattern](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3liwl7pcsxz1og2z1j8.png)

When you create a rule in Eventbridge to match events you can specify a list of targets or actions you want to execute when a match happens. Here is the target we have setup for this rule. It will call an AWS Lambda Function which will take care of notifying the people we have setup via SNS and Slack.


![Eventbridge Rule Target](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1g6m11my80flp1rxfs6.png)

## Simple Notification Service (SNS)

There are many use cases for having to notify someone or some other service on changes in your system. On AWS, the [SNS](https://docs.aws.amazon.com/sns/latest/dg/welcome.html) component is usually the best approach to use.

SNS is a managed service that provides message delivery from publishers to subscribers (also known as producers and consumers). Publishers communicate asynchronously with subscribers by sending messages to a topic, which is a logical access point and communication channel. Clients can subscribe to the SNS topic and receive published messages using a supported endpoint type, such as Amazon Kinesis Data Firehose, Amazon SQS, AWS Lambda, HTTP, email, mobile push notifications, and mobile text messages (SMS).

When you setup a new SNS topic and register endpoints (listeners) you typically have to accept being added to the notification list. For example with email notifications you will receive an email confirming you are expecting the notifications and want to receive them. For example you will see an email like this.


![SNS Confirmation Email](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6gcg4lcrhgnpfojza5bg.png)

## AWS Lambda Function

When the Cloudwatch alarm state changes we have setup a Lambda function (called for example - route53-health-check-sam-HealthCheckStateChangedFu) to be executed. The information about the site status and which site it was will be sent in the payload to the lambda function invocation.

Below is an example of lambda handler code. It is using the highly recommended [Powertools for AWS Lambda](https://docs.powertools.aws.dev/lambda/python/latest/) library to ensure best practices around tracing, logging, metrics, and more. The function gets passed an SNS topic and Slack webhook URL at creation time (via environment variables) to send notifications to. It parses the passed in event information to determine which hostname had it's status changed and what the new status is and sends out notifications.

One nice part about using SNS from a Lambda function is that you can set the notification subject and body to be whatever you want. When you use SNS directly it defines those for you.

```python
@tracer.capture_lambda_handler
@logger.inject_lambda_context(log_event=True)
@metrics.log_metrics(capture_cold_start_metric=True)
def lambda_handler(event, context):

    SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN']
    SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']

    logger.info(event)

    logger.info(f"SNS_TOPIC_NAME={SNS_TOPIC_ARN}")
    logger.info(f"SLACK_WEBHOOK_URL={SLACK_WEBHOOK_URL}")
    try:                
        if event['detail-type'] != "CloudWatch Alarm State Change":
            logger.error(f"This is not an event we care about - not sure why we got here")
            return

        event_detail = event['detail']
        site_that_changed = event_detail['alarmName'][:event_detail['alarmName'].find('-HealthCheckAlarm')]
        logger.info(f"site_that_changed={site_that_changed}")
        new_status = event_detail['state']['value']
        logger.info(f"new_status={new_status}")    

        if new_status != 'OK':
            icon = ":x:"
            text_status = "DOWN"
        else:
            icon = ":white_check_mark:"
            text_status = "UP"

        message_to_show = f"{icon} {site_that_changed} ( https://{site_that_changed} ) is now {text_status}"    
        slack_data = {
            "text": message_to_show
        }         

        if SLACK_WEBHOOK_URL:    
            logger.info(f"slack_data={slack_data}")
            slack_response = send_slack_message(slack_data, SLACK_WEBHOOK_URL)
            logger.info(f"slack_response={slack_response}")         

        sns_subject = f"{site_that_changed} is now {text_status}"
        sns_msg = f"https://{site_that_changed} is now {text_status}"
        sns_response = publish_to_sns(sns_subject, sns_msg, SNS_TOPIC_ARN)
        logger.info(f"sns_response={sns_response}") 

    except:
        traceback.print_exc()
        logger.info(f"traceback={traceback.format_exc()}")
```

## Notifications on Route53 Health Check status

In the example i have put together you will get email notifications from SNS at the email address you defined in the SAM project as below (**healthcheck_status@example.com is the code default**). Of course you will need a real email that you have access to to accept the SNS topic confirmation. You can also setup SMS text messages and more with SNS.

You will also receive Slack messages using the Slack webhook URL you define (example below uses a FAKE webhook URL of **https://hooks.slack.com/triggers/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**). You will need to get a valid webhook URL or you can set this to '' to skip the slack update part and just use SNS.

Setting up a Slack application or bot is out of scope of this article but there is a good tutorial here: [How to create a webhook URL for a Slack Channel](https://amanranjanverma.medium.com/how-to-create-a-webhook-url-for-a-slack-channel-b5b58b1d208a)?

The hostname to monitor (**www.amazon.com is setup as the default**) is defined in the parameters of the SAM template as below.

```yaml
  Route53HealthCheckSNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      DisplayName: "Route53 Health Check SNS Topic"
      Subscription:  
        - Endpoint: healthcheck_status@example.com
          Protocol: email          
      TopicName: "Route53HealthCheckSNSTopic"
```

```yaml
Parameters:
  Hostname:
    Type: String
    Description: Hostname to monitor   
    Default: www.amazon.com
  SlackWebhookURL:
    Type: String
    Description: URL to publish slack messages to when health check changes state
    Default: https://hooks.slack.com/triggers/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
```

Here are examples of the messages you will see with this setup on state changes of the alarm.


![Email from SNS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2aqsvad7sef1nib5f54f.png)


![Slack Update](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/833zj0f0o401a5f37ncn.png)

## Conclusion

I hope you learned about using various components in AWS to setup a serverless site monitoring solution. Please clone the [Github repository](https://github.com/RDarrylR/route53-health-check-sam) and try it out for yourself. There are likely many improvements that could be made.

Please let me know if you have any questions or concerns.

## Cleanup

If you did clone the repo and set this solution up for yourself please remember to clean up the resources to avoid any ongoing costs.

`sam delete` will be used to delete the underlying Cloudformation template and the resources provisioned in AWS.