亚马逊AWS官方博客

使用 EventBridge+Lambda 自动创建 CloudWatch Alarm

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

  • 依次添加如下 Policy,点击 Next

  • 输入 role name,点击 create role

4.1.2 创建 Lambda 函数需要依赖的 Layer

  • 下载 layer 用到的 zip 包

github:https://github.com/readybuilderone/lambda-layer

  • 进入 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

  • 修改 Lambda 代码

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 按钮

  • 输入 name,选择 Next

  • Edit Event Pattern

{
  "detail-type": ["EC2 Instance State-change Notification"],
  "source": ["aws.ec2"],
  "detail": {
    "state": ["running", "terminated"]
  }
}
  • Target 选之前创建的 Lambda 函数,点击 Next

  • 点击 create rule 完成创建

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

  • 创建名字为 WebServer01 的 EC2

  • 进入 CloudWatch Logs,观察 Lambda 日志,发现开始自动创建 CloudWatch Alarm

  • 在 CloudWatch Alarm 中验证,发现创建成功

测试自动删除- CloudWatch Alarm

  • 在 EC2 控制台,Terminate WebServer01

  • 观察 Lambda 日志

  • 在 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 等,在这里就不再详述。

本篇作者

韩医徽

AWS 资深解决方案架构师,曾负责 AWS 合作伙伴生态系统的云计算方案架构咨询和设计,现负责 DNB 游戏行业技术架构设计和支持,同时致力于 AWS 云服务在国内的应用和推广。

周宇翔

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的云计算方案架构的咨询和设计,在 Edge,Serverless,云迁移等方向具有丰富的实践经验。