echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【AWS Lambda】Lambdaのログ関連の新機能をCDKで実装してみた&実際に動かしてみた

はじめに

こんにちは。

最近re:Inventもあってか、AWSのアップデートがものすごいことになってますね。
そんな中、11/16(木) にLambdaのログ周りに関する下記アップデートが発表されました。

aws.amazon.com

概要としては下記の通りです。

JSONでのログ出力をサポート

今まではテキスト形式のみでしたが、今回のアプデでJSON形式も標準でサポートしました。
これでメトリクスフィルタやAmazon Athenaなどでのフィルタ、解析がやりやすくなるのではないかと思います。

これまではJSON出力するにはアプリ側でコードを書くかAWS Powertoolsを使うしかなかったですが、Lambdaの設定だけでJSON出力が行えるようになりました。

ただAWS PowertoolsにはJSON出力以外にも便利な機能が多数ありますので、標準でJSONがサポートされたからといって不要になるなんてことは一切ありません。(by AWS Powetools大好きユーザー)

ログレベルの制御をサポート

INFO, WARN, ERROR などのログレベルの制御を、Lambdaの設定で扱えるようになりました。(開発は全部出力、本番はERRORのみ...など)

今まではアプリ側で制御用のコードを書く必要がありましたが、それが不要になりました。

出力先のCloudWatchロググループを指定可能

今までは /aws/lambda/<Lambda関数名> で固定だった 出力先のCloudWatchロググループが、任意に指定可能になりました。

例えば類似処理を行っているLambdaのログを集約することで管理がしやすくなる(かも)といったことがあります。

その他、ロググループを集約することでCloudWatchアラームも1つのロググループにまとめることができ、コスト削減につながるかもしれません。

ただし「どのLambda関数のログなのか」を識別できるようにする情報(context.functionName など)をログに加えないと分からなくなるので、その辺は注意が必要です。

なおこのロググループ集約に関しては、クラスメソッドの若槻さんがブログに記載されているので、詳しく知りたい方はそちらもご参照ください

dev.classmethod.jp

試してみる

という訳で、実際に上記アップデートを試してみます。

なお、Lambdaのリソース作成&設定はAWS CDKで行い、API Gateway経由でそのLambdaを動かしてみます。

また、今回3つのLambda関数を作成しますが、その中身は全て共通で下記のソースとなっています。(重要なのはconsole.xxxでのログ出力)

import { APIGatewayEvent, APIGatewayProxyResult, Context } from "aws-lambda";

export const handler = async (event: APIGatewayEvent, context: Context ): Promise<APIGatewayProxyResult> => {
  
  console.info(`This is ${context.functionName} INFO log`);
  
  console.info('This is info message');
  console.warn('This is warning message');
  console.error('This is error message');
  
  const result: APIGatewayProxyResult = {
    statusCode: 200,
    headers: {
      contentType: 'application/json',
    },
    body: JSON.stringify({
      status: 'success',
    }),
  };
  
  return result;
};

AWS CDK

まずはAWS CDKの定義から。
AWS CDKの定義は下記の通りです。

肝心なのは applicationLogLevel, systemLogLevel, logFormat, logGroup の 4つです

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NodejsFunction, LogLevel } from 'aws-cdk-lib/aws-lambda-nodejs';
import { RetentionDays, LogGroup } from 'aws-cdk-lib/aws-logs';
import { RestApi, LambdaIntegration } from 'aws-cdk-lib/aws-apigateway';
import path from 'path';
  
export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const customLogGroup = new LogGroup(this, 'CustomLogGroup', {
      logGroupName: '/custom/jsconf/lambda/bun',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  
    const nodejsLambda = new NodejsFunction(this, 'JsConfNodeJsFunction', {
      entry: path.resolve(__dirname, '../../lambda', 'index.ts'),
      handler: 'handler',
      timeout: cdk.Duration.seconds(30),
      environment,
      functionName: 'JsConfNodeJsFunction',
      logRetention: RetentionDays.ONE_WEEK,
      // ログレベルはどちらもINFO、フォーマットはJSON
      applicationLogLevel: 'INFO',
      systemLogLevel: 'INFO',
      logFormat: 'JSON',
    });
    
    const nodejsLambda2 = new NodejsFunction(this, 'JsConfNodeJsFunction2', {
      entry: path.resolve(__dirname, '../../lambda', 'index.ts'),
      handler: 'handler',
      timeout: cdk.Duration.seconds(30),
      environment,
      functionName: 'JsConfNodeJsFunction2',
      // ログレベルは未指定、フォーマットはText(下記注意点を参照)
      // logRetention: RetentionDays.ONE_WEEK,
      // applicationLogLevel: 'WARN',
      // systemLogLevel: 'WARN',
      logFormat: 'Text',
      logGroup: customLogGroup
    });
    
    const nodejsLambda3 = new NodejsFunction(this, 'JsConfNodeJsFunction3', {
      entry: path.resolve(__dirname, '../../lambda', 'index.ts'),
      handler: 'handler',
      timeout: cdk.Duration.seconds(30),
      environment,
      functionName: 'JsConfNodeJsFunction3',
      // logRetention: RetentionDays.ONE_WEEK,
      // アプリログはERROR、システムログはDEBUG、フォーマットはJSON
      applicationLogLevel: 'ERROR',
      systemLogLevel: 'DEBUG',
      logFormat: 'JSON',
      logGroup: customLogGroup
    });
    
    const restApi = new RestApi(this, 'JsConfRestApi', {
      restApiName: 'JsConfRestApi'
    });
    
    const nodejs = restApi.root.addResource('nodejs');
    nodejs.addMethod('GET', new LambdaIntegration(nodejsLambda));
    
    const nodejs2 = restApi.root.addResource('nodejs2');
    nodejs2.addMethod('GET', new LambdaIntegration(nodejsLambda2));
    
    const nodejs3 = restApi.root.addResource('nodejs3');
    nodejs3.addMethod('GET', new LambdaIntegration(nodejsLambda3));
  }
}

applicationLogLevel, systemLogLevel, logFormat, logGroup の 説明は以下となります。

項目 説明 設定可能な値 デフォルト値
applicationLogLevel アプリログ(console.xxxなどで出力するログ)の出力レベル設定 TRACE/DEBUG/INFO/WARN/ERROR/FATAL INFO
systemLogLevel システムログ(INIT_START, REPORTなどLambda側で自動するログ)の出力レベル設定 DEBUG/INFO/WARN INFO
logFormat ログのフォーマット Text/JSON Text
logGroup ログ出力先のCloudWatchロググループ ILogGroup(LogGroupクラスインスタンスなど) /aws/lambda/<Lambda関数名>

注意点としては下記の通りです。

  • logFormat に Textを設定した場合、applicationLogLevelおよびsystemLogLevelは指定できません。
  • logGrouplogRetentionを両方指定することはできません。(どちらか一方のみ)
  • applicationLogLevelおよびsystemLogLevelは上のコードのように文字列で直指定してください。
    • LogLevel というEnum があるのですが、これで指定するとcdk deply時にエラーになります。
    • ログレベルは「INFO」のように全部大文字で指定する必要がありますが、LogLevel は Info などPascal形式のため

実行結果

上記設定を行った各Lambdaを実行した結果のログは以下の通りです。

JsConfNodeJsFunction

  • ログがJSONで出力されています。
  • アプリログ(console.xxx)に関して、INFO/WARN/ERRORが出力されています。(INFO以上)
  • システムログについて「platform.initStart」「platform.start」「platform.report」が出力されています。
  • ロググループは /aws/lambda/<Lambda関数名> です。

JsConfNodeJsFunction2

  • ログがテキストで出力されています。
  • ロググループは(logGroupで指定した) custom/jsconf/lambda/bun です。

JsConfNodeJsFunction3

  • ログがJSONで出力されています。
  • アプリログ(console.xxx)に関して、ERRORのみが出力されています。
  • システムログについて「platform.initStart」「platform.start」「platform.report」の他、「platform.initRuntimeDone」「platform.initReport」「platform.runtimeDone」も出力されています。
  • ロググループは(logGroupで指定した) custom/jsconf/lambda/bun です。

いずれのLambda関数も、ちゃんとAWS CDKの定義通りに動作していますね。

まとめ

AWS Lambdaについて、ログ周りの機能が強化されたことで、CloudWatchとの連携(メトリクスフィルタ、アラームなど)やログレベル制御がやりやすくなりました。

開発だけではなく、運用・監視にも大いに役立ちそうな機能ですね。

では、今回はこの辺で