こしあん
2025-02-01

CloudWatch埋め込みメトリクスフォーマットを試す


3{icon} {views}


aws-embedded-metricsを活用し、LambdaログをJSON形式で出力するだけでCloudWatchメトリクスが自動作成される。TerraformでIAMやスケジュール設定を行い、三角関数の値を定期的にメトリクスとして可視化してみた。

はじめに

  • CloudWatchの埋め込みメトリクスフォーマットというものを知ったので試してみた
  • Lambda内でprintするとCloudWatch Logsに出力されるが、このフォーマットを特定のJSON形式で行うと、CloudWatchメトリクスに反映されるらすう

埋め込みメトリクスフォーマットとは

  • CloudWatchの埋め込みメトリクスフォーマット(Embedded Metrics Format, EMF)は、CloudWatch Logs内に特定のJSON形式で書き込むことでCloudWatchメトリクスとして自動的にPublishされるもの
  • ログとメトリクスを一元的に管理・分析することが容易になる

EMFを使用するメリット(By ChatGPT)

  • 統合管理: ログとメトリクスを同時に記録・分析できるため、システムの監視が効率化される。
  • 簡易設定: 特別な設定や追加のツールを導入することなく、既存のログシステムにメトリクス機能を統合できる。
  • リアルタイム分析: ログの生成と同時にメトリクスが更新されるため、リアルタイムでの監視が可能。

aws-embedded-metrics

よしなに書けるライブラリがある

aws-embedded-metrics

Python 3.12の場合、aws-embedded-metrics==3.2.0でLambdaのレイヤーZipのサイズは3.48MB。

サンプルコード

Lambdaで動かした場合のサンプルコード。以下のようなことをしている

  • 0時からの経過秒数をxとする
  • 周期を1800秒として、xのsinとxのcosをメトリクスとして公開
  • それぞれSinValueとCosValueという別のメトリクスにする
import math
from datetime import datetime, timezone
from aws_embedded_metrics import metric_scope

@metric_scope
def lambda_handler(event, context, metrics):
    # 現在のUTC時刻を取得
    now = datetime.now(timezone.utc)

    # 0時のUTC時刻を取得
    midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)

    # 0時からの経過秒数を計算
    x = (now - midnight).total_seconds()

    # 三角関数の周期
    period = 1800  # 秒

    # sinとcosの計算
    sin_val = math.sin(2 * math.pi * x / period)
    cos_val = math.cos(2 * math.pi * x / period)

    # メトリクスのNamespace(必須ではありませんが明示した方が整理しやすいです)
    metrics.set_namespace("Custom/Trigonometry")

    # ここで Dimension を設定する場合は1種類にしておく
    metrics.set_dimensions({"Function": "MyLambda"})

    # 「SinValue」メトリクスと「CosValue」メトリクスで名前を分ける
    metrics.put_metric("SinValue", sin_val, "None")
    metrics.put_metric("CosValue", cos_val, "None")

    return {
        'statusCode': 200,
        'body': 'Metrics sent successfully'
    }

結果

マネジメントコンソールからCloudWatch Metricsを見ると以下のようになる。想定された結果通り。

Lambda側のログ

以下のようなJSONが吐き出されていた

"Function": "MyLambda", "executionEnvironment": "AWS_Lambda_python3.12", "memorySize": "512", "functionVersion": "$LATEST", "logStreamId": "2025/01/31/[$LATEST]326537eaf40d4a29a164f998af531f17", "_aws": {"Timestamp": 1738338232996, "CloudWatchMetrics": [{"Dimensions": [["Function"]], "Metrics": [{"Name": "SinValue", "Unit": "None"}, {"Name": "CosValue", "Unit": "None"}], "Namespace": "Custom/Trigonometry"}]}, "SinValue": 0.2317622635813739, "CosValue": -0.9727724570420555}
  • aws-embedded-metricsを使う場合は、Dimensionを複数にしたい場合がよくわからなかった
  • 仕方ないのでValueを複数にして誤魔化した

Terraformのコード

  • Lambdaの関数を./lambda_function.pyにおく
  • Lambdaのレイヤーを./lambda_layers/aws_embedded_metrics.zipにおく
# IAM ロールの定義
resource "aws_iam_role" "lambda_role" {
  name = "cloudwatch_emf_lambda_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      },
    ]
  })
}

# IAM ポリシーの定義
resource "aws_iam_policy" "lambda_policy" {
  name        = "cloudwatch_emf_lambda_policy"
  description = "IAM policy for Lambda to write logs and metrics"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      # LambdaがCloudWatch Logsに書き込むための権限
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      },
      # CloudWatch メトリクスにアクセスするための権限
      {
        Effect = "Allow"
        Action = [
          "cloudwatch:PutMetricData"
        ]
        Resource = "*"
      }
    ]
  })
}

# Lambdaレイヤーの定義
resource "aws_lambda_layer_version" "aws_embedded_metrics" {
  layer_name          = "aws_embedded_metrics"
  description         = "AWS Embedded Metrics Library"
  compatible_runtimes = ["python3.12"]
  filename            = "${path.module}/lambda_layers/aws_embedded_metrics.zip"
  source_code_hash    = filebase64sha256("${path.module}/lambda_layers/aws_embedded_metrics.zip")
}

# IAM ロールにポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "lambda_attach_policy" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.lambda_policy.arn
}

# Lambda関数のパッケージ化
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = ".cache/lambda_function.zip"
}

# Lambda関数の定義
resource "aws_lambda_function" "cloudwatch_emf_lambda" {
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = filebase64sha256(data.archive_file.lambda_zip.output_path)
  function_name    = "CloudWatchEMFLambda"
  role             = aws_iam_role.lambda_role.arn
  handler          = "lambda_function.lambda_handler"
  runtime          = "python3.12"
  timeout          = 10
  memory_size      = 512

  layers = [
    aws_lambda_layer_version.aws_embedded_metrics.arn
  ]
}

# 定期実行
resource "aws_cloudwatch_event_rule" "every_minute" {
  name                = "EveryMinuteRule"
  description         = "Triggers Lambda every minute"
  schedule_expression = "rate(1 minute)"
}

resource "aws_cloudwatch_event_target" "lambda_target" {
  rule      = aws_cloudwatch_event_rule.every_minute.name
  target_id = "CloudWatchEMFLambda"
  arn       = aws_lambda_function.cloudwatch_emf_lambda.arn
}

resource "aws_lambda_permission" "allow_cloudwatch" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.cloudwatch_emf_lambda.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.every_minute.arn
}

所感

  • 埋め込みメトリクスのPython SDKがあんまり高度なことできなそうなので、結局boto3でメトリクス公開したほうが楽な説はある。SDK使っちゃうとレイヤーを定義しないといけなくて割とだるいかもしれない
  • ただCloudWatch Logsの検索と統合できるのが旨味ではある。あるいはサブスクリプションと組み合わせると単なるメトリクスよりも高度なことはできそう
  • 「CloudWatch Logsは使えるけど、メトリックは標準装備していないケース」で飛び道具として使うといいかも。例えばCodeBuildでCI/CDの中でメトリックを吐き出すのとか
  • 正直これなにに使うの感はある


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です