CDKのNodejsFunctionでSentryを追加する
概要
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なように見えます。
が、NodejsFunctionはesbuildをCLIとして実行しているという仕様上、プラグインを利用することができないようです。
↑のリンクにあるように、NodejsFunctionを独自で拡張することで対応することはできますが、ちょっとメンテナンスが大変そうなので、別の方法を模索することにしました。
esbuildの設定変更
esbuildではloaderの設定を変更することで、対象の拡張子をどのようなファイルと見なすかを変更できます。
以前の記事では、htmlの拡張子をimportできるような設定を紹介しました。
今回は .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
の方でエラーが発生していることがわかります。
そこで、該当のリポジトリから何か読み取れることがないか見てみることにしました。
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