🔎

CDKのNodejsFunctionでSentryを追加する

2024/01/30に公開

概要

CDKを利用してLambdaをデプロイするアプリケーションを作成しています。
Node.jsで作成しているため、constructであるNodejsFunctionを利用しています。

Sentryを追加して、エラーを検知する仕組みを作成しようとしたのですが、地味に詰まったので、経緯などを共有します。

結論

  • NodejsFunctionのオプションを設定すればOK
  • 具体的には bundling -> loaderの設定で ".node": "copy" の設定

サンプルとしては以下のような形になります。

const name = "test-function";
const stage = "dev";

new NodejsFunction(this, name, {
          functionName: `${name}-${stage}`,
          runtime: Runtime.NODEJS_18_X,
          bundling: {
            loader: {
              '.node': 'copy',
            },
          },
});

経緯

TypeScriptでLambdaの開発を行っています。
普段はServerless Frameworkを使うことが多いのですが、今回はCDKを使ってデプロイできるような構成としました。
CDK + TypeScriptの構成としてぴったりなcounstructを見つけたので、NodejsFunction を利用することにしました。

最初はエラーはCloudWatch Logsに出力する設定にしていたのですが、簡単に検知できるようにするために、Sentryを利用することにしました。
Sentryはドキュメントが充実しており、Lambdaの場合の設定方法やesbuildの場合の設定方法が案内されているものの、そのままではうまく適用できずに試行錯誤して設定を行いました。

詳細

前提

Sentryのセットアップを進めると、Lambdaでの設定方法を案内してくれます。

Sentryのinstall

まずは画像で提示されている通りに設定を行っていきます。

NodejsFunctionは内部的にはesbuildを利用してビルドを行っています。

npm install --save @sentry/serverless @sentry/profiling-node

Configureの通りにSDKをいれます。

import * as Sentry from "@sentry/serverless";
import { ProfilingIntegration } from "@sentry/profiling-node";
import { Handler } from "aws-lambda";

const environment = process.env.STAGE || 'development';

Sentry.AWSLambda.init({
  dsn: "https://xxxx.ingest.sentry.io/xxxx",
  integrations: [
    new ProfilingIntegration(),
  ],
  environment,
  debug: environment !== 'production',
  // Performance Monitoring
  tracesSampleRate: 1.0, //  Capture 100% of the transactions
  // Set sampling rate for profiling - this is relative to tracesSampleRate
  profilesSampleRate: 1.0,
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const wrapHandler = <E, R = any>(handler: Handler<E, R>) => Sentry.AWSLambda.wrapHandler(handler);

wrapHandlerは実際に利用する関数用にいったんラップしたものを用意しています。
※ 記事にまとめていて気が付きましたが、何にもしてない処理ですね。。

デプロイしてみる

cdk deploy を実行して、作成したLambdaをデプロイしました。
すると、以下のようなエラーが発生しました。

✘ [ERROR] No loader is configured for ".node" files: asset-input/node_modules/@sentry/profiling-node/lib/sentry_cpu_profiler-linux-x64-glibc-93.node

    asset-input/node_modules/@sentry/profiling-node/lib/index.js:19231:25:
      19231 │ ...eturn require("./sentry_cpu_profiler-linux-x64-glibc-93.node");
            ╵                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// 同じようなエラーが並ぶ。。

.nodeというファイルがあって、それが上手く読み込みできていないようです。
ビルド時に出たエラーなので、CDKの修正を行います。

CDK側の修正

NodejsFunctionはesbuildを利用しています。
Sentryのドキュメントを調べてみると、esbuild用のプラグインがあって、それを利用したらOKなように見えます。

https://docs.sentry.io/platforms/node/guides/aws-lambda/sourcemaps/uploading/esbuild/

が、NodejsFunctionはesbuildをCLIとして実行しているという仕様上、プラグインを利用することができないようです。

https://zenn.dev/nixieminton/articles/a26d781cd9a770

↑のリンクにあるように、NodejsFunctionを独自で拡張することで対応することはできますが、ちょっとメンテナンスが大変そうなので、別の方法を模索することにしました。

esbuildの設定変更

esbuildではloaderの設定を変更することで、対象の拡張子をどのようなファイルと見なすかを変更できます。
以前の記事では、htmlの拡張子をimportできるような設定を紹介しました。
https://zenn.dev/team_delta/articles/8072decbe5fff3

今回は .node の拡張子がNGなだけであって、いい感じに変更すれば動くのではないかと思い修正を試みました。

text

まずは、htmlの時と同様にtextにしてみました。

const name = "test-function";
const stage = "dev";

new NodejsFunction(this, name, {
  functionName: `${name}-${stage}`,
  timeout: cdk.Duration.seconds(30),
  memorySize: 1024,
  runtime: Runtime.NODEJS_18_X,
  bundling: {
    loader: {
      '.node': 'text',
    },
  },
});

ビルドは成功したので、該当のLambdaを実行したのですが、エラーが発生しました。。

{
    "errorType": "TypeError",
    "errorMessage": "Cm.startProfiling is not a function",
    "stack": [ // 省略]
}

.nodeのファイルの中身を知らないで適当にやったので、原因を調査していきます。

.nodeのファイルについて

該当の.nodeファイルを開いてみると、どうやらバイナリファイルのようでした。
esbuildのloaderのオプションを調べてみると、binaryがあったので、binaryでデプロイをしてみることにしましたが、実行時エラーが発生しました。

@sentry/profiling-node について

ビルドのエラーをよく見ると、@sentry/profiling-node の方でエラーが発生していることがわかります。
そこで、該当のリポジトリから何か読み取れることがないか見てみることにしました。

https://github.com/getsentry/profiling-node

READMEを見ていくと、どうやらエラーが発生している部分はNativeで書かれているようです。

Profiling uses native modules to interop with the v8 javascript engine which means that you may be required to build it from source.

下の方を見ていくと、esbuild向けの設定っぽい記述がありました。

{
  entryPoints: ['index.js'],
  platform: 'node',
  external: ['@sentry/profiling-node'],
}

externalに設定しろって見えるので、設定してデプロイしました。
案の定、エラーが発生しました。moduleのexternal設定してたら、まあそうなりますよね。

{
    "errorType": "Runtime.ImportModuleError",
    "errorMessage": "Error: Cannot find module '@sentry/profiling-node'\nRequire stack:\n- /var/task/index.js\n- /var/runtime/index.mjs",
    "stack": [// 省略]
}

READMEをよくよく読んでみると、以下のような記述がありました。

Example of bundling @sentry/profiling-node with esbuild and .copy loader

Loaderをcopyとすると上手くいくようです。
やっと結論と同じ部分にたどり着きました。

const name = "test-function";
const stage = "dev";

new NodejsFunction(this, name, {
          functionName: `${name}-${stage}`,
          runtime: Runtime.NODEJS_18_X,
          bundling: {
            loader: {
              '.node': 'copy',
            },
          },
});

なお、このままでは各言語ごとのNativeがコピーされてしまうので、対象のもののみコピーすることが推奨されているようです。

まとめ

  • CDKのconstructであるNodejsFunctionを利用して、Sentryのプラグインを設定できました
  • Sentryのドキュメントでは明示されてない部分でも、プラグインの情報などを合わせることで、比較的容易に設定出来ました
  • READMEはよく読んで実行しないと駄目ですね

Discussion