CloudWatch埋め込みメトリクスフォーマットを試す
Posted On 2025-02-01
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
よしなに書けるライブラリがある
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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー