AWS Lambda Powertools TypeScript がbeta releaseされたので触ってみた。

AWS Lambda Powertools TypeScriptが開発中!GAが待たれる!
2022.02.15

AWS Lambda Powertools TypeScript がbetaリリースされたと古巣のCDOが教えてくれたので触ってみました😊

AWSの中の人のツイートはこれです。

is何?

端的に言うと、Lambdaを実装する上でのUtilityライブラリをまとめたものです。

AWS Lambda Powertools はもともと、 python javaがAWS Labsから出てて、Node versionはDAZNがOSSとして公開してくれていました。今回はAWS LabsからTypeScript製のものがbetaリリースされた形です。

リポジトリ: https://github.com/awslabs/aws-lambda-powertools-typescript/

ドキュメント: https://awslabs.github.io/aws-lambda-powertools-typescript/latest/

注意

ドキュメントにも大きく掲載されているとおり、現在はあくまでbeta developer previewであり、

Do not use this library for production workloads.

と明記されています。プロダクト投入についてはGAを待ちましょう!

使い方

ライブラリは以下の4つに分かれています。使いたいものを選んでinstallするのが良さそうです。

npm install @aws-lambda-powertools/commons
npm install @aws-lambda-powertools/tracer
npm install @aws-lambda-powertools/logger
npm install @aws-lambda-powertools/metrics

Exampleをデプロイしてみる。

リポジトリにはCDKのexampleも内包されていたので、早速デプロイしてみようと思います。(npm workspace使ってるー。いいねー)

Deployする前にExampleの中身を読んで見る。

やってることとしては

  • ExampleのLambdaをそれぞれデプロイする
  • それらを2回ずつ実行する

という内容になっていました!

デプロイされるLambdaのコードの中に実装サンプルとその説明が書いてあったので、とりあえずデプロイしてみて実装とLogやMetricなどの結果を合わせて見ていこうと思います。

今度こそデプロイしてみる

じゃあREADMEにあるとおりnpm iしてー。。。というところで、lockファイルが変更されてしまいました。READMEには書いてないけど余計な差分くらわないためにnpm ciのほうが良さそうですね。ということで、トップレベルとexample/cdkでそれぞれnpm ciします。 できたらおもむろにcdk deploy

npm ci
cd examples/cdk
npm ci
npm run cdk deploy

どうなった?

CDKの中でデプロイされたLambdaを実行する仕掛けも入っているので、CDKをデプロイするだけでLogとかMetricsの結果を確認できるようになっています。 Exampleのコード辺と一緒にその結果を見ていこうと思います。

Logger

以下のようなコードを書くと、

import { Context } from 'aws-lambda';
import { Logger } from '@aws-lambda-powertools/logger';

const serviceName = 'MyFunctionWithStandardHandler';
const logger = new Logger({ logLevel: 'INFO', serviceName: serviceName });

export const handler = async (event: unknown, context: Context): Promise<void> => {
  // ### Experiment with Logger
  logger.addContext(context);
  logger.addPersistentLogAttributes({
    testKey: 'testValue',
  });
  logger.info('This is an INFO log');

  ...

↓こんなログが吐かれます。

2022-02-15T03:31:38.712Z    b7ce5cf3-e9e1-435d-aede-78b7f8cc3dec    INFO    
{
    "cold_start": true,
    "function_arn": "arn:aws:lambda:ap-northeast-1:123456789012:function:LambdaPowertoolsTypeScript-Exam-MyFunctionXXXXXXXX-XXXXXXXXXX",
    "function_memory_size": 128,
    "function_name": "LambdaPowertoolsTypeScript-Exam-MyFunctionXXXXXXXX-XXXXXXXXXX",
    "function_request_id": "b7ce5cf3-e9e1-435d-aede-78b7f8cc3dec",
    "level": "INFO",
    "message": "This is an INFO log",
    "service": "MyFunctionWithStandardHandler",
    "timestamp": "2022-02-15T03:31:38.683Z",
    "testKey": "testValue"
}

構造化ログを吐いてくれるのはDAZNのPowerToolsと同じですね。嬉しい。 addPersistentLogAttributesで、各ログに毎回埋め込まれる値をセットできるのも使い所がありそう。

Metrics

以下のようなコードを書くと、

import { Context } from 'aws-lambda';
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';

const namespace = 'CDKExample';
const serviceName = 'MyFunctionWithStandardHandler';

const metrics = new Metrics({ namespace: namespace, serviceName: serviceName });

export const handler = async (event: unknown, context: Context): Promise<void> => {
  // ### Experiment with Metrics
  metrics.captureColdStartMetric();
  metrics.throwOnEmptyMetrics();
  metrics.setDefaultDimensions({ environment: 'example', type: 'standardFunction' });
  metrics.addMetric('test-metric', MetricUnits.Count, 10);

  const metricWithItsOwnDimensions = metrics.singleMetric();
  metricWithItsOwnDimensions.addDimension('InnerDimension', 'true');
  metricWithItsOwnDimensions.addMetric('single-metric', MetricUnits.Percent, 50);

  metrics.publishStoredMetrics();
  metrics.throwOnEmptyMetrics();

  ...

以下のメトリクスがputされます。(ちょっとわかりづらそうですが。)

↓ColdStartの発生率とか分析できそう。

↓デフォルトのDimensionsをセットしておける。

↓設定を引き継いだ個別のインスタンスを生成できる。

総じて、Dimensionsをコンテキストとして扱えるのが嬉しいですね。 加えて、内部実装的にはSDKなどでputMetricDataをしておらず、console.log()をしているだけになっています。これはAmazon CloudWatch Embedded Metric Format (EMF)という仕組みで、CloudWatch Logsに出力された内容を非同期的にメトリクス化してくれる仕組みです。 これによりLambdaのランタイムでは標準出力に吐くだけなので、AWS SDKでputMetricDataを実施する場合に比べてパフォーマンスへの影響を少なくできます。さらに、内部実装的にはメトリクスで出力する内容を溜めておいてpublishStoredMetrics()を呼ばれたときにまとめて標準出力に吐くので、よりパフォーマンス面に考慮されているといえます。

一方で、コスト的にはカスタムメトリクスとCloudWatch Logsの両方のコストが計上されるので、大量にメトリクスを送信する場合には注意が必要かもしれません。

Tracer

import { Context } from 'aws-lambda';
import { Tracer } from '@aws-lambda-powertools/tracer';

const serviceName = 'MyFunctionWithStandardHandler';
const tracer = new Tracer({ serviceName: serviceName });

export const handler = async (event: unknown, context: Context): Promise<void> => {
  // Since we are in manual mode we need to create the handler segment (the 4 lines below would be done for you by decorator/middleware)
  // we do it at the beginning because we want to trace the whole duration of the handler
  const segment = tracer.getSegment(); // This is the facade segment (the one that is created by AWS Lambda)
  // Create subsegment for the function & set it as active
  const handlerSegment = segment.addNewSubsegment(`## ${process.env._HANDLER}`);
  tracer.setSegment(handlerSegment);

  // Annotate the subsegment with the cold start & serviceName
  tracer.annotateColdStart();
  tracer.addServiceNameAnnotation();

  // ...
  // なんかしら、エレガントな処理がここに入る
  // ...

  // ### Experiment with Tracer
  // This annotation & metadata will be added to the handlerSegment subsegment (## index.handler)
  tracer.putAnnotation('awsRequestId', context.awsRequestId);
  tracer.putMetadata('eventPayload', event);

  // Create another subsegment & set it as active
  const subsegment = handlerSegment.addNewSubsegment('### MySubSegment');
  tracer.setSegment(subsegment);

  let res;
  try {
    res = { foo: 'bar' };
    tracer.addResponseAsMetadata(res, process.env._HANDLER);
  } catch (err) {
    // Add the error as metadata
    subsegment.addError(err as Error, false);
    throw err;
  } finally {
    // Close subsegments (the AWS Lambda one is closed automatically)
    subsegment.close(); // (### MySubSegment)
    handlerSegment.close(); // (## index.handler)
    // Set the facade segment as active again (the one created by AWS Lambda)
    tracer.setSegment(segment);
  }

};

というような実装をすると、以下のようにX-Rayを確認できます。

セグメントごとの処理時間を確認したり、

セグメントごとにアノテーションやメタデータを埋めておくとそれらも確認することができます。

アノテーションとメタデータの使い分けとしては、アノテーションはTracesの検索に使うことができ、メタデータはネストしたJSONオブジェクトをそのまま登録できます。 アノテーションであるColdStartを用いて検索する場合はannotation.ColdStart = trueのように検索窓に入力します。

まとめ

いずれの機能もLambdaで記述されたアプリケーションをCloudWatchをより簡単に可視化するための便利な機能が備わっているという印象です。 冒頭にも述べたとおり、まだbeta developer previewです。betaの解除を待ちましょう!