How to Create a Monitoring System with AWS Technologies and Twilio Voice

May 10, 2023
Written by
Rizaldi Martaputra
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

header - How to Create a Monitoring System with AWS Technologies and Twilio Voice

In this post, you will learn how to create a monitoring system using Twilio Voice that will alert you when CPU utilization percentage of an AWS EC2 instance is over a specific threshold value. To do that you will utilize other AWS services such as:

The following provides an overview of the system that you will build in this tutorial.

Overview of the system that you will build in this tutorial

In this tutorial you will configure a CloudWatch alarm that will trigger a call to the Twilio Voice API using SNS topic and AWS Lambda function.

Prerequisites

To complete this tutorial you will need:

Also note that there will be a cost for buying a Twilio number that you will use to make a phone call. You can find out about its pricing in Twilio documentation. You can also view the cost for calling between countries.

Set up the alert system

Use your preferred terminal to create a new directory. Change into that directory with the following commands:

$ mkdir twilio-tutorial
$ cd twilio-tutorial

Additionally, you can also download code used in this tutorial from its GitHub repo. There are two directories in that repository: scripts and lambda. The former is for scripts used in creating and deleting AWS resources used in this tutorial and the latter is the source code for AWS Lambda function that you will create.

Set up the AWS CLI

To create AWS resources in this tutorial, you will use AWS CLI. To install it, follow the instructions in its documentation. Please note that you will use AWS CLI version 2 for this tutorial.

After installing AWS CLI, you need to configure it first before you can start using it. For this you need an access key ID and a secret access key. To get them do the followings:

Visit the security credentials page.

"Your Security Credentials" page in AWS console

Under the Access keys (access key ID and secret access key) section, click Create New Access Key.

A dialog window will show up. Click Download Key File.

A .csv file will be downloaded. Open this file using your preferred text editor. In this file you will find your access key ID and secret access key.

Once you downloaded the .csv file, open your terminal and run:

aws configure

You will be prompted to input your access key ID, secret access key, default region, and default output format. For access key ID and secret access key, input the information that you get from the downloaded .csv file. For default region type in us-east-1 and for default format type in json. Please note that you can choose any region in the context of this tutorial.

To test if your configuration is correct, run:

aws ec2 describe-instances

It will show the output below if you don’t have any instances right now.

{
        "Reservations": []
}

Create an EC2 instance

Before creating an EC2 instance, you need 2 things:

  • A key pair which you will use to ssh into your instance.
  • A security group to define your instance ingress rule so that a ssh connection is allowed into your instance.

You can also execute commands in this section by executing file scripts/1-create-ec2-instance.sh in the aforementioned GitHub repo.

To create a key pair, in your terminal run:

aws ec2 create-key-pair \
--key-name MyKeyPair \
--query 'KeyMaterial' \
--output text \
> MyKeyPair.pem

Now you have a file named MyKeyPair.pem in your current directory. You will use this file later to ssh into your instance. Before moving onto the next step, grant the file user read permission:

chmod 400 MyKeyPair.pem

An AWS EC2 instance has to be inside a virtual network called AWS Virtual Private Cloud (VPC). Run the command below to get your VPC ID and store it in the vpc_id variable.

vpc_id=$(aws ec2 describe-vpcs \
--query 'Vpcs[?IsDefault==`true`].VpcId | [0]' \
--output text)

Now that you have your VPC ID, you will create a security group inside that VPC. A security group acts as a virtual firewall for your instance to control inbound and outbound traffic. In this tutorial, we will use this to set up a rule so that you can ssh into your instance. Run the following command:

aws ec2 create-security-group \
--group-name my-sg \
--description "My security group" \
--vpc-id $vpc_id

Run the following command to get the newly created security group’s ID and store it in the sec_group_id variable.

sec_group_id=$(aws ec2 describe-security-groups \
--group-names my-sg \
--query 'SecurityGroups[?GroupName==`my-sg`].GroupId | [0]' \
--output text)

Next, you need to create a security group rule. Run the following command to create an ingress rule in your security group to allow an ssh connection from anywhere.

aws ec2 authorize-security-group-ingress \
--group-id $sec_group_id \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0

Next, create an instance using Ubuntu 22.04 LTS image. Find a compatible image in your preferred region.

To do this, access Ubuntu Amazon EC2 AMI Locator page.

amazon ec2 ami locator

In the search box type in "22.04 LTS". Scroll down to the bottom of the page. Filter for the AMI-IDs with amd64 under the Arch column. Choose your preferred zone under the Zone column. Copy the AMI-ID.

amazon ec2 ami 22.04 lts

Run the command below to create an EC2 instance. If you choose a region other than us-east-1 replace ami-XXXXXXX with the compatible image found in the previous step.

image_id=ami-XXXXXXX
aws ec2 run-instances \
--image-id $image_id \
--instance-type t3.micro \
--key-name MyKeyPair \
--security-group-ids $sec_group_id

Now you have a new EC2 instance running Ubuntu 22.04 LTS image. Your instance will be a t3.micro instance, a low cost burstable general purpose instance type with 2 vCPUs and 1 GB of memory. This type of instance is included in AWS Free Tier so that it will incur no cost.

Once you run the command, it will show you a json output with details of the instance that you just created.

Now run the following command to get the newly created instance’s id and save it into the instance_id variable.

instance_id=$(aws ec2 describe-instances \
--query 'Reservations[*].Instances[?ImageId==`$image_id`] | [0][0].InstanceId' \
--output text)

Create an Amazon SNS topic

The corresponding script for this section in GitHub repo is scripts/2-create-sns-topic.sh.

Run following command to create an AWS SNS topic:

aws sns create-topic --name HighUsageCpuTopic

Then run the following command to retrieve the newly created SNS topic’s Arn and save it into the sns_topic_arn variable.

sns_topic_arn=$(aws sns list-topics \
--query 'Topics[?ends_with(TopicArn, `:HighUsageCpuTopic`)] | [0].TopicArn' \
--output text)

Create a CloudWatch Alarm

The corresponding script for this section in GitHub repo is scripts/3-create-cloudwatch-alarm.sh.

Run following command to create a CloudWatch alarm:

aws cloudwatch put-metric-alarm \
--alarm-name cpu-monitor \
--alarm-description "Alarm when CPU exceeds 70 percent" \
--metric-name CPUUtilization \
--namespace AWS/EC2 \
--statistic Average \
--period 60 \
--threshold 70 \
--comparison-operator GreaterThanThreshold \
--dimensions "Name=InstanceId,Value=$instance_id" \
--evaluation-periods 2 \
--alarm-actions $sns_topic_arn \
--unit Percent

Create Java application using Gradle

The code that you will write in this section can also be found in the aforementioned GitHub repo inside lambda directory.

In this tutorial we choose Gradle as build automation tool because we can scaffold a good starting code for the lambda function with the gradle init command.

Create a new directory called lambda and change into that directory.

mkdir lambda && cd lambda

Then scaffold a Java application using Gradle:

gradle init

Select "basic" as the type of project, "Groovy" as build script DSL, and answer "No" for using new APIs and behaviors. Keep the default project name as "lambda".

Running "gradle init" command in terminal

Next, open your code editor / IDE and open your lambda directory using that IDE. If you are using Intellij IDEA, you can select Open then choose your directory.

"Welcome to Intellij IDEA" window

Now open build.gradle file and replace the contents with the following code:

plugins {
   id 'java'
}

repositories {
   mavenCentral()
}

dependencies {
   implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
   implementation 'com.amazonaws:aws-lambda-java-events:3.9.0'
   implementation 'com.google.code.gson:gson:2.8.9'
   implementation 'org.apache.logging.log4j:log4j-api:[2.17.1,)'
   implementation 'org.apache.logging.log4j:log4j-core:[2.17.1,)'
   implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:[2.17.1,)'

   implementation group: "com.twilio.sdk", name: "twilio", version: "9.1.4"

   testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0'
}

test {
   useJUnitPlatform()
}

task buildZip(type: Zip) {
   from compileJava
   from processResources
   into('lib') {
       from configurations.runtimeClasspath
   }
}

java {
   sourceCompatibility = JavaVersion.VERSION_11
   targetCompatibility = JavaVersion.VERSION_11
}

build.dependsOn buildZip

In the previous code, a new Gradle task called buildZip is defined so that when gradle build is executed, you will also run the buildZip task. This task will store your Java classes and resources into a zip file which you will deploy as your Lambda function.

Next, create src/main/java/example subdirectory within the lambda directory. Create 3 new Java source files inside that subdirectory with the following commands:

mkdir -p src/main/java/example
cd src/main/java/example
touch HandlerSNS.java SNSMessage.java Trigger.java

Open the Trigger.java file. Here you will define the Trigger class with the following code:

package example;

import com.google.gson.annotations.SerializedName;

import java.util.List;

public class Trigger {

   @SerializedName("MetricName") private String metricName;
   @SerializedName("Statistic") private String statistic;
   @SerializedName("Unit") private String unit;
   @SerializedName("Dimensions") private List<Dimension> dimensions;
   @SerializedName("Period") private String period;
   @SerializedName("EvaluationPeriods") private int evaluationPeriods;
   @SerializedName("ComparisonOperator") private String comparisonOperator;
   @SerializedName("Threshold") private float threshold;

   public String getInstanceId() {
       for (int i = 0; i < dimensions.size(); i++) {
           Dimension dimension = dimensions.get(i);
           if (dimension.getName().equals("InstanceId")) {
               return dimension.getValue();
           }
       }
       return "";
   }

   public String getMetricName() {
       return metricName;
   }

   public float getThreshold() {
       return threshold;
   }

   public String getComparisonOperator() {
       return comparisonOperator;
   }

   public String getUnit() {
       return unit;
   }

   static class Dimension {
       private String name;
       private String value;

       public String getName() {
           return name;
       }

       public String getValue() {
           return value;
       }
   }
}

Next, define the SNSMessage class inside the SNSMessage.java file:

package example;

import com.google.gson.annotations.SerializedName;

public class SNSMessage {

   @SerializedName("AlarmName") String alarmName;
   @SerializedName("AlarmDescription") String alarmDescription;
   @SerializedName("Trigger") Trigger trigger;

}

Last, paste the following code in the HandlerSNS.java file which will contain your lambda function’s handler method.

package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.type.PhoneNumber;
import com.twilio.type.Twiml;

public class HandlerSNS implements RequestHandler<SNSEvent, String> {
 Gson gson = new GsonBuilder().setPrettyPrinting().create();

 public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID");
 public static final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN");

 public static final String PHONE_NUMBER = System.getenv("TWILIO_PHONE_NUMBER");
 public static final String DEST_PHONE_NUMBER = System.getenv("DEST_PHONE_NUMBER");

 @Override
 public String handleRequest(SNSEvent event, Context context) {
   String response = "200 OK";

   String message = event.getRecords().get(0).getSNS().getMessage();
   SNSMessage snsMessage = gson.fromJson(message, SNSMessage.class);

   PhoneNumber dest = new PhoneNumber(DEST_PHONE_NUMBER);
   PhoneNumber source = new PhoneNumber(PHONE_NUMBER);
   Twiml twiml = new Twiml(String.format("<Response><Say>Instance CPU utilization exceeds threshold %d %s</Say></Response>",
           (int) snsMessage.trigger.getThreshold(),
           snsMessage.trigger.getUnit()
   ));

   Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
   Call call = Call.creator(dest, source, twiml).create();

   call.getSid();

   return response;
 }
}

Before we move into the next step let me explain how your lambda function will work.

Anytime there’s a new message in your SNS topic, the handleRequest method in the HandlerSNS class is called. An SNSEvent and Context object will be passed into that method. The former contains information related to the message received in the SNS topic while the latter contains information related to the request, such as request ID, log group name, etc.

The Gson library will parse information stored in the SNSEvent object and store it inside an SNSMessage object.

Using the information inside the SNSMessage object, it will construct a Twiml (Twilio Markup Language) object. This object is the representation of a voice message that will be prompted by Twilio when you pick up a call from Twilio.

The Twilio Client is initiated with the ACCOUNT_SID and AUTH_TOKEN environment variables in order to make a call to the Twilio Voice API.

Now that you already have your Java application, you need to build it and package it into a zip file before you deploy it as a lambda function. In your terminal, go back to your Java application project’s root directory. So for example, if you started this tutorial in ~/twilio-tutorial directory, change into ~/twilio-tutorial/lambda directory.

Inside that directory, to build the application run:

gradle build -i

Inspect your directory and notice a new directory named build. Within the subdirectory build/distributions is a lambda.zip file as seen in the project structure below:

Inspecting the content of lambda directory

Create an AWS Lambda Function

Create an IAM role that will be assigned to the lambda function. A role is a way to give the lambda function an identity to help AWS decide what a resource can and cannot do. You can read more about the IAM role in AWS documentation.

Open your terminal and make sure that you are in the project root directory. Create a file named policy.json with the following contents:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
             "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Using that file, create a new role called lambda-role with the command:

aws iam create-role \
--role-name lambda-role \
--assume-role-policy-document file://policy.json

Get the newly created role’s arn:

lambda_role_arn=$(aws iam list-roles \
--query 'Roles[?RoleName==`lambda-role`].Arn | [0]' \
--output text)

Attach AWSLambdaBasicExecutionRole to your new IAM role:

aws iam attach-role-policy \
--role-name lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Create a new lambda function using the newly created role and zip file created earlier:

aws lambda create-function --function-name high-cpu-call \
--runtime java11 \
--zip-file fileb://lambda/build/distributions/lambda.zip \
--handler example.HandlerSNS \
--description 'Handle SNS event' \
--memory-size 512 \
--timeout 60 \
--role $lambda_role_arn

You can find the full script used in this section in the GitHub repo in file scripts/4-create-lambda.sh.

Set up environment variables for Lambda function

When writing code for the lambda function, you might notice that in the HandlerSNS class the code will get some values from environment variables. You need to set up the values for these environment variables in your lambda configuration.

To get your ACCOUNT_SID and AUTH_TOKEN , navigate to the API keys and auth tokens page on the Twilio console. You will be able to find it at the bottom of the page in the Live credentials section.

"API Keys and Tokens" page in Twilio console

Next, TWILIO_PHONE_NUMBER environment variable is for the phone number that you purchased from Twilio to make the call. While DEST_PHONE_NUMBER is for the phone number that will be called when there is a high CPU utilization alert, such as your personal phone number.

Now that you have all that information, you can run the following commands to set your lambda function’s environment variables. Don’t forget to replace <acc_sid>, <token>, <twilio_phone_number>, and <your_phone_number> with your relevant informationL

twilio_acc_sid=<acc_sid>
twilio_auth_token=<token>
twilio_phone_number=<twilio_phone_number>
dest_phone_number=<your_phone_number>
aws lambda update-function-configuration --function-name high-cpu-call \
--environment "Variables={TWILIO_ACCOUNT_SID=$twilio_acc_sid,TWILIO_AUTH_TOKEN=$twilio_auth_token,TWILIO_PHONE_NUMBER=$twilio_phone_number,DEST_PHONE_NUMBER=$dest_phone_number}"

You can find the command in the GitHub repo in file scripts/5-lambda-env.sh.

Create Amazon SNS Topic Subscription

Once you created the SNS topic and the lambda function, the next thing to do is to create an SNS topic subscription and add permission for the SNS topic to call the lambda function. You can find commands executed in this section in the GitHub repo in file scripts/6-sns-subscribe.sh.

Get the lambda function’s ARN and then create an SNS subscription with the command:

lambda_arn=$(aws lambda list-functions \
--query 'Functions[?FunctionName==`high-cpu-call`] | [0].FunctionArn' \
--output text)

aws sns subscribe \
--topic-arn $sns_topic_arn \
--protocol lambda \
--notification-endpoint $lambda_arn

Then add permission for the SNS topic to call your lambda function:

aws lambda add-permission \
--function-name high-cpu-call \
--action lambda:InvokeFunction \
--statement-id sns-topic-HighUsageCpuTopic \
--principal sns.amazonaws.com \
--source-arn $sns_topic_arn

Test the alert system

The last thing to do is to test your setup to see if it works. One way to do this is to simulate high CPU utilization in your server using stress-ng command.

Using AWS CLI find out your instance’s public IP address:

instance_ip=$(aws ec2 describe-instances \
--query 'Reservations[*].Instances[?ImageId==`ami-XXXXXXX`] | [0][0].PublicIpAddress' \
--output text)

Once you have the IP address, ssh into your instance using your key-pair file.

ssh -i MyKeyPair.pem ubuntu@$instance_ip

Once you’re logged in, install stress-ng package:

  1. Update package repository by running sudo apt update
  2. Install stress-ng package: sudo apt install stress-ng

Once the package is installed, run:

stress-ng --matrix 0 -t 10m --times

To check your instance’s CPU utilization, you can access CloudWatch’s all alarms page, find cpu-monitor alarm, and click on it to see its details and graph.

Alarms page in AWS console

Detail page of "cpu-mon" alarm in AWS console

Wait a few minutes after you execute the stress-ng command, you will see that your instance CPU utilization will move up. After 2 minutes of CPU utilization over 70%, CloudWatch will send a message to the SNS topic. This will trigger your lambda function which will then call your phone number.

 

What's next for creating a monitoring system with Twilio Voice and AWS?

Congratulations on creating a monitoring system using the Twilio Voice API and AWS Lambda function! There are a lot of possibilities for improvement if you want to take this project further. You can add more details related to CPU utilization in your voice message, or you may also add options in your voice message and then program your lambda function to respond to keypad input. You can read more about Twilio Voice API by going through the documentation.

Rizaldi Martaputra is a software developer from Indonesia. In the first few years of his career in software development, he worked as Android app developer but then switched into DevOps role since it allows him to tinker with more diverse tasks and help him optimize his team’s whole software development process. You can follow him on GitHub.