1. 概述
AWS CloudWatch Alarm 是 AWS CloudWatch 的一项功能,用于监控 AWS 资源和应用程序,并在达到特定阈值时发送通知或执行自动操作。一旦 CloudWatch Alarm 触发警报,可以使用多种方式通知用户,例如使用 SNS 主题发送电子邮件、短信或推送通知。此外,还可以设置自动响应,例如执行 AWS Lambda 函数、停止或重新启动 EC2 实例等。
利用 CloudWatch Alarm 可以帮助用户及时发现和解决系统问题,提高系统的可用性和可靠性,保障业务的正常运行。
然而,CloudWatch Alarm 只能针对单台的 EC2 实例创建,如果用户有数以千计的 EC2 实例需要监控,手动为每个实例创建 CloudWatch Alarm 将会是一项耗时耗力,同时也极容易出现错误的工作。
为了解决这个问题,这里介绍了一个利用 EventBridge 去监听 EC2 创建/销毁事件,并触发 Lambda 函数自动创建/删除 CloudWatch Alarm 的 Serverless 方案。
2. 方案概述
方案设计如下:
- 用户在 CloudWatch Config File 中,定义希望为 EC2 创建的 CloudWatch Alarm 的模版
- 用户创建 EC2,在 EC2 被成功创建时,触发 EC2 Instance State-change Notification 的事件,EventBridge 调用 Lambda,根据 CloudWatch Config File 自动创建对应的 CloudWatch Alarm
- 用户销毁 EC2,在 EC2 被成功销毁时,触发 EC2 Instance State-change Notification 的事件,EventBridge 调用 Lambda,根据 CloudWatch Config File 自动删除对应的 CloudWatch Alarm
3. 用户模拟场景描述
为了更好的说明,这里模拟一个用户的场景:
用户有数千台 EC2,这些 EC2 可以被分成两个组:WebServer 和 ApplicationServer。
WebServer 的命名为 WebServer001, WebServer002,WebServer003……
ApplicationServer 的命名为 ApplicationServer001, ApplicationServer002, ApplicationServer003……
用户希望为 WebServer 创建两个 CloudWatch Alarm:
- 当 CPU 平均利用率超过 40% 时,触发 p2-cpu-higher-than-40-{INSTANCENAME}的 CloudWatch Alarm
- 当 CPU 平均利用率超过 80% 时,触发 p1-cpu-higher-than-80-{INSTANCENAME}的 CloudWatch Alarm
用户希望为 ApplicationServer 创建一个 CloudWatch Alarm:
- 当 StatusCheckFailed 失败时,触发 p1-system-status-check-{INSTANCENAME}的 CloudWatch Alarm
以下分别用控制台构建和 SAM CLI 构建的两种方式说明如何操作,可根据需求选择其中一种方式进行构建即可。
4. 方式一:使用控制台方式构建
4.1 创建 Lambda,用于新建/删除 CloudWatch Alarm
4.1.1 创建 Lambda 函数使用的 IAM Role
- 登陆 AWS 控制台,进入 IAM 界面,点击左侧 Roles 链接,再点击 Create Role 按钮
- Use case 选 Lambda,点击 Next
- 输入 role name,点击 create role
4.1.2 创建 Lambda 函数需要依赖的 Layer
github:https://github.com/readybuilderone/lambda-layer
- 添入 layer name,选择 upload a .zip file,选取之前下载的 zip 包,架构选 x86, runtimes 选 python3.9,点击 Create
4.1.3 创建 Lambda 函数
- 登陆 AWS 控制台,进入 Lambda 界面,点击左侧 Functions 链接,点击右侧 Create Function 按钮
- 添入 Function name, Runtime 选 Python3.9,然后选取之前创建的 IAM Role,点击 Create function
import json
import os
import logging
import yaml
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
def lambda_handler(event, context):
event_type = event.get('detail-type')
if event_type == 'EC2 Instance State-change Notification':
instance_id = event['detail']['instance-id']
state = event['detail']['state']
instance_name = get_instance_name(instance_id)
logger.info('instance id: %s; instance name: %s; state: %s', instance_id, instance_name, state)
# ec2 state: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/monitoring-instance-state-changes.html
if state == 'running':
create_alarm_group(instance_id, instance_name)
elif state == 'terminated':
delete_alarm_group(instance_id, instance_name) #create this function
return {
"statusCode": 200
}
logger.warning('Not a supported event type: %s', event_type)
def get_instance_name(instance_id):
instance_name = ''
region = os.environ['TARGET_REGION']
ec2 = boto3.client('ec2', region)
# 查询特定实例ID的详细信息,包括实例名称
response = ec2.describe_instances(InstanceIds=[instance_id])
# 输出实例名称
for reservation in response['Reservations']:
for instance in reservation['Instances']:
for tag in instance['Tags']:
if tag['Key'] == 'Name':
instance_name = tag['Value']
return instance_name
def create_alarm_group(instance_id, instance_name):
with open('ec2-alarm.yaml') as file:
# 将文件内容解析为 YAML
config = yaml.load(file, Loader=yaml.FullLoader)
# 访问配置数据
for item in config:
prefix=item['prefix']
if instance_name.lower().startswith(prefix.strip().lower()):
for alarm in item['alarms']:
create_ec2_alarm(alarm, instance_id, instance_name)
def create_ec2_alarm(alarm, instance_id, instance_name):
region = os.environ['TARGET_REGION']
cloudwatch = boto3.client('cloudwatch',region)
alarm_name_pattern = alarm['alarmName']
alarm_name = alarm_name_pattern.replace('{INSTANCENAME}', instance_name)
logger.info('trying to create alarm: %s', alarm_name)
cloudwatch.put_metric_alarm(
Namespace ='AWS/EC2',
Dimensions=[
{
'Name': 'InstanceId',
'Value': instance_id,
},
],
AlarmName= alarm_name,
MetricName=alarm['metricName'],
Statistic=alarm['statistic'],
Period=alarm['period'],
EvaluationPeriods=alarm['evaluationPeriods'],
Threshold=alarm['threshold'],
ComparisonOperator = alarm['comparisonOperator'],
TreatMissingData = alarm['treatMissingData'],
OKActions=[
alarm['okActions'],
],
AlarmActions=[
alarm['alarmActions'],
],
InsufficientDataActions=[
alarm['insufficientDataActions'],
],
)
logger.info('alarm created: %s', alarm_name)
def delete_alarm_group(instance_id, instance_name):
with open('ec2-alarm.yaml') as file:
# 将文件内容解析为 YAML
config = yaml.load(file, Loader=yaml.FullLoader)
# 访问配置数据
for item in config:
prefix=item['prefix']
if instance_name.lower().startswith(prefix.strip().lower()):
for alarm in item['alarms']:
delete_ec2_alarm(alarm, instance_name)
def delete_ec2_alarm(alarm, instance_name):
region = os.environ['TARGET_REGION']
cloudwatch = boto3.client('cloudwatch', region)
alarm_name_pattern = alarm['alarmName']
alarm_name = alarm_name_pattern.replace('{INSTANCENAME}', instance_name)
logger.info('trying to delete alarm: %s', alarm_name)
cloudwatch.delete_alarms(
AlarmNames=[
alarm_name
]
)
logger.info('alarm deleted: %s', alarm_name)
4.1.4 新建配置文件 ec2-alarm.yaml
4.1.5 修改 ec2-alarm.yaml 内容如下
注意:okActions/alarmActions/insufficientDataActions 的 ARN 需要修改为自己账号的值
- ec2: webserver
prefix: WebServer
alarms:
- alarmName: p2-cpu-higher-than-40-{INSTANCENAME}
metricName: CPUUtilization
statistic: Average
period: 60
evaluationPeriods: 2
threshold: 40
comparisonOperator: GreaterThanThreshold
treatMissingData: missing
okActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
alarmActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
insufficientDataActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
- alarmName: p1-cpu-higher-than-80-{INSTANCENAME}
metricName: CPUUtilization
statistic: Average
period: 60
evaluationPeriods: 1
threshold: 80
comparisonOperator: GreaterThanThreshold
treatMissingData: missing
okActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
alarmActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
insufficientDataActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
- ec2: applicationserver
prefix: ApplicationServer
alarms:
- alarmName: p1-system-status-check-{INSTANCENAME}
metricName: StatusCheckFailed
statistic: Maximum
period: 60
evaluationPeriods: 1
threshold: 1
comparisonOperator: GreaterThanOrEqualToThreshold
treatMissingData: missing
okActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
alarmActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
insufficientDataActions: arn:aws:sns:us-east-1:123456789012:CloudwatchAlarmTopic
4.1.6 点击 Deploy 按钮
4.1.7 修改 Lambda 超时时间到 1 分钟,并保存
4.1.8 创建环境变量
4.1.9 添加 Layer
4.2 创建 EventBridge 规则
- 进入 EventBridge,点击左侧的 Rules 链接,再点击 Create rule 按钮
{
"detail-type": ["EC2 Instance State-change Notification"],
"source": ["aws.ec2"],
"detail": {
"state": ["running", "terminated"]
}
}
- Target 选之前创建的 Lambda 函数,点击 Next
4. 方式二:使用 SAM CLI 方式构建
4.1 SAM CLI 使用环境准备
使用 SAM CLI,需要提前安装如下工具:
4.2 使用 SAM CLI 部署方案
git clone https://github.com/readybuilderone/auto-cloudwatch-alarm.git
cd auto-cloudwatch-alarm
sam build --use-container
sam deploy --guided
5. 方案测试
测试自动创建 CloudWatch Alarm
- 进入 CloudWatch Logs,观察 Lambda 日志,发现开始自动创建 CloudWatch Alarm
- 在 CloudWatch Alarm 中验证,发现创建成功
测试自动删除- CloudWatch Alarm
- 在 EC2 控制台,Terminate WebServer01
- 在 CloudWatch Alarm 中验证,发现对应的 Alarm 被删除
测试成功。
6. 更多思考:如何对存量 EC2 创建 CloudWatch Alarm,以及如何批量更新 CloudWatch Alarm?
本文中演示了如何自动创建/删除 CloudWatch Alarm,在真实场景中,还涉及到如何批量更新 CloudWatch Alarm。
可以使用类似的 Lambda 函数,手动去触发,示例代码参考: https://github.com/readybuilderone/auto-cloudwatch-alarm/blob/b69e89a3e2093967278c1fb264f607a3a26399a0/auto_cloudwatch_alarm/update-alarm.py。
7. 总结
在本博文中,我们演示了如何使用基于 Serverless 架构的 EventBridge+Lambda 结合 CloudWatch Alarm 去自动创建资源告警,并与 SNS 集成以发送消息通知,使您及时洞悉相关的告警事件,以便快速响应。文中以自动创建/销毁 EC2 的 CloudWatch Alarm 作为例子,这个解决方案的思路也同样适用于 AWS 的其他资源,比如 RDS、ElastiCache 等,在这里就不再详述。
本篇作者