📉

【AWS CDK】LambdaからCloudWatchメトリクスに直接データをプッシュする

2024/04/12に公開

はじめに

Lambda内の特定の処理をメトリクス化する一般的な方法としては、CloudWatch Logsのメトリクスフィルタを使用し、CloudWatchメトリクスにデータを送る方法があり、そちらを案件で利用していました。例えば、ERRORという文字列を検知してメトリクスにデータをプッシュします。

この方法では、Lambdaに紐づくCloudWatch Logsのロググループそれぞれに一つずつメトリクスフィルタを追加する必要があります。具体的には、50個Lambdaがあれば50個のメトリクスフィルタを追加する必要が出てきます。
これにより、CloudFormationのリソース数上限500に引っかかる可能性が出てきたので、解消案の検討をしてみました。

解消案

複数のLambdaから1つのCloudWatchメトリクスにデータをプッシュする構成を試してみました。Lambdaの内部でSDKによってCloudWatchメトリクスにデータを送ります。

メトリクスを見る人

背景

業務で、複数のLambdaから同一の外部APIに接続する処理がありました。
外部APIでサーバーが落ちていたり、レイテンシが大きくなることがあり、その度にログを確認して調査していましたが、一括して外部APIへのリクエスト成功・失敗を簡単に確認できるメトリクスを作成したくなりました。

よくある構成がCloudWatch Logsにメトリクスフィルタを設定する構成ですが、現状CloudFormationスタックのリソース数上限(500)に達するギリギリで、かつスタック分割が難しい状況だったので、以前から気になっていたCloudWatchメトリクスに直接データをプッシュする構成を検討してみることにしました。

実装

Lambda: Node.js, TypeScript
IaC: AWS CDK v2.136.1, TypeScript

コード全体はリポジトリにあげています。

https://github.com/engineer-taro/lambda-cloud-watch-metrics

Lambdaの実装

コード全体はGitHubリポジトリにあげているので、抜粋して紹介します。
メトリクスにデータをプッシュするコードです。

    // Clientを作成
    cloudWatchClient = captureAWSv3Client(
      new CloudWatchClient({
        requestHandler: {
          connectionTimeout: 1000,
          requestTimeout: 100,
        },
        maxAttempts: 1,
      })
    );

    // データをプッシュ
    await cloudWatchClient?.send(
      new PutMetricDataCommand({
        Namespace: CW_METRIC_NAMESPACE,
        MetricData: [
          {
            MetricName: CW_METRIC_NAME,
            Value: 1,
            Unit: "Count",
            Timestamp: new Date(),
            Dimensions: [
              {
                Name: "Service",
                Value: "SampleService",
              },
            ],
          },
        ],
      })
    );

CDKの実装

こちらもコード全体はGitHubリポジトリにあげているので、抜粋して紹介します。

    // CloudWatchMetricを定義
    const cloudWatchMetric = new cw.Metric({
      namespace: "AWS/Lambda",
      metricName: "RequestSuccessCount",
      period: cdk.Duration.minutes(1),
      dimensionsMap: {
        Service: "SampleService",
      },
    });

    // Lambdaを定義
    const getUserLambda = new NodejsFunction(this, `${id}-get-user-lambda`, {
      entry: "src/get-user-handler.ts",
      handler: "handler",
      tracing: lambda.Tracing.ACTIVE,
      memorySize: 1024,
      bundling: {
        externalModules: ["aws-sdk"],
        forceDockerBundling: false,
      },
      environment: {
        CW_METRIC_NAMESPACE: cloudWatchMetric.namespace,
        CW_METRIC_NAME: cloudWatchMetric.metricName,
      },
    });

    // CloudWatchメトリクスへの権限を追加
    cw.Metric.grantPutMetricData(getUserLambda);
    const cloudWatchMetric = new cw.Metric({
      namespace: "AWS/Lambda",
      metricName: "RequestSuccessCount",
      period: cdk.Duration.minutes(1),
      dimensionsMap: {
        Service: "SampleService",
      },
    });

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Metric.html#static-grantwbrputwbrmetricwbrdatagrantee

一点混乱しやすいところとして、aws-cdk-lib » aws_cloudwatch » Metric のリソースは初期化時にリソースが作成されるわけではありません。
ここでMetricを定義することで、他リソース(Alertなど)でメトリクスを参照しやすくします。
実際にメトリクスが作成されるのは、Lambdaでデータをプッシュしたタイミングになります。
今回はnamespacemetricNameを参照するために定義しました。

動作確認

メトリクスの表示

実際にAPIGateway経由でLambdaを実行してみて、メトリクスを確認してみます。

メトリクス

実際にメトリクスが見れることを確認できました。

レイテンシ

LamndaのmemorySize: 1024MB

CloudWatchメトリクスにデータを送る処理によってLambdaの処理時間が増えてしまうのは避けたいので、どのくらい時間がかかるかX-Rayを導入して確認しました。
(負荷テストではないので、参考程度でお願いします)

20回ほど実行し、平均18ms程度でした。

レイテンシ

レイテンシ平均

また、CloudWatchクライアントの初期化についても、処理時間を確認しました。クライアントの初期化はハンドラー外に定義しているため、コールドスタート時のみ実行されます。

一度だけの確認ですが、6ms程度でした。

初期化処理

logger.info("createCwClient START");
cloudWatchClient = captureAWSv3Client(
  new CloudWatchClient({
    requestHandler: {
      connectionTimeout: 1000,
      requestTimeout: 100,
    },
    maxAttempts: 1,
  })
);
logger.info("createCwClient END");

※なお、ハンドラー外でX-Rayセグメントを取得できなかったため、loggerで計測しています。

料金

料金も気にする必要があります。

https://aws.amazon.com/jp/cloudwatch/pricing/

料金表

(2024/04/12 時点) 料金は 1,000リクエストごとに USD 0.01です。
超ざっくりですが、月ごとに30万ユーザーがそれぞれ100回ほどアクセスするとして試算すると
300,000 * 100 / 1,000 * 0.01 = USD300

サーバーレス環境にしては、そこそこ料金がかかってしまいますね。
現実的に考えると、エラーの場合のみメトリクスにプッシュするように実装すれば、コストをかなり抑えられそうです。

所感

LambdaからCloudWatchに直接プッシュする実装をしてみました。
レイテンシを気にする必要が出てきたり、ローカルで実行する際の考慮が必要になったり、色々検討事項が増えることに気がつきました。
業務の環境で採用するか、さらに検討と相談をしていこうと思います。
CloudWatchLogsにログを送り、後からメトリクスフィルタでプッシュする場合はその考慮事項が必要なくなるので、やはりベストプラクティスな構成だと思います。

何かしら制限があり、同じような構成を検討している方の参考になれば幸いです!

Discussion