EventBridge と API Destination で実現する ECS タスク失敗イベントの New Relic 連携

に公開

はじめに

コンテナ化されたアプリケーションを Amazon Elastic Container Service (Amazon ECS) にて運用する際、ECS タスクの異常終了を迅速に検知することは安定運用の鍵となります。本記事では、Amazon EventBridge と API Destination を活用して ECS タスクの失敗イベントを New Relic に送信する Terraform モジュールを紹介します。

このモジュールの最大の特徴は、AWS Lambda を使用せずに実装している点です。 これにより、ランタイムの更新やコード保守といった運用負荷を最小限に抑えることができます。

アーキテクチャ概要

このモジュールは以下の主要コンポーネントで構成されています:

  • EventBridge ルール: ECS タスクの失敗イベントをキャプチャ、特定条件でのフィルタリング
  • EventBridge ターゲット: イベントの送信先定義と New Relic 形式への変換
  • API Destination: New Relic Event API へのエンドポイント設定
  • Event Connection: API キー認証による New Relic API 接続
  • IAM ロール: 必要な権限の付与
  • Secrets Manager: New Relic 認証情報の安全な管理

イベント処理の流れは次のようになります:

  1. ECS タスク異常終了 → EventBridge イベント発生
  2. ルールによるフィルタリング → ターゲットによる形式変換
  3. API Destination による New Relic への送信

AWS Lambda を使わないメリット

AWS から外部サービスへのイベント連携には Lambda が使われることも多くあります。Lambda を使うことでより柔軟にイベント連携を実現することができますが、以下のような運用上の課題もあります:

  • ランタイムの更新: セキュリティパッチや言語バージョンアップへの対応
  • コード保守: ビジネスロジックの変更や依存ライブラリの更新
  • モニタリングとトラブルシューティング: Lambda 自体の監視やログ管理

本モジュールでは、AWS のマネージドサービスである EventBridge と API Destination を使用することで、これらの課題を解決しています。コードレスなアプローチにより、運用負荷を最小限に抑えながら、堅牢なイベント連携を実現しています。

イベントフィルタリング

本当に注意が必要なイベントだけを New Relic に送信するため、すべての ECS イベントを New Relic に送信するのではなく、以下の条件に合致するイベントのみを送信します:

  • 指定した ECS クラスターに属する ECS タスクである
  • 終了ステータスが STOPPED である
  • コンテナの終了コードが 0 以外(異常終了)である
  • スケーリングによる停止ではない

New Relic へ送信するイベント形式

New Relic には以下のような形式でカスタムイベントを送信します:

{
  "eventType": "EcsTaskFailure",
  "ruleArn": "EventBridge ルールの ARN",
  "ruleName": "EventBridge ルールの名前",
  "ingestionTime": "イベント取り込み時間",
  "id": "イベント ID",
  "time": "イベント発生時間",
  "clusterArn": "ECS クラスターの ARN",
  "desiredStatus": "希望するタスクステータス",
  "lastStatus": "最後のタスクステータス",
  "taskArn": "タスクの ARN",
  "taskDefinitionArn": "タスク定義の ARN"
}

イベント形式は New Relic の NRQL クエリで検索・分析できるように設計しています。例えば、以下のようなクエリで、特定のタスク定義における失敗した ECS タスク数を計算できます:

FROM EcsTaskFailure
SELECT count(*)
WHERE taskDefinitionArn = 'MY TASK DEFINITION ARN'

使用方法

このモジュールは以下のように簡単に使用できます:

module "ecs_task_failure_event_sender_newrelic" {
  source = "../../modules/ecs-task-failure-event-sender-newrelic"

  resource_prefix = "your-resource-prefix"
  ecs_cluster_arn = "YOUR ECS CLUSTER ARN"
}

必要な入力パラメータは以下の2つだけです:

  • resource_prefix: 作成する AWS リソースのプレフィックス
  • ecs_cluster_arn: 監視対象の ECS クラスターの ARN

Terraform モジュールのソースコード

Terraform モジュール ecs-task-failure-event-sender-newrelic のソースコードは以下のようになっています。AWS Secrets Manager から New Relic の認証情報を取得し、EventBridge を通じて ECS タスクの失敗イベントを New Relic に送信できます。

variables.tf

variable "resource_prefix" {
  type = string
}

variable "ecs_cluster_arn" {
  type = string
}

main.tf

locals {
  # New Relic のアカウント ID
  newrelic_account_id = jsondecode(data.aws_secretsmanager_secret_version.newrelic.secret_string)["ACCOUNT_ID"]
  # New Relic の API LICENSE KEY
  newrelic_api_license_key = jsondecode(data.aws_secretsmanager_secret_version.newrelic.secret_string)["API_LICENSE_KEY"]
  # New Relic に送信するカスタムイベント名
  newrelic_custom_event_name = "EcsTaskFailure"
}

data "aws_secretsmanager_secret" "newrelic" {
  # New Relic の認証情報を格納した Secrets Manager のシークレット名
  name = "newrelic"
}

data "aws_secretsmanager_secret_version" "newrelic" {
  secret_id = data.aws_secretsmanager_secret.newrelic.id
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    effect = "Allow"
    principals {
      type = "Service"
      identifiers = ["events.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "ecs_task_failure_event_target_newrelic" {
  name               = "${var.resource_prefix}-ecs-task-failure-event-target-newrelic"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "invoke_api_destination" {
  statement {
    effect = "Allow"
    actions = ["events:InvokeApiDestination"]
    resources = [aws_cloudwatch_event_api_destination.newrelic.arn]
  }
}

resource "aws_iam_policy" "invoke_api_destination" {
  name        = "${var.resource_prefix}-ecs-task-failure-event-target-newrelic"
  description = "Policy to allow invoking the New Relic API destination"
  policy      = data.aws_iam_policy_document.invoke_api_destination.json
}

resource "aws_iam_role_policy_attachment" "invoke_api_destination" {
  role       = aws_iam_role.ecs_task_failure_event_target_newrelic.name
  policy_arn = aws_iam_policy.invoke_api_destination.arn
}

resource "aws_cloudwatch_event_target" "ecs_task_failure_newrelic" {
  arn  = aws_cloudwatch_event_api_destination.newrelic.arn
  rule = aws_cloudwatch_event_rule.ecs_task_failure.name
  # See:
  # - https://docs.aws.amazon.com/eventbridge/latest/ref/events-ref-ecs.html
  # - https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html
  input_transformer {
    input_paths = {
      id : "$.id",
      time : "$.time",
      clusterArn : "$.detail.clusterArn",
      desiredStatus : "$.detail.desiredStatus",
      lastStatus : "$.detail.lastStatus",
      taskArn : "$.detail.taskArn",
      taskDefinitionArn : "$.detail.taskDefinitionArn",
    }
    # New Relic には 専用の timestamp フィールドがあるが、Unix タイムスタンプとする必要がある。
    # ECS events の time フィールドは ISO 文字列形式であるため、専用の timestamp フィールドにはマッピングしない。
    # See:
    # - https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/#instrument
    # - https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_task_events.html
    input_template = <<-EOT
    {
      "eventType" : "${local.newrelic_custom_event_name}",
      "ruleArn" : "<aws.events.rule-arn>",
      "ruleName" : "<aws.events.rule-name>",
      "ingestionTime" : "<aws.events.event.ingestion-time>",
      "id" : "<id>",
      "time" : "<time>",
      "clusterArn" : "<clusterArn>",
      "desiredStatus" : "<desiredStatus>",
      "lastStatus" : "<lastStatus>",
      "taskArn" : "<taskArn>",
      "taskDefinitionArn" : "<taskDefinitionArn>"
    }
    EOT
  }
  role_arn = aws_iam_role.ecs_task_failure_event_target_newrelic.arn
  # NOTE:
  #   このイベントが複数回送信されても問題ないため、一定回数はリトライする。設定値は調整の余地がある。
  retry_policy {
    maximum_event_age_in_seconds = 300
    maximum_retry_attempts       = 10
  }
}

# NOTE:
#   Amazon Elastic Container Service のイベントは必ず Event Bridge に送られる:
#   https://docs.aws.amazon.com/eventbridge/latest/ref/events-ref-ecs.html#events-ref-ecs-events
resource "aws_cloudwatch_event_rule" "ecs_task_failure" {
  name = "${var.resource_prefix}-ecs-task-failure-rule"
  description = "Rule to capture ECS task failures"
  # See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs_task_events.html
  event_pattern = jsonencode({
    source = ["aws.ecs"]
    detail-type = ["ECS Task State Change"]
    detail = {
      clusterArn = [var.ecs_cluster_arn]
      lastStatus = ["STOPPED"]
      containers : {
        # 終了コードが正常ではない(!=0)のイベントを対象とする
        exitCode : [
          {
            anything-but : 0
          }
        ]
      },
      # スケーリングによる停止イベントを除外する
      "stoppedReason" : [
        {
          "anything-but" : {
            "prefix" : "Scaling activity initiated by"
          }
        }
      ]
    }
  })
}

resource "aws_cloudwatch_event_api_destination" "newrelic" {
  name                             = "${var.resource_prefix}-ecs-task-failure-event-api-destination-newrelic"
  description = "New Relic Custom Event API Endpoint"
  # see: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/
  invocation_endpoint              = "https://insights-collector.newrelic.com/v1/accounts/${local.newrelic_account_id}/events"
  http_method                      = "POST"
  # New Relic のレートリミット 100,000 リクエスト/分/アカウント 以下に制限すること:
  # https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/#post-limit
  invocation_rate_limit_per_second = 10
  connection_arn                   = aws_cloudwatch_event_connection.newrelic.arn
}

resource "aws_cloudwatch_event_connection" "newrelic" {
  name               = "${var.resource_prefix}-ecs-task-failure-event-connection-newrelic"
  description        = "New Relic API Connection"
  authorization_type = "API_KEY"
  # See: https://docs.newrelic.com/docs/data-apis/ingest-apis/event-api/introduction-event-api/#submit-event
  auth_parameters {
    api_key {
      key   = "Api-Key"
      value = local.newrelic_api_license_key
    }
    invocation_http_parameters {
      header {
        key   = "Content-Type"
        value = "application/json"
      }
    }
  }
}

信頼性と耐障害性

このモジュールには、イベント送信の信頼性を高めるための機能も組み込まれています:

  • リトライポリシー: New Relic へのイベント送信に失敗した場合、最大10回まで再試行する
  • レート制限: New Relic の API レート制限に合わせて、1秒あたり10リクエストに制限する

まとめ

本記事で紹介した Terraform モジュールは、Lambda を使わずに ECS タスクの失敗イベントを New Relic に送信できる、メンテナンスフリーなソリューションです。AWS のマネージドサービスを最大限に活用することで、運用負荷を最小限に抑えながら、重要なイベントの監視を実現しています。

このアプローチは、他のイベント連携シナリオにも応用可能です。イベントの形式や送信先を変更するだけで、さまざまなユースケースに対応できる柔軟性を持っています。

ぜひ、このモジュールを活用して、コンテナ運用の信頼性向上にお役立てください。

レバテック開発部

Discussion