ECSタスクの異常終了をCloudFormationでメトリクスにして死活監視

概要

ECSタスクの異常終了などの死活監視をしているとき、その結果をメトリクス(グラフ)として保存しておくと、ダッシュボード上で異常終了した履歴が分かって、さらにそこから簡単にアラーム発火もできて便利だなと思い、作ってみました。


前提

ECSはFargateで構築しています。(が、基本的にはon EC2も変わらないかと思います。)


手法

CloudWatch Logsのメトリクスフィルター機能で、カスタムメトリクスを作成します。


そこで、メトリクスフィルターでフィルタリングするためには、ECSタスクの異常終了情報をCloudWatch Logsに転送する必要があります。

そのため、EventBridgeECSタスクの異常終了イベントをCloudWatch Logsに転送します。


EventBridgeから直接SNSトピックに飛ばして死活監視をするのはよくありますが、今回はメトリクスとして残したいのでCloudWatch Logsを挟んでいます。


機能・サービス説明

メトリクスフィルター

メトリクスフィルターとは、CloudWatch Logsから特定の検索条件に一致するログの件数などをカスタムメトリクスに落とし込む機能です。


カスタムメトリクス

カスタムメトリクスとは、デフォルトでAWSが提供するメトリクスとは別に、ユーザーが自由に作成できるメトリクス(グラフ)になります。


EventBridge

上記の手法項目の通り、メトリクスフィルターはCloudWatch Logsのロググループをもとに作成されるため、CloudWatch LogsにECSの異常終了情報を保管するロググループがなければいけません。

しかし、ECSの異常終了情報というのは基本的には「イベント」として発生するものであり、ログがCloudWatch Logsに貯まるものではありません。


そこで、EventBridgeによって、ECSのイベントをCloudWatch Logsに転送するようにします。


コード

GitHubにもあります。

github.com

AWSTemplateFormatVersion: "2010-09-09"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "ECSClusterArn"
        Parameters:
          - ECSClusterArn
      - Label:
          default: "ECSMetricNamespace"
        Parameters:
          - ECSMetricNamespace

Parameters:
  ECSClusterArn:
    Type: String
  ECSMetricNamespace:
    Type: String

Resources:
  ResourcePolicyForEventsToLogs:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: "ECSEvents-ToLogs-ResourcePolicy"
      PolicyDocument: !Sub 
        - |
          {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Principal": {
                  "Service": "events.amazonaws.com"
                },
                "Action": [
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": "${CloudWatchLogsLogGroupArn}"
              }
            ]
          }
        - CloudWatchLogsLogGroupArn: !GetAtt ECSEventsLogGroup.Arn

  ECSEventsLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: /aws/events/ECSEvents

  ECSEventsRule:
    Type: AWS::Events::Rule
    DependsOn: ResourcePolicyForEventsToLogs
    Properties:
      Name: ECSEventsToLogs
      Description: "Transfer ECSEvents to CloudWatch Logs"
      State: ENABLED
      EventPattern: { "source": ["aws.ecs"] }
      Targets:
        - Arn: !GetAtt ECSEventsLogGroup.Arn
          Id: !Sub "ECSEventsLogGroup"

  TaskExitEventsFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: !Sub
        - '{ ($.detail.clusterArn = "${ECSClusterArn}") && ($.detail-type = "ECS Task State Change") && ($.detail.desiredStatus = "STOPPED") && ( ($.detail.stoppedReason = "Essential container in task exited") || ($.detail.stoppedReason = "Task failed container health checks") || ($.detail.stoppedReason.prefix = "Task failed ELB health checks") ) }'
        - ECSClusterArn: !Ref ECSClusterArn
      LogGroupName: !Ref ECSEventsLogGroup
      MetricTransformations:
        - MetricName: "ECS-TaskExitEvents"
          MetricNamespace: !Ref ECSMetricNamespace
          MetricValue: "1"

  TaskExitEventsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: ECS-TaskExitEvents-Alarm
      ActionsEnabled: true
      AlarmActions:
        - !Ref SNSTopic
      MetricName: "ECS-TaskExitEvents"
      Namespace: !Ref ECSMetricNamespace
      Statistic: "Sum"
      Period: 300
      EvaluationPeriods: 1
      DatapointsToAlarm: 1
      Threshold: 1
      ComparisonOperator: "GreaterThanOrEqualToThreshold"
      TreatMissingData: "notBreaching"


解説

EventBridgeでのログ転送

こちらは以下が対象になります。

  • AWS::Logs::ResourcePolicy
  • AWS::Logs::LogGroup
  • AWS::Events::Rule


リソースポリシーがCloudWatch Logsのロググループに与えられていないと、EventBridgeからログの転送ができません。

そのためロググループに付与するリソースポリシーを定義し、EventBridgeの「ルール」でイベントをログに転送するパターンターゲットのログを設定します。


こちらの構成の細かい解説や注意点などは以下記事にて説明しております。 go-to-k.hatenablog.com


ここでは、全イベントのログをCloudWatch Logsに転送し、そこからメトリクスフィルターでフィルタリングをしてカスタムメトリクスを作成します。


メトリクスフィルター

  • AWS::Logs::MetricFilter

今回の肝はこちらです。

  TaskExitEventsFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: !Sub
        - '{ ($.detail.clusterArn = "${ECSClusterArn}") && ($.detail-type = "ECS Task State Change") && ($.detail.desiredStatus = "STOPPED") && ( ($.detail.stoppedReason = "Essential container in task exited") || ($.detail.stoppedReason = "Task failed container health checks") || ($.detail.stoppedReason.prefix = "Task failed ELB health checks") ) }'
        - ECSClusterArn: !Ref ECSClusterArn
      LogGroupName: !Ref ECSEventsLogGroup
      MetricTransformations:
        - MetricName: "ECS-TaskExitEvents"
          MetricNamespace: !Ref ECSMetricNamespace
          MetricValue: "1"


FilterPatternに、ECSの死活監視として、検知したいパターンを記述しています。

      FilterPattern: !Sub
        - '{ ($.detail.clusterArn = "${ECSClusterArn}") && ($.detail-type = "ECS Task State Change") && ($.detail.desiredStatus = "STOPPED") && ( ($.detail.stoppedReason = "Essential container in task exited") || ($.detail.stoppedReason = "Task failed container health checks") || ($.detail.stoppedReason.prefix = "Task failed ELB health checks") ) }'


イベントはJSON形式で保存されますが、「$.キー」というような記法で特定のキーの値を参照することができます。

フィルタリングするイベントのパターンとしては、具体的には以下のような内容です。

detail.clusterArn

  • $.detail.clusterArn = "${ECSClusterArn}"
    • ECS ClusterのARNを指定して特定クラスターの検知をしています。
    • 全部のクラスターを対象にしたい場合、この指定は消してください。


detail-type

  • $.detail-type = "ECS Task State Change"
    • ECS Task State Changeに絞ることでタスクの状態変更のみを対象にします。


detail.desiredStatus

  • $.detail.desiredStatus = "STOPPED"
    • タスクの状態変更のうち、「停止(状態)」のみを対象にします。


detail.stoppedReason

正常終了は検知せず、異常終了のみを検知したいため、以下の条件に絞っています。

  • $.detail.stoppedReason = "Essential container in task exited"
    • 「essential パラメータが true のコンテナが終了またはクラッシュしたためタスクが失敗」した
  • $.detail.stoppedReason = "Task failed container health checks"
    • 「タスクがコンテナレベルのヘルスチェックに失敗」した
  • $.detail.stoppedReason.prefix = "Task failed ELB health checks"
    • 「ALB のヘルスチェックでタスクが失敗」した


実はこのパターンの部分は、AWS公式の「Amazon Web Services ブログ」の記事「Amazon EventBridge を利用した Amazon Elastic Container Service Anomaly Detector」より参考にさせていただいています。

こちらをご覧になった方がわかりやすく、また色々な検知をされているソリューションになっています。

aws.amazon.com


MetricTransformationsでは、メトリクス(グラフ)としての情報を設定します。

      MetricTransformations:
        - MetricName: "ECS-TaskExitEvents"
          MetricNamespace: !Ref ECSMetricNamespace
          MetricValue: "1"

MetricNameMetricNamespaceに関しては、メトリクスフィルターと合わせてください。

MetricValueは、1にしておくことで、フィルターに一致したときに1ずつカウントするというものです。基本的には1で良いかと思います。


CloudWatchアラーム

  • AWS::CloudWatch::Alarm

メトリクスフィルターから簡単にアラームも発火できるため、ついでに書きました。

メトリクスフィルターで作成したメトリクスの名前と名前空間を指定して設定します。


あくまでついでなので、細かい設定の説明は省略します。

  TaskExitEventsAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: ECS-TaskExitEvents-Alarm
      ActionsEnabled: true
      AlarmActions:
        - !Ref SNSTopic
      MetricName: "ECS-TaskExitEvents"
      Namespace: !Ref ECSMetricNamespace
      Statistic: "Sum"
      Period: 300
      EvaluationPeriods: 1
      DatapointsToAlarm: 1
      Threshold: 1
      ComparisonOperator: "GreaterThanOrEqualToThreshold"
      TreatMissingData: "notBreaching"


補足

ECSタスクの異常終了情報をメトリクスフィルターによってカスタムメトリクスにすることで、グラフとして表示したりできるようになりました。

しかし、基本的にはそんなに異常終了しないと思うので、このグラフは線にはならず点になるかと思います。


いわゆるグラフというと折線グラフを想像するかと思いますが、これが折れ線グラフになってしまうと、しょっちゅう落ちているということになります。

むしろそうでない、点どころか、データポイントがないのが安心なので、「期待外れな方が良いグラフ」になってしまいました・・・笑


まあとにかくメトリクスにしておくことで、普段見るダッシュボードにグラフを追加して眺めることができるので、現状の状況や過去のアラーム状況を追いやすくなり意外と便利です。


最後に

元を辿るとCloudWatch LogsのリソースポリシーがCloudFormationに対応したことを知って、それ関係の記事を書いていたら色々と派生する内容が思い浮かんできて、つい色々関連した記事を書いてしまいました。

連続で投稿が似た内容になってしまうのはご愛嬌で。。。