😸
✍️ Lambda × CloudWatch Application Signals(Node.js)実践メモ:設定手順
Node.js(TypeScript開発・esbuildバンドル)で CloudWatch Application Signals を有効化した際の、
Cannot redefine property: handler
を含むハンドラ衝突問題の原因と解決を、最小構成のコード/CDK設定と併せて整理。Projen/CDK のNodejsFunction
を前提に、ハンドラ名は index.handler のままで解決する方法に絞ります。
0. ゴール(最終状態)
-
Layers:
AWSOpenTelemetryDistroJs
(例: ap-northeast-1 の:8
)+必要ならAWS Parameters and Secrets Extension
(干渉なし) -
Env:
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
(唯一の実行ラッパ) - Tracing: X-Ray Active(任意)
-
Handler:
index.handler
固定(※モジュールのエクスポート方法のみ変更) -
ビルド: esbuild(
NodejsFunction
既定)/ CJS 出力(推奨) -
ログ: コールドスタート時に
/opt/otel-instrument
の初期化メッセージが 1 回、Cannot redefine property: handler
は発生しない
1. まず最小構成をつくる(CDK)
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
const fn = new HogeNodejsFunction(this, 'HogeFn', {
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
tracing: lambda.Tracing.ACTIVE, // 任意(X-Ray)
});
// Application Signals レイヤー(リージョンの ARN を指定)
fn.addLayers(lambda.LayerVersion.fromLayerVersionArn(
this, 'AppSignalsLayer',
'arn:aws:lambda:ap-northeast-1:615299751070:layer:AWSOpenTelemetryDistroJs:8',
));
// 唯一の実行ラッパ(←これだけ使う)
fn.addEnvironment('AWS_LAMBDA_EXEC_WRAPPER', '/opt/otel-instrument');
// 必須のマネージドポリシー
fn.role?.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaApplicationSignalsExecutionRolePolicy'),
);
// (任意)Parameters & Secrets Extension は併用可(干渉しない)
// paramsAndSecrets: lambda.ParamsAndSecretsLayerVersion.fromVersion(...)
2. ハンドラ衝突の正体
症状
TypeError: Cannot redefine property: handler
at Function.defineProperty (<anonymous>)
at ... (/opt/wrapper.js:...)
-
/opt/wrapper.js
は Application Signals の内部(otel ラッパ)。exports.handler
をラップしようとして失敗。
原因
-
export const handler = ...
のような ESM 形式の書き方をバンドラ(esbuild)が CJS に落とす際、exports.handler
が 再定義不可 (non-configurable) なアクセサとして生成される場合がある。 - その結果、otel ラッパによる
exports.handler
の再定義が例外になる。
3. 根治策:ハンドラの“エクスポート方法”を変更(ハンドラ名はそのまま)
ハンドラ名 handler
を変えずに、CJS の「代入」形式でエクスポートする。
export = { handler }
(TypeScript の CommonJS 構文)
✅ 推奨:// src/xxx.lambda.ts
import type { Handler } from 'aws-lambda';
const handler: Handler = async (event, context) => {
// 実処理
return { ok: true };
};
// これで CJS の module.exports に代入され、再定義可能(writable)なプロパティになる
export = { handler };
- index.handler はそのまま(CDK/Projen 側の設定変更不要)。
- CJS 代入になるため、otel ラッパの再ラップが成功し、例外が消える。
4. テストでの import/型の取り扱い
export = { handler }
を使った場合、ESM 風の名前付き import は不可。
// ✅ OK(TS の import = 構文)// eslintを無効化
// eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs
const { handler } = require('../src/xxx.lambda');
// ❌ NG(ESM風・名前付き)
// import { handler } from '../src/xxx.lambda';
5. 検証チェックリスト
-
関数の環境変数:
-
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
のみ(NODE_OPTIONS
は未設定)
-
-
レイヤー:
-
AWSOpenTelemetryDistroJs
(+ 任意で Parameters & Secrets)。監視/APM 系の他レイヤーは無し
-
-
コールドスタートログ:
-
/opt/otel-instrument
の初期化ログが 1 回 -
Cannot redefine property: handler
が出ない
-
-
可視化:
- X-Ray にトレースが出る
- CloudWatch Application Signals のサービスマップに関数が表示
6. オプション調整(必要になってから)
- サンプリング密度:
AWS_APPLICATION_SIGNALS_SAMPLING_RATE=1
(ログ/コスト増に注意) - ログ詳細度:
AWS_APPLICATION_SIGNALS_LOG_LEVEL=info|debug
- サービス名集約(関数を1サービスに見せたい):
OTEL_SERVICE_NAME=your-service
(Application Signals 環境では必須ではない)
7. まとめ
-
衝突の本質は「バンドル産物の
exports.handler
が 再定義不可なアクセサになっている」点。 -
解決は「CJS の代入で
handler
をエクスポート」すること。ハンドラ名は変えず、Projen/CDK の既定(index.handler
)を維持したまま直せる。 - 構成は レイヤー1本+実行ラッパ1本に収斂させる。これで Application Signals を最短・安定に有効化できる。
Discussion