Gift of Terraform (CDKTF): Building Automated Load Balancer Health Monitoring with CloudWatch Alarms and SNS
Published: 2024-02-12 16:49:42

We often prefer to engage with familiar elements in our surroundings. What if I told you that you can utilize your preferred programming language to provision resources from various service providers?

Cloud Development Kit for Terraform (CDKTF) enables some of the popular programming languages to define and deploy infrastructure. You can check out this installation guide for cdktf https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-install.

To begin with, create a new directory,
`mkdir send_alarm
cd send_alarm`

Inside the directory, run cdktf init, specifying the template for your preferred language and Terraform's AWS provider. We will not be using terraform cloud so you can leave that for now.

`cdktf init --template="python" --providers="aws@~>4.0"
(check the latest version in official terraform documentation)` 

Note: **You will be provided information to activate the virtual environment.**
Open the main.py file and copy and paste the below code.

```
 #!/usr/bin/env python

 from constructs import Construct
 from cdktf import TerraformStack, App, Token

 from imports.aws.provider import AwsProvider
 from imports.aws.cloudwatch_metric_alarm import CloudwatchMetricAlarm
 from imports.aws.sns_topic import SnsTopic
 from imports.aws.data_aws_lb_target_group import DataAwsLbTargetGroup
 from imports.aws.data_aws_lb import DataAwsLb
 from imports.aws.sns_topic_subscription import SnsTopicSubscription

 class MyStack(TerraformStack):
     def __init__(self, scope: Construct, name: str, config: dict):
         super().__init__(scope, name)
         AwsProvider(self, "aws", region="us-east-1", profile=your-credential-profile-name)
         for load_balancer in config["load_balancers"]:
             print(load_balancer) #used for debugging purpose
             lb = DataAwsLb(self, "load_balancer" + "_" + load_balancer["name"],
                            name=load_balancer["load_balancer_name"]
                            )

             lb_tg = DataAwsLbTargetGroup(self, "load_balancer_tg_name" + "_" + load_balancer["name"],
                                          name=load_balancer["load_balancer_tg_name"]
                                          )

             sns = SnsTopic(self, "sns_topic_name" + "_" + load_balancer["name"],
                            name=load_balancer["name"] + "_" + "topic"
                            )

             for email in load_balancer["email_list"]:
                 SnsTopicSubscription(self, "terraform-sns-topic_subscription" + "_" + load_balancer["name"] + email,
                                      protocol="email",
                                      topic_arn=sns.arn,
                                      endpoint=email,
                                      )

             CloudwatchMetricAlarm(self, "alb-healthy-hosts" + "_" + load_balancer["name"],
                                   alarm_actions=[sns.arn],
                                   alarm_description="Number of unhealthy nodes in Target Group",
                                   alarm_name=load_balancer["alarm_name"],
                                   comparison_operator="GreaterThanOrEqualToThreshold",
                                   dimensions={
                                       "LoadBalancer": lb.arn_suffix,
                                       "TargetGroup": lb_tg.arn_suffix
                                   },
                                   evaluation_periods=1,
                                   metric_name="UnHealthyHostCount",
                                   namespace="AWS/ApplicationELB",
                                   # ok_actions=[sns.arn],
                                   period=60,
                                   statistic="SampleCount",
                                   threshold=1
                                   )
 app = App()
 MyStack(app, "MyStack", config={
     "load_balancers": [
         {
             "name": "test-fulfillment1",
             "region": "us-east-1",
             "load_balancer_name": "lb-test-fulfillment",
             "load_balancer_tg_name": "lb-test-fulfillment-tg01",
             "alarm_name": "test-fulfillment-alarm",
             "email_list": ["mygmail1@gmail.com", "mygmail2@gmail.com"]
         },
         {
             "name": "test-fulfillment2",
             "region": "us-east-1",
             "load_balancer_name": "lb-test-fulfillment2",
             "load_balancer_tg_name": "test-fulfillment2-tg01",
             "alarm_name": "test-fulfillment2-alarm",
             "email_list": ["mygmail121@gmail.com"]
         },
     ]
 })
 app.synth()
```

**Ahhhhhh!!!!!!! The code is so long.....**
Wait This code may be lengthy, but we'll break down each component thoroughly to understand its functionality.

`#!/usr/bin/env python
from constructs import Construct
from cdktf import App, NamedRemoteWorkspace, TerraformStack, TerraformOutput, RemoteBackend`

In CDKTF (Cloud Development Kit for Terraform), the line from constructs import Construct is used to import the Construct class from the constructs module.

The Construct class is a fundamental class in CDKTF that represents a building block of your infrastructure. It serves as the base class for all other constructs and provides common functionality for creating, manipulating, and organizing resources in your CDKTF program.

By importing Construct, you can utilize the functionality provided by this class to create and manage your infrastructure components in CDKTF.

In CDKTF (Cloud Development Kit for Terraform), the line from cdktf import TerraformStack, App, and Token is used to import specific classes and objects from the cdktf module.

- TerraformStack is a class that represents a CDKTF stack, which is a collection of infrastructure resources defined and managed together. It provides methods and properties for defining and deploying your Terraform-based infrastructure.
- App is a class that represents a CDKTF application. It serves as the entry point for your CDKTF program and is responsible for initializing and executing the stack(s) defined in your program.
A token is an object that represents a CDKTF token. Tokens are used in CDKTF to represent references to resources and properties within the constructs. They provide a way to express dependencies and relationships between different parts of your infrastructure.

By importing these classes and objects, you can use them in your CDKTF program to define and manage your infrastructure stacks, create CDKTF applications, and work with tokens for resource references and dependencies.
`from imports.aws.cloudwatch_metric_alarm import CloudwatchMetricAlarm
 from imports.aws.sns_topic import SnsTopic
 from imports.aws.data_aws_lb_target_group import DataAwsLbTargetGroup
 from imports.aws.data_aws_lb import DataAwsLb
 from imports.aws.sns_topic_subscription import SnsTopicSubscription`

Each line is for importing the specific resources and data modules from the imports.aws module.

`class MyStack(TerraformStack):
     def __init__(self, scope: Construct, name: str, config: dict):
         super().__init__(scope, name)
         AwsProvider(self, "aws", region="us-east-1", profile=your-credential-profile-name)`

The code provided defines a class named MyStack that extends the TerraformStack class.

The MyStack class is defined with three parameters in its constructor: scope, name, and config. scope represents the construct scope, name is the name of the stack, and config is a dictionary that can be used to pass additional configuration parameters.

The super().__init__(scope, name) line calls the constructor of the parent class TerraformStack with the provided scope and name arguments. This ensures that the base TerraformStack class is properly initialized.

The AwsProvider class is instantiated with the line AwsProvider(self, "aws", region="us-east-1"). This creates an AWS provider resource within the stack. The self-argument refers to the current instance of the MyStack class. The string "aws" is the provider's name, and region="us-east-1" specifies the AWS region to use.

**You might have configured credentials inside the .aws/credentials file and your profile might be default which is generally in most of the cases.**

Now let me describe the portion below before moving on.

`app = App()
 MyStack(app, "MyStack", config={
     "load_balancers": [
         {
             "name": "loadbalancer1",
             "region": "us-east-1",
             "load_balancer_name": "lb-loadbalancer-1",
             "load_balancer_tg_name": "lb-loadbalancer-tg01",
             "alarm_name": "loadbalancer-1-alarm",
             "email_list": ["test1@gmail.com", "test2@gmail.com"]
         },
         {
             "name": "loadbalancer2",
             "region": "us-east-1",
             "load_balancer_name": "lb-loadbalancer-2",
             "load_balancer_tg_name": "lb-loadbalancer-tg02",
             "alarm_name": "loadbalancer-2-alarm",
             "email_list": ["test1@gmail.com"]
         },
     ]
 })
 app.synth()`

The code provided creates an instance of the App class and initializes a MyStack stack within it.

MyStack(app, "MyStack", config={...}) creates an instance of the MyStack class within the app. It takes three arguments: app (the App instance), "MyStack" (the name of the stack), and config (a dictionary that contains configuration details for the stack).

app.synth() synthesizes the CDKTF app and generates the Terraform configuration files based on the defined stack and resources.

`for load_balancer in config["load_balancers"]:
             print(load_balancer) #used for debugging purpose
             lb = DataAwsLb(self, "load_balancer" + "_" + load_balancer["name"],
                            name=load_balancer["load_balancer_name"]
                            )
             lb_tg = DataAwsLbTargetGroup(self, "load_balancer_tg_name" + "_" + load_balancer["name"],
                                          name=load_balancer["load_balancer_tg_name"]
                                          )
             sns = SnsTopic(self, "sns_topic_name" + "_" + load_balancer["name"],
                            name=load_balancer["name"] + "_" + "topic"
                            )
             for email in load_balancer["email_list"]:
                 SnsTopicSubscription(self, "terraform-sns-topic_subscription" + "_" + load_balancer["name"] + email,
                                      protocol="email",
                                      topic_arn=sns.arn,
                                      endpoint=email,
                                      )
             CloudwatchMetricAlarm(self, "alb-healthy-hosts" + "_" + load_balancer["name"],
                                   alarm_actions=[sns.arn],
                                   alarm_description="Number of unhealthy nodes in Target Group",
                                   alarm_name=load_balancer["alarm_name"],
                                   comparison_operator="GreaterThanOrEqualToThreshold",
                                   dimensions={
                                       "LoadBalancer": lb.arn_suffix,
                                       "TargetGroup": lb_tg.arn_suffix
                                   },
                                   evaluation_periods=1,
                                   metric_name="UnHealthyHostCount",
                                   namespace="AWS/ApplicationELB",
                                   # ok_actions=[sns.arn],
                                   period=60,
                                   statistic="SampleCount",
                                   threshold=1
                                   )`

The code provided iterates over the list of load balancer configurations in the config["load_balancers"] list and creates corresponding resources for each load balancer. Here's an explanation of what the code does:

- The for load_balancer in config["load_balancers"] loop iterates over each load balancer configuration in the config["load_balancers"] list. Inside the loop, the code prints the load_balancer dictionary, which represents the current load balancer configuration.

- The DataAwsLb class is instantiated with the line lb = DataAwsLb(self, "load_balancer" + "_" + load_balancer["name"], name=load_balancer["load_balancer_name"]). This creates a data source for an existing Application Load Balancer (ALB) with the specified name.

- The DataAwsLbTargetGroup class is instantiated with the line lb_tg = DataAwsLbTargetGroup(self, "load_balancer_tg_name" + "_" + load_balancer["name"], name=load_balancer["load_balancer_tg_name"]). This creates a data source for an existing ALB target group with the specified name.

- The SnsTopic class is instantiated with the line sns = SnsTopic(self, "sns_topic_name" + "_" + load_balancer["name"], name=load_balancer["name"] + "_" + "topic"). This creates an Amazon SNS topic resource with the specified name. Inside a nested loop, the code iterates over the load_balancer["email_list"] list and creates a SnsTopicSubscription for each email address. This sets up email subscriptions to the SNS topic for receiving notifications
(Will be talking more about the SNS topic and other SNS features in upcoming blog posts).

- The CloudwatchMetricAlarm class is instantiated with the line CloudwatchMetricAlarm(self, "alb-healthy-hosts" + "_" + load_balancer["name"], ...). This creates a CloudWatch metric alarm resource with the specified configurations such as alarm actions, alarm description, alarm name, comparison operator, dimensions, evaluation periods, metric name, namespace, period, statistic, and threshold
(Will be talking about the cloud watch alarm and other features in upcoming blog posts).

Note: **One thing is that you have to manually subscribe for the topic clicking on your email inbox.**

To deploy the code , run:
`cdktf deploy your_stack_name`

By executing this code, you will create multiple resources, an SNS topic, SNS topic subscriptions, and CloudWatch metric alarms based on the provided load balancer configurations.

**Excellent! This marks the conclusion of our comprehensive blog post detailing the process of setting up CloudWatch metric alarms through an SNS topic to notify when the load balancer is identified as unhealthy.**