Fusic Tech Blog

Fusion of Society, IT and Culture

CloudWatchAlarmの設定を全てTerraformでコード化するぞ!ASG配下のEC2、お前もだ!
2021/10/20

CloudWatchAlarmの設定を全てTerraformでコード化するぞ!ASG配下のEC2、お前もだ!

みなさまこんにちは。IoTチームの毛利です。

AWSでシステムを作るとき、CloudWatchAlarmによるモニタリングは欠かせないものだと思います。 いざ設定するとなるとモニタリングするメトリクスはかなりの数になりますし、1つあたりの設定項目も多いので、ぜひコード化したいところです。再利用もしやすいですしね。今回は、CloudWatchAlarmのコード化について書いていきます。

設定方法2種類

一般的なWebシステムにCloudWatchAlarmの設定をするにあたって、設定方法が2種類あります。

IaCで作成したリソースに対しての設定

TerraformやCloudFormationでインフラを構築するのと一緒にやるパターンです。 その場で作ったリソースに対してCloudWatchAlarmの作成を行うのでイメージしやすいと思います。

AutoScalingでスケールするEC2に対しての設定

AutoScalingでEC2を増減させる場合、IaCのコードにはこれらのリソースは登場しないので直接設定ができません。EC2が起動、終了するタイミングでCloudWatchAlarmの作成、削除を行いましょう。

CloudWatchAlarmの用語をおさらい

CloudWatchAlarmを設定していく前に、用語をざっくりおさらいしておきましょう。\ 詳細は公式ドキュメントで。

メトリクス

CloudWatchに送られた時系列データの集まりを指します。

例えばEC2インスタンスのCPU使用率です。CPU使用率はデフォルトで送信されるようになっていますが、これらの値をタイムスタンプ付きで保存したデータの集まりがメトリクスです。各メトリクスは、CloudWatchのコンソールでグラフとして確認できますし、サービスによってはサービスの画面でもメトリクスのグラフが確認できます。

また、メトリクスは独自のものも作成でき、カスタムメトリクスと呼ばれます。API, CLI, SDKなどを使って任意の値を送信していきます。例えば、ユーザーが何か特定の操作をした回数だったり、バッチ処理にかかった時間だったりと、アプリケーションによって監視したいものがいろいろあると思いますが、数値化できるものであれば何でも監視できてしまいます。すばらしい。

EC2が提供するデフォルトのメトリクスにはいろいろありますが、ディスク使用率やメモリ使用率などはデフォルトでは提供されません。CloudWatch Agentと呼ばれる公式のエージェントをインストールすることで、カスタムメトリクスの仕組みを使ってデフォルト以外のメトリクスを収集してくれるようになります。詳しくは公式ドキュメントをどうぞ。

ネームスペース

メトリクスのひとつ上のレイヤの概念で、メトリクスのコンテナを指します。グループみたいなものですね。デフォルトで提供されるメトリクスは、AWS/<サービス名>というネームスペースに入ります。EC2ならAWS/EC2、RDSならAWS/RDSです。

ディメンジョン

メトリクスのひとつ下のレイヤの概念で、メトリクスをさらに分類するための「名前と値のペア」です。例として、ALBのメトリクスを見てみましょう。

metrics1

xx別、xx別、とありますが、これらがディメンジョンを表しています。AppELB 別、AZ 別、TG 別メトリクスを見てみるとこのようになっています。

metrics2

一番右がメトリクス名、それ以外がディメンジョンです。このアカウントには1つのターゲットグループを持つ1つのALBしかありませんが、同じメトリクスでもAZごとに分かれて集計されているのが分かります。

タグのようにも見えますが、タグと違ってディメンジョンは一部だけ指定して取得するといったことができません。つまり、さきほどのALBの例でいうと、ALBのディメンジョンだけ指定して各AZの値の合計を取得するということはできません。(ALBの場合、ALBのみをディメンジョンとするメトリクスがありますが、これはまた別のメトリクスとして送られています。)そのため、必ずディメンジョンが一意に決まるセットを指定する必要があります。

IaCで作成したリソースに対しての設定

今回はTerraformを使っていきます。 varで始まるものは外部から持ってきた変数です。直書きするなり、variableとして定義するなり、他モジュールからのoutputを持ってくるなり、適宜やってください。

resource aws_cloudwatch_metric_alarm rds_cpu_utilization {
  alarm_name                = "rds_cpu_utilization"
  comparison_operator       = "GreaterThanThreshold"
  evaluation_periods        = var.cw_alarm.rds_default_evaluation_periods
  metric_name               = "CPUUtilization"
  namespace                 = "AWS/RDS"
  period                    = var.cw_alarm.rds_default_period
  statistic                 = "Average"
  threshold                 = var.cw_alarm.rds_cpu_utilization_threshold
  alarm_actions             = [var.sns_topic_cw_notification_arn]
  ok_actions                = [var.sns_topic_cw_notification_arn]
  insufficient_data_actions = [var.sns_topic_cw_notification_arn]
  dimensions = {
    DBInstanceIdentifier = var.rds_db_instance_identifier
  }
}

namespace, metric_name, dimensionsは説明したとおりで、この3つで監視するパラメーターを特定しています。

その他のパラメーターは判定方法やイベント発生時の動作を指定しています。 詳細はTerraformの公式ドキュメントを参照してください。

AutoScalingでスケールするEC2に対しての設定

AutoScalingでスケールするEC2はTerraformには登場しないので、別の設定が必要です。 AutoScalingによるEC2の起動・終了をEventBridgeでキャッチして、LambdaでCloudWatch Alarmの設定を行いましょう…と思っていたのですが、先日こんなリリースが! AWS Step Functions が AWS SDK 統合で 200 を超える AWS のサービスのサポートを追加

(もしかしたら元から対応されていたかもしれませんが、)Step FunctionsだけでCloudWatchAlarmの設定が完結すればLambdaも書かなくていいし、何よりTerraformに書ける!TerraformとSAMが混在しないのは嬉しいですね。

Step Functions

ここでは、EC2のCPU利用率に対するアラームの作成、削除をやってみます。 SNSとIAM RoleのARNは外部から渡すようにしています。

Alarmの作成(PutMetricAlarm)

resource aws_sfn_state_machine step-function-create-ec2-alarm {
  name     = "create-ec2-alarm"
  role_arn = var.iam_role_step_function_for_asg_event_arn

  definition = <<-EOF
  {
    "Comment": "create-ec2-alarm",
    "StartAt": "PutMetricAlarm",
    "States": {
      "PutMetricAlarm": {
        "Type": "Task",
        "End": true,
        "Parameters": {
          "AlarmName.$": "States.Format('ec2_cpu_utilization_{}', $.detail.EC2InstanceId)",
          "AlarmDescription.$": "States.Format('ec2_cpu_utilization_{}', $.detail.EC2InstanceId)",
          "OkActions": [
            "${var.sns_topic_cw_notification_arn}"
          ],
          "AlarmActions": [
            "${var.sns_topic_cw_notification_arn}"
          ],
          "InsufficientDataActions": [
            "${var.sns_topic_cw_notification_arn}"
          ],
          "Namespace": "AWS/EC2",
          "MetricName": "CPUUtilization",
          "Statistic": "Average",
          "Dimensions": [
            {
              "Name": "InstanceId",
              "Value.$": "$.detail.EC2InstanceId"
            }
          ],
          "Period": 60,
          "EvaluationPeriods": 3,
          "Threshold": 30,
          "ComparisonOperator": "GreaterThanThreshold"
        },
        "Resource": "arn:aws:states:::aws-sdk:cloudwatch:putMetricAlarm"
      }
    }
  }
  EOF
}

Alarmの削除(DeleteAlarms)

resource aws_sfn_state_machine step-function-delete-ec2-alarm {
  name     = "delete-ec2-alarm"
  role_arn = var.iam_role_step_function_for_asg_event_arn

  definition = <<-EOF
    {
      "Comment": "delete-ec2-alarm",
      "StartAt": "DeleteAlarms",
      "States": {
        "DeleteAlarms": {
          "Type": "Task",
          "End": true,
          "Parameters": {
            "AlarmNames.$": "States.Array(States.Format('ec2_cpu_utilization_{}', $.detail.EC2InstanceId))"
          },
          "Resource": "arn:aws:states:::aws-sdk:cloudwatch:deleteAlarms"
        }
      }
    }
  EOF
}

補足

definitionを自分で書いていくと日が暮れてしまうので、一旦AWSのコンソールのStep Functions Workflow Studioで組み立ててみるといいと思います。直感的にワークフローが組み立てられるので非常に楽です。

step_functions

APIのパラメーターはStep Functions Workflow Studioでは参照できないので、APIのドキュメントを見ましょう。 各サービスごとに、APIリファレンスがあります。例えばCloudWatchだと、こちら。ちゃんとPutMetricAlarmDescribeAlarmsもあります。

また、入力されたJSONの一部をパラメータに利用する方法がちょっと特殊です。

"AlarmNames.$": "States.Array(States.Format('ec2_cpu_utilization_{}', $.detail.EC2InstanceId))"

といった部分ですね。こういったことをする場合、キーの最後に.$を付ける必要があります。また、組み込み関数も有効に使っていきましょう。

EventBridgeの設定

AutoScalingでEC2の起動、停止が成功した場合に、Step Functionsを呼び出すようにしましょう。 StepFunctionsとIAM RoleのARNは外部から渡すようにしています。

EC2インスタンス起動時

resource aws_cloudwatch_event_rule eb-asg-ec2-launch-successful {
  name = "eb_asg_ec2_launch_successful"
  event_pattern = <<-EOF
  {
    "source": ["aws.autoscaling"],
    "detail-type": ["EC2 Instance Launch Successful"]
  }
  EOF
}

resource aws_cloudwatch_event_target eb-asg-ec2-launch-successful {
  rule     = aws_cloudwatch_event_rule.eb-asg-ec2-launch-successful.name
  arn      = var.step_function_create_ec2_alarm_arn
  role_arn = var.iam_role_event_bridge_for_asg_arn
}

EC2インスタンス停止時

resource aws_cloudwatch_event_rule eb-asg-ec2-terminate-successful {
  name = "eb_asg_ec2_terminate_successful"
  event_pattern = <<-EOF
  {
    "source": ["aws.autoscaling"],
    "detail-type": ["EC2 Instance Terminate Successful"]
  }
  EOF
}

resource aws_cloudwatch_event_target eb-asg-ec2-terminate-successful {
  rule     = aws_cloudwatch_event_rule.eb-asg-ec2-terminate-successful.name
  arn      = var.step_function_delete_ec2_alarm_arn
  role_arn = var.iam_role_event_bridge_for_asg_arn
}

動作確認

ここまでできたら、実際にAutoScalingGroupでEC2の台数を増減させてみましょう。 新しくできたEC2にAlarmが設定されていて、削除されたEC2のAlarmが削除されていたら成功です。

まとめ

この作業を始めた当初は、Terraformで作成したリソースはTerraformで、AutoScalingGroupで増減するEC2に関してはSAMを使ってEventBridgeからLambdaを呼び出してCloudWatch Alarmを設定する予定でしたが、非常にタイミングよくStepFunctionsの大型アップデートが来てくれました。おかげで全てTerraformで完結!素晴らしいですね。

プロジェクトごとの変数などを変数化して一度作っておけば、いろんなところで使い回せると思いますので、ぜひお試しください。

kta-m

kta-m

先進技術部門 IoTチーム チームリーダー