エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

(小ネタ) AutoScaling で増減した EC2 インスタンスに動的に CloudWatch Alarm を設定

こんにちは、エムスリーエンジニアの園田です。

AWS の AutoScaling で増減する EC2 インスタンスに対して CloudWatch Alarm を動的に設定したくなることありますよね?

AutoScalingGroup のメトリクスで AutoScalingGroup 内の平均 CPU 利用率などを監視することはできますが、 個々のインスタンスそれぞれに対してアラームを設定することは(今のところ)できません。
正確には、設定したとしても AutoScaling で新しく起動したインスタンスには設定されませんし、 スケールインで削除されたからといって自動でアラームの設定が削除されることもありません。

そこで、 CloudWatch Event Rule と Lambda を使って増減するインスタンスに対して動的にアラームを設定するのを試してみました。(何番煎じかわかりませんが・・・)

なお、この記事は エムスリーアドベントカレンダー ではないです。

監視シナリオ

今回は、以下のシナリオを例として実装してみます。

EC2 インスタンスのCPU使用率が 10 分間 80 % を超えていたら、SNSトピックに通知する。

イベントデータの JSON 確認

Lambda を実装する上で、CloudWatch Event からどういったイベントデータを受け取れるのかを確認しなければいけません。

イベントデータの JSON を確認するために、マネジメントコンソールで CloudWatch > イベント > ルール の ルールの作成 ボタンをポチります。ルールの作成画面で以下のように設定すると、サンプルイベントデータが表示されるので、コピっておきます。

f:id:ryoheisonoda:20181207165540p:plain

表示されたサンプルイベントデータは以下の通りです。

{
  "version": "0",
  "id": "3e3c153a-8339-4e30-8c35-687ebef853fe",
  "detail-type": "EC2 Instance Launch Successful",
  "source": "aws.autoscaling",
  "account": "123456789012",
  "time": "2015-11-11T21:31:47Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/sampleLuanchSucASG",
    "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f"
  ],
  "detail": {
    "StatusCode": "InProgress",
    "AutoScalingGroupName": "sampleLuanchSucASG",
    "ActivityId": "9cabb81f-42de-417d-8aa7-ce16bf026590",
    "Details": {
      "Availability Zone": "us-east-1b",
      "Subnet ID": "subnet-95bfcebe"
    },
    "RequestId": "9cabb81f-42de-417d-8aa7-ce16bf026590",
    "EndTime": "2015-11-11T21:31:47.208Z",
    "EC2InstanceId": "i-b188560f",
    "StartTime": "2015-11-11T21:31:13.671Z",
    "Cause": "At 2015-11-11T21:31:10Z a user request created an AutoScalingGroup changing the desired capacity from 0 to 1.  At 2015-11-11T21:31:11Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 1."
  }
}

今回利用するのは、起動したのか削除されたのかを判別するための $.detail-type と、インスタンスを特定するための $.detail.EC2InstanceId だけです。

Lambda 関数の実装

これを参考に、Lambda を実装してみます。せっかくなので、最近使えるようになった Ruby で実装しました。
追加ライブラリを利用していないため、パッケージのアップロードは不要です。

require 'json'
require 'aws-sdk'

# 通知先の SNS トピックは Lambda の環境変数に設定しておく
TOPIC_ARN = ENV['TOPIC_ARN']

# 実際にプロダクション利用する場合はイベントデータから受け取ったリージョンを指定してください
EC2 = Aws::EC2::Resource.new
CW = Aws::CloudWatch::Resource.new

# サンプルイベントデータを参考に、eventから必要な値を取得して処理する
def lambda_handler(event:, context:)
    instance_id = event['detail']['EC2InstanceId']

    case event['detail-type']
        # スケールアウトして新しくインスタンスが起動した場合
        when 'EC2 Instance Launch Successful'
            on_launched(instance_id)
        # スケールインしてインスタンスが削除された場合
        when 'EC2 Instance Terminate Successful'
            on_terminated(instance_id)
    end
end

def on_launched(instance_id)
    instance = EC2.instance(instance_id)

    return unless instance.exists?
    return if CW.alarm("ec2-#{instance.id}-cpu-util-notification").exists?

    # CloudWatch Alarm を作成
    CW.client.put_metric_alarm({
        alarm_name: "ec2-#{instance.id}-cpu-util-notification",
        alarm_description: "CPU使用率が 10 分間 80 % を超えていたら通知",
        # EC2
        namespace: 'AWS/EC2',
        # インスタンスを指定
        dimensions: [
            { name: "InstanceId", value: instance.id }
        ],
        # CPU 使用率の平均
        metric_name: 'CPUUtilization',
        statistic: "Average",
        # > 80 %
        threshold: 80,
        unit: 'Percent',
        comparison_operator: "GreaterThanThreshold",
        # 10 分間 (5 分 * 2 回) のうち 2 回
        period: 300,
        evaluation_periods: 2,
        datapoints_to_alarm: 2,
        # データなしは無視
        treat_missing_data: "ignore",
        # 閾値を超えたら警告通知
        alarm_actions: [ TOPIC_ARN ],
        # 元に戻ったことも通知
        ok_actions: [ TOPIC_ARN ],
    })
end

# インスタンスが削除されたらアラームも削除
def on_terminated(instance_id)
    alarm = CW.alarm("ec2-#{instance_id}-cpu-util-notification")
    alarm.delete if alarm.exists?
end

Lambda の実行ロールには以下の権限をつけておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "cloudwatch:PutMetricAlarm",
                "cloudwatch:DeleteAlarms",
                "cloudwatch:DescribeAlarms"
            ],
            "Resource": "*"
        }
    ]
}

サンプルイベントデータをテストイベントに設定して、インスタンスIDだけ実在するIDに変更して Lambda をテスト実行してみると、CloudWatch Alarm が作成されます。

aws cloudwatch describe-alarms --alarm-name-prefix ec2-
{
    "MetricAlarms": [
        {
            "AlarmName": "ec2-i-xxxxxxxxxxxxxxxx-cpu-util-notification",
            "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:999999999999:alarm:ec2-i-xxxxxxxxxxxxxxxx-cpu-util-notification",
            "AlarmDescription": "CPU使用率が 10 分間 80 % を超えていたら通知",
            "AlarmConfigurationUpdatedTimestamp": "2018-12-07T08:19:33.703Z",
            "ActionsEnabled": true,
            "OKActions": [
                "arn:aws:sns:ap-northeast-1:999999999999:system-notification"
            ],
            "AlarmActions": [
                "arn:aws:sns:ap-northeast-1:999999999999:system-notification"
            ],
            "InsufficientDataActions": [],
            "StateValue": "OK",
            "StateReason": "Threshold Crossed: 2 out of the last 2 datapoints [0.0 (07/12/18 08:14:00), 0.0 (07/12/18 08:09:00)] were not greater than the threshold (80.0) (minimum 2 datapoints for ALARM -> OK transition).",
            "StateReasonData": "{\"version\":\"1.0\",\"queryDate\":\"2018-12-07T08:19:34.301+0000\",\"startDate\":\"2018-12-07T08:09:00.000+0000\",\"unit\":\"Percent\",\"statistic\":\"Average\",\"period\":300,\"recentDatapoints\":[0.0,0.0],\"threshold\":80.0}",
            "StateUpdatedTimestamp": "2018-12-07T08:19:34.311Z",
            "MetricName": "CPUUtilization",
            "Namespace": "AWS/EC2",
            "Statistic": "Average",
            "Dimensions": [
                {
                    "Name": "InstanceId",
                    "Value": "i-xxxxxxxxxxxxxxxx"
                }
            ],
            "Period": 300,
            "Unit": "Percent",
            "EvaluationPeriods": 2,
            "DatapointsToAlarm": 2,
            "Threshold": 80.0,
            "ComparisonOperator": "GreaterThanThreshold",
            "TreatMissingData": "ignore"
        }
    ]
}

テストデータの detail-typeEC2 Instance Terminate Successful に変更して実行すれば、アラームが削除されることも確認できます。

CloudWatch Event Rule の作成

Lambda 関数ができたら、先ほど作りかけた CloudWatch Event Rule を作成します。

イベントタイプに EC2 Instance Launch SuccessfulEC2 Instance Terminate Successful を指定しています。

f:id:ryoheisonoda:20181207172807p:plain

まとめ

CloudWatch Event Rule と Lambda の組み合わせで AutoScalingGroup で増減するインスタンスに動的にアラームを設定できました。今回は CPU使用率でしたが、 CloudWatch Agent と組み合わせればメモリやディスクの監視通知も可能ですし、ECS タスクでも同様のことができると思います。

2018/12/27 追記

この仕組みを利用した SAM (Serverless Application Model) を OSS として公開しました。 公開した SAM については後日またこのテックブログにポストしたいと思います。

エンジニア募集!!

エムスリーでは、共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!

jobs.m3.com