[アップデート] Lambda@Edgeでもログレベルの変更やJSON形式のログ出力を簡単に行えるようになりました

[アップデート] Lambda@Edgeでもログレベルの変更やJSON形式のログ出力を簡単に行えるようになりました

Lambda@Edgeのログ制御が楽になった
Clock Icon2025.04.17

Lambda@Edgeでも出力するログレベルを楽にコントロールをしたい

こんにちは、のんピ(@non____97)です。

皆さんはLambda@Edgeでも出力するログレベル楽にコントロールをしたいなと思ったことはありますか? 私はあります。

以下記事で紹介されているアップデートでログ制御に関する以下対応が行えるようになりました。

  • JSON 構造化形式での Lambda 関数ログのキャプチャ
  • Lambda 関数ログのログレベルの詳細度の制御
  • Lambda がログを送信する Amazon CloudWatch ロググループの設定

https://dev.classmethod.jp/articles/lambda-logging-update/

ただし、Lambda@Edgeはこのアップデートの恩恵を受けることはできていませんでした。Lambda@Edgeでは以下Lambda関数の機能がサポートされていません。

以下の Lambda 機能は、Lambda@Edge でサポートされていません。

  • [自動] (デフォルト) 以外の Lambda ランタイム管理設定
  • VPC 内のリソースにアクセスするための Lambda 関数の設定
  • Lambda 関数のデッドレターキュー
  • Lambda 環境変数 (自動的にサポートされる予約環境変数は除く)
  • レイヤーによる AWS Lambda 依存関係の管理を使用する Lambda 関数
  • AWS X-Ray の使用
  • Lambda プロビジョニング済み同時実行
  • コンテナイメージを使用した Lambda 関数の作成
  • arm64 アーキテクチャを使用する Lambda 関数
  • エフェメラルストレージが 512 MB を超える Lambda 関数
  • JSON 構造化形式での Lambda 関数ログのキャプチャ
  • Lambda 関数ログのログレベルの詳細度の制御
  • Lambda がログを送信する Amazon CloudWatch ロググループの設定
  • カスタマーマネージドキーを使用した .zip デプロイパッケージの暗号化

Lambda@Edge に対する制限 - Amazon CloudFront

個人的にはデバッグなどで一時的にログレベルを変更したい時が辛いところです。

通常のLambda関数であれば環境変数でログレベルを渡し、内部でその環境変数をベースにLoggerを設定することが多いと思います。

ただし、Lambda@Edgeは環境変数の設定をすることができません。つまり、ログレベルを変更する際は都度Lambda関数のコード自体を変更する必要がありました。

今回アップデートでLambda@Edgeでも先述のログ制御が行えるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/04/aws-lambda-edge-advanced-logging-controls/

これで簡単にログレベルの制御が可能になりました。

他にもJSON形式でのログ出力もLambda関数側で簡単に行える要因あったのは嬉しいポイントです。

実際に試してみたので紹介します。

やってみた

検証環境

検証環境は以下のとおりです。

CloudFront Functionsを使ってCloudFrontのキャッシュヒット率を向上させてみた検証環境構成図.png

以下記事で使用した仕組みを利用します。

https://dev.classmethod.jp/articles/cloudfront-functions-cache-key-normalization-improve-hit-rate/

この仕組みで言うとLambda@Edgeはオリジンリクエストの際にAcceptヘッダーやS3バケット上のオブジェクトを見て、WebPを返すか否かの判定をする処理を担っています。

Lambda関数のコード

Lambda@Edgeとして動作するLambda関数のコードは以下のとおりです。

./lib/src/lambda/rewrite-to-webp/index.ts
import { CloudFrontRequestEvent } from "aws-lambda";
import {
  S3Client,
  HeadObjectCommand,
  NotFound,
  S3ServiceException,
} from "@aws-sdk/client-s3";

// Constants
const IMAGE_EXTENSION_PATTERN = /\.(jpe?g|png)$/i;
const s3Client = new S3Client({
  followRegionRedirects: true,
  region: process.env.AWS_REGION,
});

/**
 * Lambda@Edge handler for WebP image conversion
 */
export const handler = async (event: CloudFrontRequestEvent) => {
  const request = event.Records[0].cf.request;
  const uri = request.uri;

  // Check WebP support using case-insensitive header check
  const acceptHeader = request.headers["accept"]?.[0]?.value ?? "";
  const viewerAcceptWebP = acceptHeader.split(",").some((type) => {
    const [mimeType, params = ""] = type.trim().split(";");

    if (mimeType === "image/webp") {
      return true;
    }

    const qMatch = params.match(/q=([0-9.]+)/);
    const q = qMatch ? parseFloat(qMatch[1]) : 1.0;

    if (q < 1) {
      return false;
    }

    return mimeType === "*/*" || mimeType === "image/*";
  });

  console.debug({
    message: "WebP support check completed",
    uri,
    viewerAcceptWebP,
    acceptHeader,
  });

  // Process if the request is for an image and browser supports WebP
  if (viewerAcceptWebP && IMAGE_EXTENSION_PATTERN.test(uri)) {
    // Extract bucket information from origin
    const s3Origin = request.origin?.s3;
    if (!s3Origin?.domainName) {
      console.debug({
        message: "S3 origin not found",
        uri,
      });

      return request;
    }

    const bucketName = s3Origin.domainName.split(".")[0];
    const webpKey = uri.startsWith("/")
      ? uri.slice(1) + ".webp"
      : uri + ".webp";

    try {
      // Check if WebP version exists
      await s3Client.send(
        new HeadObjectCommand({
          Bucket: bucketName,
          Key: webpKey,
        })
      );

      // WebP exists, modify request
      request.headers["x-original-uri"] = [
        {
          key: "x-original-uri",
          value: uri,
        },
      ];
      request.uri = `${uri}.webp`;

      console.debug({
        message: "WebP version found and request modified",
        originalUri: uri,
        newUri: request.uri,
      });
    } catch (error) {
      if (error instanceof NotFound) {
        // WebP file doesn't exist, silently use original image
        console.debug({
          message: "WebP version not found, using original image",
          uri,
          webpKey,
        });
        return request;
      }

      // Log other errors
      console.error({
        message: "Error checking WebP existence",
        region: process.env.AWS_REGION,
        bucket: bucketName,
        key: webpKey,
        error: error instanceof Error ? error.message : String(error),
        errorStack: error instanceof Error ? error.stack : undefined,
        errorType: error instanceof S3ServiceException ? error.name : "Unknown",
      });
    }
  } else {
    console.debug({
      message: "Skipping WebP processing",
      uri,
      isImage: IMAGE_EXTENSION_PATTERN.test(uri),
      supportsWebP: viewerAcceptWebP,
    });
  }

  return request;
};

ひとしきりconsole.debug()でログレベルがDEBUG以下の場合にログ出力するようにしています。

AWS CDKのコード

AWS CDKのコードは以下のとおりです。

./lib/construct/contents-delivery-construct.ts
    return new cdk.aws_lambda_nodejs.NodejsFunction(this, functionName, {
      runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
      bundling: {
        minify: true,
        tsconfig: path.join(__dirname, "../src/lambda/tsconfig.json"),
        format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
      },
      awsSdkConnectionReuse: false,
      architecture: cdk.aws_lambda.Architecture.X86_64,
      timeout: cdk.Duration.seconds(5),
      role: role,
      entry: path.join(__dirname, entry),
+     loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
+     applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.DEBUG,
+     systemLogLevelV2: cdk.aws_lambda.SystemLogLevel.INFO,
    });

アプリケーションログはログレベルをDEBUG、システムログはINFOでJSON形式でログ出力するようにしています。

アプリケーションログとシステムログで設定できるログレベルは以下のとおりです。

ログレベル 標準的な使用状況
TRACE (最も詳細) コードの実行パスを追跡するために使用される最も詳細な情報
DEBUG システムデバッグの詳細情報
INFO 関数の通常の動作を記録するメッセージ
WARN 対処しないと予期しない動作を引き起こす可能性がある潜在的なエラーに関するメッセージ
ERROR コードが期待どおりに動作しなくなる問題に関するメッセージ
FATAL (詳細度が最も低い) アプリケーションの機能停止を引き起こす重大なエラーに関するメッセージ
ログレベル 使用方法
DEBUG (詳細度が最も高い) システムデバッグの詳細情報
INFO 関数の通常の動作を記録するメッセージ
WARN (詳細度が最も低い) 対処しないと予期しない動作を引き起こす可能性がある潜在的なエラーに関するメッセージ

抜粋 : Lambda 関数の高度なログ記録コントロールの設定 - AWS Lambda

動作確認

この状態でアクセスしてみます。

> curl -I https://www.non-97.net/test.png -H "Accept:image/webp"
HTTP/2 404
content-type: text/html
content-length: 12
date: Thu, 17 Apr 2025 01:23:22 GMT
last-modified: Tue, 25 Feb 2025 02:38:39 GMT
etag: "347dfa37997b9353b3da6992f8753439"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 00e57612ea90b844bafde55ba310ccc8.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Q5PLmuIWUEt5-AS-GGslc-laYQcyIBuZrZUWDNwCnz0MrYoO496pCQ==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

> curl -I https://www.non-97.net/non__97.png -H "Accept:image/png, image/*;q=1.0"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Thu, 17 Apr 2025 01:24:22 GMT
last-modified: Tue, 25 Feb 2025 02:38:38 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 7d7a3c2bfaf3829a0c2cf20c167810ae.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: 57hbHuPqg1W3FkGA53pAk2L4GXPH4oERdb4XwDkyt8s8YeXiqco_Wg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

CloudWatch Logsで確認すると、以下ログが出力されていました。

{
    "time": "2025-04-17T01:23:18.661Z",
    "type": "platform.initStart",
    "record": {
        "initializationType": "on-demand",
        "phase": "init",
        "runtimeVersion": "nodejs:22.v35",
        "runtimeVersionArn": "arn:aws:lambda:ap-northeast-1::runtime:8ce0861cbacc1c3e68742d7537616587fd39490817ec413514cf66613b6bed7d",
        "functionName": "us-east-1.WebsiteStack-ContentsDeliveryConstructRewriteToWeb-qBTJvrRRqQ0Q",
        "functionVersion": "2",
        "instanceId": "2025/04/17/[2]b095dacd261d406a9b9ff1acd76e2f17",
        "instanceMaxMemory": 134217728
    }
}
{
    "time": "2025-04-17T01:23:19.080Z",
    "type": "platform.start",
    "record": {
        "requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
        "version": "2"
    }
}
{
    "timestamp": "2025-04-17T01:23:19.082Z",
    "level": "DEBUG",
    "requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
    "message": {
        "message": "WebP support check completed",
        "uri": "/test.png",
        "viewerAcceptWebP": true,
        "acceptHeader": "image/webp"
    }
}
{
    "timestamp": "2025-04-17T01:23:20.608Z",
    "level": "DEBUG",
    "requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
    "message": {
        "message": "WebP version not found, using original image",
        "uri": "/test.png",
        "webpKey": "test.png.webp"
    }
}
{
    "time": "2025-04-17T01:23:20.629Z",
    "type": "platform.report",
    "record": {
        "requestId": "d4ce37e3-3ded-46fb-a479-f0faeda7e5dc",
        "metrics": {
            "durationMs": 1548.054,
            "billedDurationMs": 1549,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 98,
            "initDurationMs": 415.632
        },
        "status": "success"
    }
}
{
    "time": "2025-04-17T01:24:19.901Z",
    "type": "platform.start",
    "record": {
        "requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
        "version": "2"
    }
}
{
    "timestamp": "2025-04-17T01:24:20.107Z",
    "level": "DEBUG",
    "requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
    "message": {
        "message": "WebP support check completed",
        "uri": "/non__97.png",
        "viewerAcceptWebP": true,
        "acceptHeader": "image/png, image/*;q=1.0"
    }
}
{
    "timestamp": "2025-04-17T01:24:20.949Z",
    "level": "DEBUG",
    "requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
    "message": {
        "message": "WebP version found and request modified",
        "originalUri": "/non__97.png",
        "newUri": "/non__97.png.webp"
    }
}
{
    "time": "2025-04-17T01:24:21.007Z",
    "type": "platform.report",
    "record": {
        "requestId": "67dcef0c-52da-4052-b511-6e4bb8dc9935",
        "metrics": {
            "durationMs": 1105.321,
            "billedDurationMs": 1106,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 99
        },
        "status": "success"
    }
}

アプリケーションログレベルをDEBUGからINFOに変更して再度デプロイします。

./lib/construct/contents-delivery-construct.ts
    return new cdk.aws_lambda_nodejs.NodejsFunction(this, functionName, {
      runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
      bundling: {
        minify: true,
        tsconfig: path.join(__dirname, "../src/lambda/tsconfig.json"),
        format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
      },
      awsSdkConnectionReuse: false,
      architecture: cdk.aws_lambda.Architecture.X86_64,
      timeout: cdk.Duration.seconds(5),
      role: role,
      entry: path.join(__dirname, entry),
      loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
+     applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.INFO,
-     applicationLogLevelV2: cdk.aws_lambda.ApplicationLogLevel.DEBUG,
      systemLogLevelV2: cdk.aws_lambda.SystemLogLevel.INFO,
    });

デプロイ後に同様にアクセスします。

> curl -I https://www.non-97.net/test.png -H "Accept:image/webp"
HTTP/2 404
content-type: text/html
content-length: 12
date: Thu, 17 Apr 2025 04:33:35 GMT
last-modified: Tue, 25 Feb 2025 02:38:39 GMT
etag: "347dfa37997b9353b3da6992f8753439"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 e47b43971a4fe3d8e6ac20fb2a92327c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: J16PYkurL-rG4VwgtbSG6bMNkduHSwaN4XH0nJQlwyqm33JwGjBBPg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

> curl -I https://www.non-97.net/non__97.png -H "Accept:image/png, image/*;q=1.0"
HTTP/2 200
content-type: image/webp
content-length: 5464
date: Thu, 17 Apr 2025 04:33:39 GMT
last-modified: Tue, 25 Feb 2025 02:38:38 GMT
etag: "54b0857ccfbab67746434fb9042aacff"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 fe707d17f9bf2bbec18e874a73b8a21a.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C3
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: dUzH1EJs5Yob1HlHchw7o3bxXtq_m3_l1f4lohDUtLmWUc9-fp3Bcw==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000

この時のログは以下のとおりです。

{
    "time": "2025-04-17T04:33:30.597Z",
    "type": "platform.initStart",
    "record": {
        "initializationType": "on-demand",
        "phase": "init",
        "runtimeVersion": "nodejs:22.v35",
        "runtimeVersionArn": "arn:aws:lambda:ap-northeast-1::runtime:8ce0861cbacc1c3e68742d7537616587fd39490817ec413514cf66613b6bed7d",
        "functionName": "us-east-1.WebsiteStack-ContentsDeliveryConstructRewriteToWeb-qBTJvrRRqQ0Q",
        "functionVersion": "3",
        "instanceId": "2025/04/17/[3]945c29df82034ba3bed42db43a145b85",
        "instanceMaxMemory": 134217728
    }
}
{
    "time": "2025-04-17T04:33:31.088Z",
    "type": "platform.start",
    "record": {
        "requestId": "8d3b0c27-475b-4ada-aa6b-95f6e33d39a4",
        "version": "3"
    }
}
{
    "time": "2025-04-17T04:33:32.588Z",
    "type": "platform.report",
    "record": {
        "requestId": "8d3b0c27-475b-4ada-aa6b-95f6e33d39a4",
        "metrics": {
            "durationMs": 1499.572,
            "billedDurationMs": 1500,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 100,
            "initDurationMs": 487.745
        },
        "status": "success"
    }
}
{
    "time": "2025-04-17T04:33:37.665Z",
    "type": "platform.start",
    "record": {
        "requestId": "a521e81b-fc2d-407f-b1d0-f28ea8b1ef3b",
        "version": "3"
    }
}
{
    "time": "2025-04-17T04:33:37.907Z",
    "type": "platform.report",
    "record": {
        "requestId": "a521e81b-fc2d-407f-b1d0-f28ea8b1ef3b",
        "metrics": {
            "durationMs": 241.122,
            "billedDurationMs": 242,
            "memorySizeMB": 128,
            "maxMemoryUsedMB": 100
        },
        "status": "success"
    }
}

はい、先ほど出力されていたDEBUGログが出力されなくなりました。

意図した通りに動作していますね。

Lambda@Edgeのログ制御が楽になった

Lambda@Edgeでもログレベルの変更やJSON形式のログ出力を簡単に行えるようになったアップデートを紹介しました。

ログレベルの切り替えが面倒だなと思っていた方にとっては朗報ではないでしょうか。嬉しいアップデートです。

また、個人的にはログ分析の観点からJSON形式でのログ出力がオススメです。ログ分析基盤との兼ね合いを考慮しながら、JSON形式で出力できるならJSON形式で出力すると良いかと思います。

今回の検証で使用したリポジトリは以下に保存しています。

https://github.com/non-97/cloudfront-s3-website/tree/v6.0.0

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.