AWS Lambdaのタイムアウトを検知する (Terraform)

## 概要
- `AWS Lambda` のタイムアウトを検知する実装を解説します
- 実装の `Terraform` コードサンプルを提供します
- `CloudWatchLogs MetricFilter` におけるハックをお伝えします

## AWS Lambdaのタイムアウトを検知する実装
### 想定ケース
以下のように、EventBridge経由でLabmdaを実行するケースを考えます

![architecture](https://raw.githubusercontent.com/RyuyaIshibashi/great-chatgpt-quotes-bot/master/chat_architecture.jpg)

引用：

https://qiita.com/rubita/items/966bcbfd58c6e9368556

### Lambdaの仕様
Lambdaは以下の仕様を持っています
- タイムアウト設定のデフォルト値は 3 秒です
- 最大値の 900 秒 (15 分) まで 1 秒単位で調整できます
- タイムアウトエラーとなった場合、2回までリトライされます

### 望ましくないシナリオ
- Lambdaの処理時間は、ユーザー数に比例する
- ユーザー数が漸増する
- あるタイミングからLambdaのタイムアウト＋リトライが発生し始める
- 上記を検知する機構がない

### 発生する問題
このシナリオにより、以下のような問題が生じます
- Lambdaの処理が完了できていないことに気づけない
- 意味のないリトライにより、コストが増加する

### 解決方法
本質的にはタイムアウトが起こらないように処理の並行化などを検討すべきでしょうが、
まずは最低限タイムアウトが発生しないように、タイムアウトの時間を伸ばします

その上で、将来的なタイムアウトを検知できる機構を実装しておきましょう

### 実装の概要
実装の概要は以下の通りです
なお、最後の通知先はビジネスニーズによると思いますので、本記事では解説しません

- タイムアウト時、CloudWatchLogsに`"Task timed out"`というログが出力される
- 上記を検出するCloudWatchLogs MetricFilterを作成する
- MetricFilterを検知してSNSトピックに通知するMetricAlarmを設定する
- (Option) SNSの送信先をChatBotにすることでSlackに通知する

#### 概要図
![lambda-timeout-notification.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643664/9f732625-1507-488d-f16b-31a103158f39.jpeg)

## 実装のTerraformコードサンプル
それでは、実装の`Terraform` コードサンプルを提供します

### CloudWatchLogs MetricFilter
`"Task timed out"`を検知するMetricFilterを作成します

```terraform
// NOTE: Lambdaのタイムアウトを検知。以下のAWS re:Postを参考に作成
// https://repost.aws/ja/knowledge-center/lambda-verify-invocation-timeouts
resource "aws_cloudwatch_log_metric_filter" "sample_task_timed_out" {
  log_group_name = "/aws/lambda/${local.lambda_name}"
  name           = "${local.lambda_name}-task-timed-out"
  pattern        = "Task timed out"

  // 任意で設定可。CloudWatchLogs MetricAlarmで利用する
  metric_transformation {
    name      = "TaskTimedOut"
    namespace = ${local.lambda_name}
    value     = 1
  }
}
```

### CloudWatchLogs MetricAlarm & SNS Topic
作成したMetricFilterを検知するMetricAlarmを作成します
```terraform
resource "aws_cloudwatch_metric_alarm" "sample_task_timed_out" {
  alarm_name          = "${local.lambda_name}-task-timed-out"
  namespace           = local.lambda_name
  metric_name         = "TaskTimedOut"
  statistic           = "Sum"
  threshold           = 1
  period              = 60
  evaluation_periods  = 1
  datapoints_to_alarm = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_actions       = [aws_sns_topic.sample_task_timed_out.arn]
  ok_actions          = [aws_sns_topic.sample_task_timed_out.arn]
  insufficient_data_actions = [aws_sns_topic.sample_task_timed_out.arn]
  alarm_description   = <<-EOT
                          ## Lambdaがタイムアウトしました
                          Lambdaがタイムアウトし、処理は完了しませんでした
                          開発者までお知らせください
                        EOT
}

resource "aws_sns_topic" "sample_task_timed_out" {
  name = "alert-${local.lambda_name}-task-timed-out"
}
```

## CloudWatchLogs MetricFilterにおけるハック
ところで、この`"Task timed out"`では検知できずにタイムアウトが発生する場合がありました
原因は正直よくわからないのですが、その場合は`"status": "timeout"`がログに出力されるようです
仮に偽陽性になっても運用開始後に調整できるので、一旦このパターンも検出できるようにしました
同じ問題にお困りの方は参考にしてみてください

以下はterrafromのコード例です
```terraform
// NOTE: 上記のフィルターでは検出できないLambdaのタイムアウトのパターンがあるため、"status": "timeout"の場合も検知
resource "aws_cloudwatch_log_metric_filter" "sample_status_timeout" {
  log_group_name = "/aws/lambda/${local.lambda_name}"
  name           = "${local.lambda_name}-status-timeout"
  pattern        = "\"\\\"status\\\":\\\"timeout\\\"\""

  metric_transformation {
    name      = "StatusTimeout"
    namespace = local.lambda_name
    value     = 1
  }
}

resource "aws_cloudwatch_metric_alarm" "sample_status_timeout" {
  alarm_name          = "${local.lambda_name}-status-timeout"
  namespace           = local.lambda_name
  metric_name         = "StatusTimeout"
  statistic           = "Sum"
  threshold           = 1
  period              = 60
  evaluation_periods  = 1
  datapoints_to_alarm = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
  alarm_actions       = [aws_sns_topic.sample_task_timed_out.arn]
  ok_actions          = [aws_sns_topic.sample_task_timed_out.arn]
  insufficient_data_actions = [aws_sns_topic.sample_task_timed_out.arn]
  alarm_description   = <<-EOT
                          ## Lambdaがタイムアウトしました
                          Lambdaがタイムアウトし、処理は完了しませんでした
                          開発者までお知らせください
                        EOT
}
```

## 参考
以下の記事を参考にしました

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-timeout.html

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/invocation-retries.html

https://repost.aws/ja/knowledge-center/lambda-verify-invocation-timeouts