ECS スケジュールされたタスクが起動しなかったことを監視する

この記事は[ツクリンクアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/tsukulink)の３日目の記事になります！

# はじめに
ECSのスケジュールタスクで定期的にジョブを実行しているのですが、
ある日、いつもは正常に動作しているジョブが実行されていませんでした。
しかも普段出力しているCloudWatch Logsにログも出力されていません。。

早速調べてみると、この事象はよくあるケースの様で、StepFunctionsを経由させて起動することで対応できる様です。
今回は、ecscheduleで管理していることもあり、まずはスケジュールされたタスクが起動していないことを監視して通知する対応をすることにしました。

ただ、通常のサブスクリプションフィルターやメトリクスフィルター(+CloudWatchアラーム)では、出力を監視して通知自体は可能ですが、今回は逆に出力されない時に通知する必要があります。
そこでMetric Mathを使うことで、この要件を実現できましたので、その手法を記事にします。

::: note
Metric Mathの紹介なのですが、タイトルの事象を解決のために調べてい時に、
Metric Mathに辿り着くために時間がかかったので、このタイトルにしています！
:::

# CloudWatch Metric Mathとは
Metric Mathは、CloudWatchメトリクスに対して数式を使い新たなメトリクスを作成できます。単一のメトリクスだけでなく、複数のメトリクスに対しても使用することができます。

例えば以下のようなメトリクスを作成できます。
- 単一のメトリクスの値が500以下だと、一律0を返すメトリクスを作成
- 2つのメトリクスの合計値を返すメトリクスを作成
- 2つのメトリクスの内、値が大きい方を返すメトリクスを作成

詳細については[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/using-metric-math.html)を参照してください。

これまでMetric Mathの存在は認知していたのですが、
いざ調べてみると多くの関数が提供されていて、感動しました。

# 本題
では、特定の文字列を特定期間に出力されていない時に通知する方法について説明します。
今回は1:00~1:05(JST)に特定の文字列が出力されていなかったらSNS通知するようにします。

:::note warn
JSTとしているのは、CloudWatchではUTC時刻で指定する必要があるためです。
ですので、以降は1:00~1:05(JST)をUTCの16:00~16:05として記載します。
:::

なお、今回はterraformを使用していますが、同じ設定はマネジメントコンソールやCloudFormationでも可能です。

## やっていること
やっていることは以下だけです！
- CloudWatch Logsから該当の文字列を検知したら1を返すメトリクスフィルタを作成する
- CloudWatchアラームを作成する
    - メトリクス値が1以上であれば正常、0になるとアラーム状態とする
    - 基本となるCloudWatch Logsから出力されるメトリクスを定義する。
    - Metric Mathで以下条件で値を返却するメトリクスを定義する。
        - 指定時間(16:00~16:05)以外は固定で1を返す（正常状態を維持させる）
        - 指定時間(16:00~16:05)は上記のメトリクスフィルターのメトリクス値を返す

## 実装例
文章で説明するより実装例の方がわかりやすいと思いますので実際の実装例を紹介します。
（CloudWatch Logs及びメトリクスフィルター、SNSの実装部分は割愛します。）

```monitor.tf
resource "aws_cloudwatch_metric_alarm" 　"execution_monitoring" {
  alarm_name          = "execution_monitoring"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 1
  threshold          = 1
  treat_missing_data = "ignore"
  alarm_actions      = [aws_sns_topic.notify.arn]
  
  # オリジナルのメトリクス値
  metric_query {
    id = "m1"
    metric {
      metric_name = "execution_monitoring"
      period      = 300
      stat        = "Maximum"
    }
  }

  # Metric Mathのメトリクス値
  metric_query {
    id          = "e1"
    # 16:00UTC のデータポイント(16:00~ 300秒間の評価)の時だけm1のメトリクス値を返却
    expression  = "IF(HOUR(m1) == 16 AND MINUTE(m1) == 00, m1, 1)"
    return_data = true
  }
}
```

### アラーム全体の設定
アラーム全体の設定としては、メトリクス値が1以上の場合は正常として、0になるとアラーム状態になるという設定しています。

### メトリクス値の設定
メトリクス値の設定は`metric_query`で指定します。
今回は2つのメトリクス値（オリジナルのメトリクス値とMetric Mathのメトリクス値）を定義します。

### オリジナルのメトリクス値
オリジナルのメトリクス値とは、計算元になるメトリクス値です。
今回は、CloudWatch Logsのメトリクスフィルターで作成したメトリクスになります。
idは任意の文字列で大丈夫ですが、このidをMetric Mathのメトリクス値で使用します。

＜重要ポイント＞
periodを300(300秒=5分)としています。
これにより16:00のデータポイントは16:00:00~16:04:59までの期間を評価するので、
16:00のデータポイントを検証するだけで5分間にログが出力されているか確認できます。
このメトリクスの結果を直接アラームに返さないため、`return_data`は指定しません。

### Metric Mathのメトリクス値
ここが本題です！
expressionでMetric Mathを使ってメトリクス値を作成します。

`"IF(HOUR(m1) == 16 AND MINUTE(m1) == 30, m1, 1)"`

IF関数は普通のExcel等のIF関数と同じ使い方（IF (条件, 真値, 偽値)）になります。
この例では以下のようになります。
条件：取得したm1のメトリクス値のタイムスタンプの時が16時、かつ分が0分である場合
　　　　　　　　　（つまり16:00の時）
新値(16:00の時)：m1のメトリクス値をそのまま返す
偽値(16:00以外)：固定で1を返す

また、`return_data = true`としてCloudWatchアラームはこのメトリクスを評価するようします。

これにより16:00のデータポイント、つまり16:00~16:04:59に該当の文字列が出力された場合は、
1を返し正常な状態を維持する、出力を検知できなかった場合は0が返りアラーム状態になります。
こちらで設定後に正常動作を確認できました。

# 終わりに
Metric Mathには様々な関数があり、これまで無理に設定していた監視がより簡単になりそうです。
また、今回のような監視設計は今後もありそうなので、次はモジュール化して汎用性を高めることをやろうと思いました。