見出し画像

terraformのaws_cloudwatch_composite_alarm

ここ一年terraformを利用してきた中で、最近AWS CloudWatch AlarmのComposite Alarmを作成する際にほんのちょっと試行錯誤する必要があったので、忘れないうちに書き殴ろうと思います。

Composite Alarmとは

Composite Alarmとは簡単に説明すると、複数のアラームを組み合わせてひとつのアラームを作成するものとなります。
詳しくは下記を参照してください。

terraformでComposite Alarmを利用するための下準備

こちらのリポジトリに内容をまとめたのでご確認下さい。
下準備はREADMEにある通りとなります。
https://github.com/kf3225/terraform-composite-alarm
terraformでComposite Alarmを利用することができるのは、AWSのproviderのバージョンが3.24.0以上の場合となります。
また、terraformのバージョンは今回はあまり関係ないですが、現時点(2022/01/09)での最新バージョン(1.1.3)を使用するものとします。

以下はバージョンを指定するためのtfファイルとなります。
バケット名、リージョン、プロファイル、stateファイル名は適宜読み替えて下さい。

terraform {
  required_version = "1.1.3"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.24.0"
    }
  }
  backend "s3" {
    bucket  = "test-terraform-bucket-ap-northeast-1"
    key     = "dev.tfstate"
    region  = "ap-northeast-1"
    profile = "default"
  }
}
provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}

構成

リソース

フォルダ構成

terraform-composite-alarm
├── LICENSE
├── README.md
├── cloud.drawio
├── cloud.png
├── cloudwatch
│   ├── composite_alarm
│   │   └── main.tf
│   └── metric_alarm
│       └── main.tf
├── lambda
│   ├── function
│   │   └── lambda_function.py
│   └── main.tf
├── main.tf
├── provider.tf
├── sns
│   └── main.tf
└── sqs
    └── main.tf

このようなフォルダ構成となります。
terraformを実行するためのコマンドはすべてプロジェクトルート直下で実行して下さい。

実装上のポイント

実装で難しい部分はあまりないと思います。
(一部公式ドキュメント通りにいかないリソースがあるのでそれは注意)

cloudwatch/metric_alarm/main.tf

resource "aws_cloudwatch_metric_alarm" "this" {
  alarm_name          = var.cloudwatch_metric_alarm.alarm_name
  comparison_operator = var.cloudwatch_metric_alarm.comparison_operator
  evaluation_periods  = var.cloudwatch_metric_alarm.evaluation_periods
  threshold           = var.cloudwatch_metric_alarm.threshold
  alarm_description   = var.cloudwatch_metric_alarm.alarm_description
  alarm_actions       = var.cloudwatch_metric_alarm.alarm_actions
  ok_actions          = var.cloudwatch_metric_alarm.ok_actions

  dynamic "metric_query" {
    for_each = lookup(var.cloudwatch_metric_alarm, "metric_query", [])
    content {
      id          = lookup(metric_query.value, "id")
      expression  = lookup(metric_query.value, "expression", null)
      label       = lookup(metric_query.value, "label", null)
      return_data = lookup(metric_query.value, "return_data", null)

      dynamic "metric" {
        for_each = lookup(metric_query.value, "metric", [])
        content {
          metric_name = lookup(metric.value, "metric_name")
          namespace   = lookup(metric.value, "namespace")
          period      = lookup(metric.value, "period")
          stat        = lookup(metric.value, "stat")
          unit        = lookup(metric.value, "unit", null)
          dimensions  = lookup(metric.value, "dimensions", null)
        }
      }
    }
  }
}

metric_queryはlist(object)型としてmoduleの方に設定して下さい。
このlistにobjectを複数設定することでより複雑なメトリクスを作成することも可能です。
今回は5分間のLambdaのErrorsのカウントが10を超える場合にアラーム状態となるものと、
5分間のLambdaのError率が50%を超える((Errors / Invocations * 100) > 50)場合にアラーム状態となるものの2つのアラームを用意するようにしています。

lambda/function/lambda_function.py

今回作成するComposite Alarmとは直接関係ない部分ですが実装は以下の通りです。

from typing import Dict, Any

def lambda_handler(event: Dict[str, Any], context: Any):
  event_type = event.get("type", "ng")
  if event_type == "ok":
    msg = 'hello world!'
    print(msg)
    return {'result': msg, 'event_type': event_type}
  else:
    raise Exception("raise exception!: {}".format(event_type))

実装はかなり適当です。
読んで頂ければわかると思いますが、eventに、

{
  "type": "ok"
}

を渡したら関数が正常終了し、

{
  "type": "ng"
}

を渡すと(ok以外なら何でもOK笑)関数が異常終了します。

./main.tf

locals {
  alarm_rule = <<EOF
ALARM(${module.cloudwatch_metric_alarm_error_count.alarm_name}) AND
ALARM(${module.cloudwatch_metric_alarm_error_rate.alarm_name})
EOF
}

L119からの実装が上記のようになっています。
そしてこのローカル変数の使用箇所がL135であり以下のようになっています。

alarm_rule = trimspace(replace(local.alarm_rule, "/\n+/", " "))

公式ドキュメントを読むと単純にヒアドキュメントをalarm_ruleに設定していますが、改行文字をスペースで置換したり、最初から一行で表記するようにしないとエラーとなってしまうので注意が必要です。

動作確認

動かし方はREADMEを参照して下さい。
applyが正常に完了すると
Lambda, SNS, SQS, CloudWatch Alarmが3つ, 関連するロールが作成されているはずです。

最初にコンソールからアラームを確認します。

次にコンソールからLambdaページへ遷移し、

テストイベントページを開きます。
そして以下のように失敗するようなイベントを設定し、11回以上テストボタンを連打して下さい笑
(私はngを11回、okを5回ほど叩きました)

CloudWatchのページへ戻り、アラームの詳細を確認します。

まずはError Rateを見てみます。
50%以上でアラーム状態となるところ、61%ちょっととなっているため、想定通り状態が変化しているようです。

次にError Countを見てみます。
エラー11回以上でアラーム状態となりますが、その回数Lambdaが失敗しているため、こちらも想定通り状態が変化しています。

そして複合アラームの確認をします。
こちらも子アラームが2つともアラーム状態となったことにより、想定通り状態が変化しています。

最後にSQSにメッセージが飛ばされているか確認します。
コンソールからSQSページへ遷移して確認してみます。
メッセージをポーリングしたところ以下のように受信できたので成功しているようです。

メッセージの中身は下記のようになっていました。

{
  "Type" : "Notification",
  "MessageId" : "56abf44c-a75d-52f9-b10c-ae185cf5c486",
  "TopicArn" : "arn:aws:sns:ap-northeast-1:111122223333:test-project-dev-sns-topic-error-action",
  "Subject" : "ALARM: \"test-project-dev-cloudwatch-alarm-error-count-and-rate\" in Asia Pacific (Tokyo)",
  "Message" : "{\"AlarmName\":\"test-project-dev-cloudwatch-alarm-error-count-and-rate\",\"AlarmDescription\":\"Lambda function's error count and rate check.\",\"AWSAccountId\":\"111122223333\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"arn:aws:cloudwatch:ap-northeast-1:111122223333:alarm:test-project-dev-cloudwatch-alarm-error-count transitioned to ALARM at Sunday 09 January, 2022 15:04:07 UTC\",\"StateChangeTime\":\"2022-01-09T15:04:07.942+0000\",\"Region\":\"Asia Pacific (Tokyo)\",\"AlarmArn\":\"arn:aws:cloudwatch:ap-northeast-1:111122223333:alarm:test-project-dev-cloudwatch-alarm-error-count-and-rate\",\"OldStateValue\":\"OK\",\"AlarmRule\":\"ALARM(test-project-dev-cloudwatch-alarm-error-count) AND ALARM(test-project-dev-cloudwatch-alarm-error-rate)\",\"TriggeringChildren\":[{\"Arn\":\"arn:aws:cloudwatch:ap-northeast-1:111122223333:alarm:test-project-dev-cloudwatch-alarm-error-count\",\"State\":{\"Value\":\"ALARM\",\"Timestamp\":\"2022-01-09T15:04:07.942+0000\"}},{\"Arn\":\"arn:aws:cloudwatch:ap-northeast-1:111122223333:alarm:test-project-dev-cloudwatch-alarm-error-rate\",\"State\":{\"Value\":\"ALARM\",\"Timestamp\":\"2022-01-09T15:03:00.562+0000\"}}]}",
  "Timestamp" : "2022-01-09T15:04:08.029Z",
  "SignatureVersion" : "1",
  "Signature" : "XXXXX",
  "SigningCertURL" : "XXXXX",
  "UnsubscribeURL" : "XXXXX"
}

最後に

Composite Alarmを利用することによってより複雑なアラームを作成することができ、それをterraformで管理可能というのは割とメリットなのかなあと思いました。
公式ドキュメント通りやると失敗する(ヒアドキュメントの部分)箇所もありますが、全体を通してそこまで複雑に実装しなければならないわけではないのが嬉しいですね。

いいなと思ったら応援しよう!