これは、なにをしたくて書いたもの?
AWS Lambda Powertoolsというものを、ちょっと調べておきたいなということで。
AWS Lambda Powertools
AWS Lambda Powertoolsというのは、サーバーレスのベストプラクティスに添えるように提供されている、ユーティリティ関数群
だそうです。
AWS Lambda Powertools for TypeScriptでサーバーレスのベストプラクティスを簡素化する | Amazon Web Services ブログ
プログラミング言語ごとに提供されていて、現在はPython、Java、TypeScript、.NETがあるようです。
- Homepage - Powertools for AWS Lambda (Python)
- Homepage - Powertools for AWS Lambda (Java)
- Homepage - Powertools for AWS Lambda (TypeScript)
- Powertools for AWS Lambda (.NET) - Powertools for AWS Lambda (.NET)
AWS CDKで使えるLambdaレイヤーもあります。
GitHub - aws-powertools/powertools-lambda-layer-cdk
それぞれ、言語ごとのWebページがありますが、全体のロードマップなどはこちらを見るのが良さそうです。
GitHub - aws-powertools/powertools-lambda
AWS Lambda Powertools for TypeScript
ここからは、AWS Lambda Powertools for TypeScriptについて見ていきます。
Homepage - Powertools for AWS Lambda (TypeScript)
現在のバージョンは、1.16.0です。
Homepage - Powertools for AWS Lambda (TypeScript)
導入は、npm install
またはLambdaレイヤーで行います。
Lambda Powertools for TypeScript / Install
機能については、以下のようです。
- Tracer … 同期・非同期の両方のトレーシングを行うためのユーティリティ
- Logger … Lambdaのコンテキストも合わせて使用し、構造化ログを簡単に扱えるようにするミドルウェア
- Metrics … CloudWatch Embedded Metrics Format(EMF)経由で非同期に作成されるカスタムメトリクス
- Parameters … AWS SSM Parameter Store、AWS Secrets Manager、AWS AppConfig、Amazon DynamoDBから1つ以上のパラメーターを取得する高レベル関数
- Idempotency … Lambda関数をべき等にし、ペイロードの内容に基づいた重複実行を防ぐクラスメソッドデコレーター。Middyミドルウェアとして提供される
- Batch Processing … Amazon SQS、Amazon Kinesis Data Streams、Amazon DynamoDB Streamsからのバッチ処理を行う時に部分的なエラーハンドリングを行うユーティリティ
Lambda Powertools for TypeScript / Features
それぞれの機能ごとにページもあります。
設定は、明示的な引数での指定と、環境変数での指定ができるようです。明示的な引数を指定した場合は、そちらが優先されるようです。
環境変数の一覧はこちら。
Lambda Powertools for TypeScript / Environment variables
AWS SAM、AWS CDK向けのサンプルもあるようです。
あとは実際に使って確認していきましょう。
確認はLocalStack上で行うことにします。今回は以下の2つを試してみましょう。
- Logger
- Parameters
環境
今回の環境は、こちら。
$ python3 -V Python 3.10.12 $ localstack --version 3.0.0
LocalStackの起動。
$ localstack start
AWS CLIおよびAWS SAM CLIの、LocalStack提供版。
$ awslocal --version aws-cli/2.13.37 Python/3.11.6 Linux/5.15.0-88-generic exe/x86_64.ubuntu.22 prompt/off $ samlocal --version SAM CLI, version 1.103.0
AWS SAMプロジェクトの作成とライブラリー等の最新化
AWS Lambda関数は、AWS SAMで作成、デプロイすることにします。
$ samlocal init --name lambda-powertools-logger-example --runtime nodejs18.x --app-template hello-world-typescript --package-type Zip --no-tracing --no-application-insights --no-structured-logging
TypeScriptテンプレートで作成。構造化ログのオプションは、今回はオフにしておきます。
ちなみに、AWS Lambda Powertools for TypeScriptを組み込み済みのhello-ts-ptというテンプレートもあるのですが、今回は置いておきます。
https://github.com/aws/aws-sam-cli-app-templates/tree/master/nodejs18.x/hello-ts-pt
プロジェクト内に移動。
$ cd lambda-powertools-logger-example
ディレクトリツリー。
$ tree . ├── README.md ├── events │ └── event.json ├── hello-world │ ├── app.ts │ ├── jest.config.ts │ ├── package.json │ ├── tests │ │ └── unit │ │ └── test-handler.test.ts │ └── tsconfig.json ├── samconfig.toml └── template.yaml 4 directories, 9 files
Lambda関数のディレクトリへ移動。
$ cd hello-world
package.json
は、こんな感じです。
package.json
{ "name": "hello_world", "version": "1.0.0", "description": "hello world sample for NodeJS", "main": "app.js", "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", "author": "SAM CLI", "license": "MIT", "scripts": { "unit": "jest", "lint": "eslint '*.ts' --quiet --fix", "compile": "tsc", "test": "npm run compile && npm run unit" }, "dependencies": { "esbuild": "^0.14.14" }, "devDependencies": { "@types/aws-lambda": "^8.10.92", "@types/jest": "^29.2.0", "@types/node": "^18.11.4", "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", "eslint": "^8.8.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^29.2.1", "prettier": "^2.5.1", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "typescript": "^4.8.4" } }
ライブラリを最新化。
$ DEV_PACKAGES=$(cat package.json | jq '.devDependencies | keys | .[]' | perl -wp -e 's!\n! !g; s!"!!g') $ npm uninstall $DEV_PACKAGES $ npm i -D $DEV_PACKAGES
Node.jsの型定義だけ、あらためてv18でインストール。
$ npm uninstall @types/node $ npm i -D @types/node@v18
esbuildも最新化。
$ npm uninstall esbuild $ npm i esbuild
今回は、テストまわりのライブラリーは削除しておきます。
$ npm uninstall ts-node jest ts-jest @types/jest
テストコード、設定は削除。
$ rm -rf tests $ rm jest.config.ts
この時点での依存関係。
"devDependencies": { "@types/aws-lambda": "^8.10.126", "@types/node": "^18.18.10", "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", "eslint": "^8.54.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", "prettier": "^3.1.0", "typescript": "^5.2.2" }, "dependencies": { "esbuild": "^0.19.6" }
ソースコードは移動。
$ mkdir src $ mv app.ts src
template.yaml
の以下の部分も変更しておきます。
Metadata: # Manage esbuild properties BuildMethod: esbuild BuildProperties: Minify: true Target: "esnext" Sourcemap: true EntryPoints: - src/app.ts
AWS Lambda Powertools for TypeScriptのパッケージをインストールする
では、AWS Lambda Powertools for TypeScriptを使っていきます。以下のドキュメントに習っていきます。
Logger - Powertools for AWS Lambda (TypeScript)
Parameters - Powertools for AWS Lambda (TypeScript)
まずはnpmパッケージのインストール。
$ npm i @aws-lambda-powertools/logger $ npm i @aws-lambda-powertools/parameters @aws-sdk/client-ssm
インストールされたバージョンはこちら。
"dependencies": { "@aws-lambda-powertools/logger": "^1.16.0", "@aws-lambda-powertools/parameters": "^1.16.0", "@aws-sdk/client-ssm": "^3.454.0", "esbuild": "^0.19.6" }
とりあえず、簡単に組み込んでみました。
src/app.ts
import { Logger } from '@aws-lambda-powertools/logger'; import { SSMClientConfig } from '@aws-sdk/client-ssm'; import { SSMProvider } from '@aws-lambda-powertools/parameters/ssm'; import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; const logger = new Logger(); const ssmClientConfig: SSMClientConfig = { endpoint: getSsmEndpoint() }; const parametersProvider = new SSMProvider({ clientConfig: ssmClientConfig }); export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => { console.info(`request path: ${event.path}`); logger.info(`request path: ${event.path}`); return { statusCode: 200, body: JSON.stringify({ // message: await parametersProvider.get('message', { decrypt: true }), message: await parametersProvider.get('message'), }), }; }; function getSsmEndpoint(): string { if (process.env['LOCALSTACK_HOSTNAME'] !== undefined) { return `http://${process.env['LOCALSTACK_HOSTNAME']}:4566`; } else { return 'http://localhost:4566'; } }
Loggerは、インスタンスを作成して
const logger = new Logger();
ログ出力を行うだけです。
console.info(`request path: ${event.path}`); logger.info(`request path: ${event.path}`);
表示内容を比較するために、console#info
も隣に置いておきました。
AWS SSM Parameter Storeにアクセスしている箇所はこちら。
message: await parametersProvider.get('message'),
レスポンスのメッセージに使うようにしています。
通常はこれくらいで書けるようなのですが
await getParameter('message');
今回は、LocalStack内でAWS SSM Parameter Storeにアクセスするために、AWS SDKの設定を行うのでこの形になっています。
const ssmClientConfig: SSMClientConfig = { endpoint: getSsmEndpoint() }; const parametersProvider = new SSMProvider({ clientConfig: ssmClientConfig });
Key features / Advanced / Customizing AWS SDK v3 configuration
template.yaml
には、AWS SSM Parameter Storeに登録する値を追加しておきます。
Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello-world/ Handler: app.lambdaHandler Runtime: nodejs18.x Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Metadata: # Manage esbuild properties BuildMethod: esbuild BuildProperties: Minify: true Target: "esnext" Sourcemap: true EntryPoints: - src/app.ts Message: Type: AWS::SSM::Parameter Properties: Name: message Type: SecureString Value: "Hello Lambda Powertools!!"
ビルドして、デプロイ。
$ samlocal build $ samlocal deploy --region us-east-1
確認してみます。
$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'reverse(sort_by(items[], &createdDate))[0].id' --output text) $ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello {"message":"kms:alias/aws/ssm:Hello Lambda Powertools!!"}
AWS SSM Parameter Storeから値は取れましたが、値がちょっと変ですね。これはSecureStringを復号していないからです。
ログも見てみましょう。
$ awslocal logs tail /aws/lambda/lambda-powertools-logger-example-HelloWorldFunction-fa7fe391 2023-11-19T13:40:25.265000+00:00 2023/11/19/[$LATEST]5a317705660d442f2e1b95002de6c204 START RequestId: ead30926-70e6-45d4-b7d0-76e579129fe4 Version: $LATEST 2023-11-19T13:40:25.270000+00:00 2023/11/19/[$LATEST]5a317705660d442f2e1b95002de6c204 2023-11-19T13:40:25.224Z ead30926-70e6-45d4-b7d0-76e579129fe4 INFO request path: /hello 2023-11-19T13:40:25.275000+00:00 2023/11/19/[$LATEST]5a317705660d442f2e1b95002de6c204 {"level":"INFO","message":"request path: /hello","service":"service_undefined","timestamp":"2023-11-19T13:40:25.225Z","xray_trace_id":"1-655a1049-93231bd3ad1aeb180265abe4"} 2023-11-19T13:40:25.280000+00:00 2023/11/19/[$LATEST]5a317705660d442f2e1b95002de6c204 END RequestId: ead30926-70e6-45d4-b7d0-76e579129fe4 2023-11-19T13:40:25.285000+00:00 2023/11/19/[$LATEST]5a317705660d442f2e1b95002de6c204 REPORT RequestId: ead30926-70e6-45d4-b7d0-76e579129fe4 Duration: 35.58 ms Billed Duration: 36 ms Memory Size: 128 MB Max Memory Used: 128 MB
ログの部分だけ抜き出すと、こうなりますね。
INFO request path: /hello {"level":"INFO","message":"request path: /hello","service":"service_undefined","timestamp":"2023-11-19T13:40:25.225Z","xray_trace_id":"1-655a1049-93231bd3ad1aeb180265abe4"}
確かにJSONログになっています。service
がservice_undefined
ですが。
では、このあたりを調整していきます。
まずはAWS SSM Parameter StoreのSecureStringを復号するところから。対処方法としては、以下のように復号するように指定すれば
よいのですが。
message: await parametersProvider.get('message', { decrypt: true }),
環境変数でも設定できるようなので、Loggerの方と合わせて環境変数で設定することにしましょう。
$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello {"message":"Hello Lambda Powertools!!"}
設定したtemplate.yaml
のリソース定義部分は、こちら。
Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello-world/ Handler: app.lambdaHandler Runtime: nodejs18.x Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Environment: Variables: POWERTOOLS_SERVICE_NAME: hello-world-ts-lambda # サービス名 # POWERTOOLS_DEV: true # ログを整形する。開発時のみ使用 POWERTOOLS_LOG_LEVEL: info # ログレベル POWERTOOLS_PARAMETERS_SSM_DECRYPT: true # AWS SSM Parameter Storeから取得した値を自動で復号するか否か POWERTOOLS_PARAMETERS_MAX_AGE: 5 # AWS SSM Parameter Storeから取得した値をキャッシュする秒数 Metadata: # Manage esbuild properties BuildMethod: esbuild BuildProperties: Minify: true Target: "esnext" Sourcemap: true EntryPoints: - src/app.ts Message: Type: AWS::SSM::Parameter Properties: Name: message Type: SecureString Value: "Hello Lambda Powertools!!"
このあたりですね。
Environment: Variables: POWERTOOLS_SERVICE_NAME: hello-world-ts-lambda # サービス名 # POWERTOOLS_DEV: true # ログを整形する。開発時のみ使用 POWERTOOLS_LOG_LEVEL: info # ログレベル POWERTOOLS_PARAMETERS_SSM_DECRYPT: true # AWS SSM Parameter Storeから取得した値を自動で復号するか否か POWERTOOLS_PARAMETERS_MAX_AGE: 5 # AWS SSM Parameter Storeから取得した値をキャッシュする秒数
説明はコメントで書いていますが、こちらを参照してください。
Lambda Powertools for TypeScript / Environment variables
POWERTOOLS_DEV
は、有効にするとLocalStack上だとかえって見にくくなったのでやめておきました。
あと、しれっと書いていますが、AWS SSM Parameter Storeから取得する値をキャッシュするんですね…。
確かにKey featuresを見るとそう書いています。
話を戻しましょう。
再度ビルドして、デプロイ。
$ samlocal build $ samlocal deploy --region us-east-1
確認。
$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello {"message":"Hello Lambda Powertools!!"}
今度は復号できました。
ログも見てみます。
2023-11-19T13:54:00.151000+00:00 2023/11/19/[$LATEST]09ef54f686e6b0c4f68a90d17629f214 START RequestId: c4e13c08-9088-4d64-ad0a-d01089bd7573 Version: $LATEST 2023-11-19T13:54:00.155000+00:00 2023/11/19/[$LATEST]09ef54f686e6b0c4f68a90d17629f214 2023-11-19T13:54:00.112Z c4e13c08-9088-4d64-ad0a-d01089bd7573 INFO request path: /hello 2023-11-19T13:54:00.160000+00:00 2023/11/19/[$LATEST]09ef54f686e6b0c4f68a90d17629f214 {"level":"INFO","message":"request path: /hello","service":"hello-world-ts-lambda","timestamp":"2023-11-19T13:54:00.113Z","xray_trace_id":"1-655a1378-271337c7703bfce005c2c181"} 2023-11-19T13:54:00.164000+00:00 2023/11/19/[$LATEST]09ef54f686e6b0c4f68a90d17629f214 END RequestId: c4e13c08-9088-4d64-ad0a-d01089bd7573 2023-11-19T13:54:00.169000+00:00 2023/11/19/[$LATEST]09ef54f686e6b0c4f68a90d17629f214 REPORT RequestId: c4e13c08-9088-4d64-ad0a-d01089bd7573 Duration: 33.20 ms Billed Duration: 34 ms Memory Size: 128 MB Max Memory Used: 128 MB
ログにサービス名が入るようになりました。
まずはこんなところでしょうか。
おわりに
AWS Lambda Powertools for TypeScriptのLoggerとParametersを試してみました。
ドキュメントを見ると意外と内容が多くて驚きますが、便利なものも多くあるようなのでAWS Lambda関数を書く時には見ておいた方が
良さそうですね。LoggerもParametersも、今回扱っていない機能もまだまだありますし、middyと組み合わせたりもできますし。