AWS Batch on Fargateのサーバレスなバッチ実行環境をTerraformで自動構築する(モニタリング編)

# はじめに
AWS Batch記事第2弾。[第1弾はこちら](https://qiita.com/neruneruo/items/570403dcc9c14be735d8)。

AWS Batchは標準のメトリクスでエラーをモニタリングすることが難しいので、今回はEventBridgeを使って外部に通知を行うことでモニタリング可能にする。今回はAPI Destinationsを使ってSlackに通知を行う。

前提として、本記事では、第1弾で作成した構成にAWSリソースを付け加えていくので、未読の型は第1弾から読んでいただきたい。

# API Destinationsの事前準備
API Destnationsの前準備として、Slack側の設定とIAMロールの設定が必要になる。

## Slack側の設定
[以前の別の記事でも書いたように](https://qiita.com/neruneruo/items/2cdf4fc7413d0cfbb489#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99)、クラスメソッド先生の記事を参考に、Slackのボットを作っておいて、OAuth用のトークンを取得しておこう。
OAuth用トークンはvar.slack_api_keyで指定できるようにしておく。

```HCL
variable "slack_api_key" {
  type = string
}
```

## IAMロール
IAMロールは以下のように設定する。

:::note warn
**API Destinationsのロール設計におけるハマりどころ**
API DestinationsではAPIキーの管理をする際に自動的にSecrets Managerを使うため、関連した権限を付与しておく必要がある。また、イベント処理の実行のために```events:InvokeApiDestination```の権限を付与しておく。
:::

```HCL
resource "aws_iam_role" "eventbridge" {
  name               = local.iam_eventbridge_role_name
  assume_role_policy = data.aws_iam_policy_document.eventbridge_assume.json
}

data "aws_iam_policy_document" "eventbridge_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "events.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "eventbridge" {
  role   = aws_iam_role.eventbridge.id
  name   = local.iam_eventbridge_policy_name
  policy = data.aws_iam_policy_document.eventbridge_custom.json
}

data "aws_iam_policy_document" "eventbridge_custom" {
  statement {
    effect = "Allow"

    actions = [
      "secretsmanager:CreateSecret",
      "secretsmanager:UpdateSecret",
      "secretsmanager:DescribeSecret",
      "secretsmanager:DeleteSecret",
      "secretsmanager:GetSecretValue",
      "secretsmanager:PutSecretValue"
    ]

    resources = [
      "*",
    ]
  }

  statement {
    effect = "Allow"

    actions = [
      "events:InvokeApiDestination",
    ]

    resources = [
      aws_cloudwatch_event_api_destination.batch_failed.arn,
    ]
  }
}
```

# API Destinationsの設定
## イベントルール
AWS Batchの状態変更時におけるイベント通知は、[公式のユーザーガイドのチュートリアル](https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/batch_sns_tutorial.html)に載っているので参考にする。これで、```"FAILED"```に遷移した際のイベントが拾える。

```HCL
resource "aws_cloudwatch_event_rule" "batch_failed" {
  name        = local.eventbridge_batch_failed_rule_name
  description = "AWS Batch failed event capture rule."

  is_enabled = true

  event_pattern = jsonencode({
    detail-type = [
      "Batch Job State Change"
    ]

    source = [
      "aws.batch"
    ]

    detail = {
      status = [
        "FAILED"
      ]
    }
  })
}
```

## イベントターゲット
イベントターゲットでAPI Destinationsを設定する。

なお、Slackに通知する際に何のジョブがエラーになったか分からないと困るので、ジョブ名を設定するために入力トランスフォーマー(```input_transformer```)でイベント通知されたJSONを変換する。```input_paths```で置換したい要素をJSONから取り出し変数名として定義し、```input_template```で"<>"で変数名を囲んで利用する。詳細は[公式のEventBridgeのユーザーガイド](https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-transform-input-rule.html)を参照してほしい。また、イベント通知のJSONの内容は[公式のAWS Batchのユーザーガイド](https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/batch_cwe_events.html#batch_job_events)を参照。

ここで、変なreplaceを使っているのは、```jsonencode```関数は"<"と">"をエスケープ対象として扱って置換してしまうためである。そのままの文字でSlack通知する場合はエスケープするのが正しいが、今回はあくまでもSlack通知前にAWS側で変数の置換をさせたいため、エスケープをしないのが正しい。


:::note warn
**リトライ設計を忘れずに**
外部のSaaSではRate Limitにかかる可能性もあるので、```retry_policy```を適切に設定しよう。今回のケースでは、イベント保存期間(```maximum_event_age_in_seconds```)とリトライ回数(```maximum_retry_attempts```)を設定可能な最大値に設定する。
:::

```HCL
resource "aws_cloudwatch_event_target" "batch_failed" {
  rule = aws_cloudwatch_event_rule.batch_failed.name
  arn  = aws_cloudwatch_event_api_destination.batch_failed.arn

  role_arn = aws_iam_role.eventbridge.arn

  retry_policy {
    maximum_event_age_in_seconds = 86400
    maximum_retry_attempts       = 185
  }

  input_transformer {
    input_paths = {
      jobname = "$.detail.jobName"
    }
    input_template = replace(replace(jsonencode({
      channel    = var.slack_channel_id,
      username   = "AWSモニタリング",
      icon_emoji = ":fire:",
      text       = "バッチ処理が失敗しました。処理名: <jobname>"
    }), "\\u003c", "<"), "\\u003e", ">")
  }
}
```

## API Destinations
API Destinationsの設定はたいして難しいことはない。
払い出したAPIキーを```aws_cloudwatch_event_connection```で```Authorization```ヘッダに設定しておこう。

```HCL
resource "aws_cloudwatch_event_api_destination" "batch_failed" {
  name                             = "slack"
  invocation_endpoint              = "https://slack.com/api/chat.postMessage"
  http_method                      = "POST"
  invocation_rate_limit_per_second = 1
  connection_arn                   = aws_cloudwatch_event_connection.batch_failed.arn
}

resource "aws_cloudwatch_event_connection" "batch_failed" {
  name               = "slack-connection"
  authorization_type = "API_KEY"

  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "Bearer ${var.slack_api_key}"
    }
  }
}
```

# いざ、実行！
さて、これでジョブを意図的にテキトーに失敗させてみると、以下のような通知がSlackに上がってくる。
![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/89b79340-c21d-a515-3235-fce2032c2f18.png)
成功だ！これでジョブの失敗をしても即座に気付くことができる！

しかし、これではいつ開始したジョブが失敗したのかが分からない。
ジョブの開始日時は、```$.detail.createdAt```で設定されてくるのだが、入力トランスフォーマーでこれを変換する手段がない。ちょっとイケてないんだよな……。