Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

EventBridgeとECSでお手軽バッチ処理基盤 (後編)

こんにちは, メディア開発部の今村です.

この記事はGunosy Advent Calendar 2022 7日目の記事です. 昨日の前編から引き続き, EventBridgeとECSでバッチ処理基盤を整備した話を紹介します.

後編は監視についてです. EventBridgeやECSの情報をDatadogに集めてモニターを設定していきます.

ECSタスクの失敗を検知する

タスクのメトリクスとログについては, サイドカーコンテナとしてDatadog agentとFluent Bitを配置することで収集できます. 方法は大体公式ドキュメントにある通りです. しかし, ドキュメントに

The ECS Fargate check does not include any events.

とあるように, この方法ではイベントは収集できないようでした.

そこで, タスクの異常終了をEventBridgeで検知し, Datadog Events APIへ転送することにしました. 転送するには, EventBridge上で

  • ECSタスクの異常終了にマッチするイベントパターン
  • Datadog APIを呼び出す際の認証情報やペイロード

を設定する必要があります. このうち, イベントパターンの記述は dev.classmethod.jp を参考にしました*1.

Datadog APIを呼び出す部分にはEventBridgeのAPI destinationsを使います. これは, ターゲットとしてエンドポイントを指定してHTTPリクエストを送る機能です. Terraformの記述は以下のようになります*2.

resource "aws_cloudwatch_event_rule" "gunosy_batch_ecs_task_failure" {
  name          = "${terraform.workspace}-ecs-task-failure"
  # クラスター上の全てのタスクの失敗にマッチするイベントパターン
  event_pattern = <<EOF
  ...
  EOF
}

resource "aws_cloudwatch_event_connection" "datadog_api" {
  name               = "${terraform.workspace}-datadog-api"
  authorization_type = "API_KEY"
  auth_parameters {
    api_key {
      key   = "DD-API-KEY"
      value = "******" # API key
    }
  }
}

resource "aws_cloudwatch_event_api_destination" "datadog_post_event" {
  name                             = "${terraform.workspace}-datadog-post-event"
  invocation_endpoint              = "https://api.datadoghq.com/api/v1/events"
  http_method                      = "POST"
  invocation_rate_limit_per_second = 5
  connection_arn                   = aws_cloudwatch_event_connection.datadog_api.arn
}

resource "aws_cloudwatch_event_target" "post_ecs_task_failure_to_datadog" {
  rule     = aws_cloudwatch_event_rule.ecs_task_failure.name
  arn      = aws_cloudwatch_event_api_destination.datadog_post_event.arn
  role_arn = "******" # events:InvokeApiDestinationを許可したIAM roleのARN
  input_transformer {
    input_paths = {
      taskArn           = "$.detail.taskArn"
      taskDefinitionArn = "$.detail.taskDefinitionArn"
      stopCode          = "$.detail.stopCode"
      stoppedReason     = "$.detail.stoppedReason"
      startedAt         = "$.detail.startedAt"
      stoppedAt         = "$.detail.stoppedAt"
    }
    input_template = <<EOF
    {
      "title": "ECS task failed",
      "text": "%%% \n*taskArn*: `<taskArn>`\n*stopCode*: `<stopCode>`\n*stoppedReason*: `<stoppedReason>`\n*startedAt*: `<startedAt>`\n*stoppedAt*: `<stoppedAt>`\n %%%",
      "tags": [
        "cluster:***",
        "task_arn:<taskArn>",
        "task_definition_arn:<taskDefinitionArn>",
        "environment:${terraform.workspace}"
      ],
      "alert_type": "error",
      "source_type_name": "amazon ecs"
    }
    EOF
  }
}

出てくるリソースが多いですが, connectionで認証情報, destinationでエンドポイントを管理し, targetの中でイベントルールと紐づける形です. input transformerの部分では, 受け取ったイベントの情報を整形し, Datadog APIを呼び出すときのtext, tagsなどのパラメーターとして使っています.

これでイベントが転送できるようになったので, Datadog側で

events("source:amazon_ecs cluster:****** status:error").rollup("count").last("5m") > 0

のようにモニターを設定すればタスクの異常終了を検知できます. 試しにタスクを異常終了させてみると, アラートが発火してSlackに以下の通知が来ます.

該当するタスクのARNやStopCodeといった情報が取得できています.

EventBridgeの失敗を検知する

タスクの起動やイベントの転送に使うEventBridgeが失敗する可能性もあるため, こちらにも監視を設定していきます.

方法としては, FailedInvocations というメトリクスを使います. これはEventBridgeの失敗回数を表す値です. このメトリクスはAWS integrationでそのままDatadogに送れるので,

change(sum(last_1h),last_5m):sum:aws.events.failed_invocations{rulename:******} > 0

のようにモニターを設定すれば失敗に気づくことができます*3.

ただし, これだけだとエラーの詳細が分からないので, DLQ (Dead Letter Queue) を併用します. DLQというのは, EventBridgeで処理に失敗したイベントを指定のSQSキューに残す機能です*4.

以上の設定をした上で, 「ECSタスクが異常終了し, イベント転送用のEventBridgeも権限不足で失敗する」というパターンの挙動を確認してみます. イベントの転送に失敗するのでタスク失敗のアラートは発火しませんが, EventBridgeの失敗に対するアラートが発火して通知が来ます.

この状態で, DLQのメッセージをコンソールから手動でポーリングし, 中身を確認してみます.

本文は以下のようになっていて, EventBridgeが処理し損ねたイベントが入っています.

{
      "title": "ECS task failed",
      "text": "%%% \n*taskArn*: `arn:aws:ecs:...
}

また, メッセージの属性には以下のような値が登録されており, どのEventBridgeがなぜ失敗したのかが分かります*5.

ERROR_CODE : NO_PERMISSIONS
ERROR_MESSAGE : Unable to invoke ApiDestination endpoint: Internal Failure
RULE_ARN : ...
TARGET_ARN : ...

タスク起動用のEventBridgeが失敗した場合も, 大体同じ様な挙動になります.

監視のまとめ

以上の設定内容を図にまとめると下のようになっています.

アラートの発火パターンは

  • タスクの起動に失敗 → FailedInvocationsのアラートが発火
  • タスクの起動に成功, 実行に成功 → 問題なし
  • タスクの起動に成功, 実行に失敗, イベント転送に成功 → タスク失敗のアラートが発火
  • タスクの起動に成功, 実行に失敗, イベント転送にも失敗 → FailedInvocationsのアラートが発火

という感じです.

おわりに

というわけで, 2日間に渡ってEventBridgeとECSでバッチ処理基盤を整備した話を紹介しました. 「サッと作れてメンテナンスの手間が少ない」という当初の目標はある程度達成できたかな, と思っています. 特に監視周りがお気に入りです. Lambdaなどを使えばより柔軟にできるとは思うのですが, そのLambdaが失敗したら…というような考慮事項も増えます. 今回は少ない構成要素かつTerraformの設定ファイルを書くだけで一通りの監視を設定できたのが嬉しかったです.

明日の記事は石川さんの「リモートモブプログラミング開発の実践」です. お楽しみに!

*1:マッチ対象のタスクをどう選ぶかという問題がありますが, 今回は専用のECSクラスターを作っているため, クラスター上の全てのタスクにマッチするよう設定しました

*2:EventBridgeは元々CloudWatch Eventという名前のサービスであり, Terraform上のリソース名は昔のままになっています

*3:対象となるルールをどう選ぶかという問題がありますが, 今回はルール名に共通のprefixを付ける形にしました

*4:DLQを使わない場合はCloudTrailのログ一覧から詳細を探し出す必要があるらしく, なかなか厳しそうです😱

*5:この情報をさらにDatadogに転送してもいいかもしれません