CLOVER🍀

That was when it all began.

AWS Lambda Powertools for TypeScript(Logger、Parameters)を試す

これは、なにをしたくて書いたもの?

AWS Lambda Powertoolsというものを、ちょっと調べておきたいなということで。

AWS Lambda Powertools

AWS Lambda Powertoolsというのは、サーバーレスのベストプラクティスに添えるように提供されている、ユーティリティ関数群
だそうです。

AWS Lambda Powertools for TypeScriptでサーバーレスのベストプラクティスを簡素化する | Amazon Web Services ブログ

プログラミング言語ごとに提供されていて、現在はPython、Java、TypeScript、.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

機能については、以下のようです。

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を見るとそう書いています。

Parameters / 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と組み合わせたりもできますし。