😸

✍️ 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. 検証チェックリスト

  1. 関数の環境変数:

    • AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument のみ(NODE_OPTIONS は未設定)
  2. レイヤー:

    • AWSOpenTelemetryDistroJs(+ 任意で Parameters & Secrets)。監視/APM 系の他レイヤーは無し
  3. コールドスタートログ:

    • /opt/otel-instrument の初期化ログが 1 回
    • Cannot redefine property: handler が出ない
  4. 可視化:

    • 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