はじめに
仕事で AWS X-Ray を利用してアプリケーションの性能検証を行うことになり、いろいろやったのでその経験を記録として残したいと思います。
今回検証を行いたいアプリケーションの作りは以下の通りです。Next.js の Route Handler 機能を使用し Client と Service の間には BFF が存在します。
普段担当しているのがフロントエンドなので X-Ray 有効化を目指すのは Client(Next.js)⇒ServiceA(Spring)の部分です。ちなみに X-Ray の使用経験はありません。
AWS X-Ray SDK for Node.js
AWS X-Ray を有効にしたい訳なのでまずは公式を探します。
ありました。早速読んでみると・・・
X-Ray SDK for Node.js は、Express ウェブアプリケーションと Node.js Lambda 関数用のライブラリで、トレースデータを生成して X-Ray デーモンに送信するためのクラスとメソッドを提供します。
上図の通りフロントエンドのフレームワークはNext.jsなのでサーバーもNext.js製です。ExpressでもLambdaでもないのでこのSDKは使用できない説が浮上しましたが、とりあえず使い方を見てみます。
ということは自動モードが動かないだけで手動モードなら動かせるのでは??と思い(ここで夏休みに入ったため)その後の調査をメンバーに押し付けます。
(夏休みが明けるとSDKを利用してトレースする事が出来ていました。)
自動モードは、Express、Restify、Lambda アプリケーションで使用するために設計されていますが、これらのアプリケーション以外でも使用できます。独自のミドルウェアの開発、またはミドルウェアなしでの自動モードの使用の詳細については、以下の「自動モードを使用したカスタム ソリューションの開発」セクションを参照してください。
どうやらNext.jsでも自動モードで動かすことが可能なようです。
アプリケーションがサポートされているフレームワークを使用していない場合、新しいセグメントを作成し、これを SDK に設定する必要があります。CLS を使用して新しいコンテキストを作成し、そこにセグメントを保存して、SDK がそれを取得して自動キャプチャできるようにする必要があります。これを行うには、CLS 名前空間オブジェクトを使用します。これは、次の API を介して公開されます。
AWSXRay.getNamespace();
ライブラリ
cls-hooked
はコンテキストを設定するためのいくつかの方法を提供します。以下に使用例を示します。var segment = new AWSXRay.Segment(name, [optional root ID], [optional parent ID]); var ns = AWSXRay.getNamespace(); ns.run(function () { AWSXRay.setSegment(segment); // Requests using AWS SDK, HTTP calls, SQL queries... segment.close(); });
最終的に出来上がっていたのがこちら ※抜粋
import * as AWSXRay from 'aws-xray-sdk/lib';
import { NextRequest, NextResponse } from 'next/server';
const xrayDaemonAddress = process.env.AWS_XRAY_DAEMON_ADDRESS;
const xraySegmentName = process.env.AWS_XRAY_SEGMENT_NAME;
// 自動モード
AWSXRay.enableAutomaticMode();
// SDKに接続先を設定
AWSXRay.setDaemonAddress(xrayDaemonAddress);
/**
* トレース情報を収集する
* デーモンを介して、AWS X-Rayコンソールにトレース情報が連携される
*
* @param request APIへのリクエスト
* @param handler APIの実処理
* @returns {Promise<NextResponse>} レスポンスをPromiseで返却する
* @throws {Error}
*/
export function xrayTrace(
request: NextRequest,
handler: (trace_id: string) => Promise<NextResponse>,
): Promise<NextResponse> {
// セグメントを生成
const segment = new AWSXRay.Segment(xraySegmentName);
// CLS 名前空間
const ns = AWSXRay.getNamespace();
// route handlerの結果としてPromiseを返せるように
return new Promise<NextResponse>((resolve, reject) => {
ns.run(() => {
AWSXRay.setSegment(segment);
handler(segment.trace_id)
.then((nextResponse) => {
segment.close();
resolve(nextResponse);
})
.catch((error: Error) => {
segment.close();
reject(error);
});
});
});
}
// route.ts
export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from 'next/server';
import { xrayTrace } from '@/util/app/x-ray/awsXrayUtils';
export async function POST(request: NextRequest): Promise<NextResponse> {
return xrayTrace(request, async (trace_id: string) => {
const requestBody = (await request.json());
const sendBody= {
// request body
};
// Service の API を呼び出し
const purchaseorderResponse = await fetch(
`${process.env.SERVER_HOST_URL}/xxx`,
{
method: 'POST',
headers: {
// その後のSNSやSQSまでトレースを繋げるためトレースIDを渡す
'X-Amzn-Trace-Id': `Root=${trace_id}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(sendBody),
cache: 'no-store',
},
);
if (purchaseorderResponse.status === 201) {
return new NextResponse(null, {
status: 200,
});
}
return new NextResponse(`failed request`, { status: 500 });
});
}
@vercel/otel
AWS X-Ray SDK for Node.js を利用してトレース情報の収集ができるようになりましたが、 自力でセグメントの開始終了を記載するやり方となっていたため、今後APIが増えるたびに xrayTrace
を噛ませなければいけなくなりました。これでは今後の開発が面倒だったり見通しが悪いため別の方法を探ります。
Next.js が AWS X-Ray SDK for Node.js とは違う OpenTeremetry のライブラリを用意しているようです。こちらを使用する方法を試してみます。
To get started, install the following packages:
npm install @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation
Next, create a custom
instrumentation.ts
(or.js
) file in the root directory of the project (or insidesrc
folder if using one):import { registerOTel } from '@vercel/otel' export function register() { registerOTel({ serviceName: 'next-app' }) }
ドキュメント通りにパッケージをインストールし instrumentation.ts
を作成しました。
Instrumentation is currently an experimental feature, to use the
instrumentation.js
file, you must explicitly opt-in by definingexperimental.instrumentationHook = true;
in yournext.config.js
:import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { instrumentationHook: true, }, } export default nextConfig
instrumentation.ts
のページによると instrumentation の機能を有効にするには設定で明示的に指定する必要があるようなのでこれも対応しておきます。
これでトレース情報の生成はライブラリが勝手にやってくれるはずなので、次は収集するための OpenTelemetry コレクターを用意します。最終的に AWS X-Ray へデータを送信するため、AWSが用意している https://github.com/aws-observability/aws-otel-collector を利用してみます。
ローカル otel-collector
本来アプリケーションはECSにデプロイされますが、動作確認の段階ではデプロイの手間が多くて面倒なのでまずはローカルにコレクターを起動して試してみます。
ドキュメントは aws-otel-collector のサンプルアプリを使用するようになっているのでコレクターの起動部分だけを拝借して試します。
config-test.yaml
は https://github.com/aws-observability/aws-otel-collector/blob/main/examples/docker/config-test.yaml を元に微調整を加えた以下のものを用意、
extensions:
health_check:
pprof:
endpoint: 0.0.0.0:1777
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318 # 追加した
processors:
batch:
exporters:
logging:
loglevel: debug
awsxray:
region: 'ap-northeast-1' # 変更した
awsemf:
region: 'ap-northeast-1' # 変更した
service:
pipelines:
traces:
receivers: [otlp]
exporters: [awsxray]
metrics:
receivers: [otlp]
exporters: [awsemf]
extensions: [pprof]
telemetry:
logs:
level: debug
コレクター起動はssoで作成したprofileを指定してもうまくいかなかったので AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
を利用する方法を取りました。
docker run --rm -p 4317:4317 -p 4318:4318 -p 55680:55680 -p 8889:8888 \
-e AWS_REGION=ap-northeast-1 \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
-v ~/.aws:/home/aoc/.aws \
-v "${PWD}/config-test.yaml":/otel-local-config.yaml \
--name awscollector public.ecr.aws/aws-observability/aws-otel-collector:latest \
--config otel-local-config.yaml;
先ほど修正を加えたアプリケーションを起動し適当に画面をポチポチしてみます。
トレース情報がAWSコンソールから確認できるようになりました😀
ECS aws-otel-collector
ECSで動作するアプリケーションのトレースを収集するためにどうすれば良いか、AWSのドキュメントを色々読んだ結果ECSのサイドカーとしてコンテナを起動すれば良いとの結論にたどり着きました。https://aws-otel.github.io/docs/setup/ecs を参考にセットアップします。
しました。早速アプリケーションをポチポチしてみます。
トレースマップでは分かりませんがECSで動作しているアプリケーションのトレース情報を取る事ができました。next-appが最初の図のclient, xxx:8080がServiceAに当たるアプリケーションです。
イベント駆動アーキテクチャのトレースマップ
ECSで動作しているアプリケーションのトレース情報を取る事ができましたが、システムの構成ではServiceAの後にSNS, SQS, Lambda, ServiceBと続いている部分にトレースマップが続いていません。対応しましょう。
ServiceAには既にAWS X-Ray SDK for Java が組み込まれており、トレースヘッダーをリクエストに付与する事で自動的にトレースIDを引き継いでくれるはずです。その辺りは @vercel/otel
パッケージが良しなにやってくれるはずなので設定を探してみます。
•
propagators
: A set of propagators that may extend inbound and outbound contexts. By default,@vercel/otel
configures W3C Trace Context propagator.
よく分からないですが W3C Trace Context を見てみるとトレースヘッダーが AWS X-Ray のものとは違うようです。
世の中には便利なものがあるもので X-Ray 対応の propagator があるようですね。使ってみましょう。
import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray'; // 追加
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({
serviceName: 'next-app',
propagators: [new AWSXRayPropagator()], // 追加
});
}
ローカルでリクエストヘッダーをログ出力してみると x-amzn-trace-id
が付与されていました。勝ちましたね。
ECSへデプロイしてポチポチします。
おかしいですね。先ほどと同じ結果です。(面倒だったので本当に同じ画像を使用していますがSNS以降がトレースマップに出てこないという意です)
~~ 6時間くらいあっちこっちでログ出力したり原因調査 ~~
結局ローカルで確認した時には付与されていた x-amzn-trace-id
が無いことが原因で、何故無いのかというと設定でした。
•
propagateContextUrls
: A set of URL matchers (string prefix or regex) for which the tracing context should be propagated (seepropagators
). By default the context is propagated only for the deployment URLs, all other URLs should be enabled explicitly. Example:fetch: { propagateContextUrls: [ /my.api/ ] }
.
import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({
serviceName: 'next-app',
propagators: [new AWSXRayPropagator()],
instrumentationConfig: {
fetch: {
propagateContextUrls: [new RegExp(`${process.env.SERVER_HOST_URL}/*`)], // 追加
}
}
});
}
伝搬先にServiceAを指定して再チャレンジです。
SNSやlambdaがトレースマップに表示されています。やりました🎉
組み込み方まとめ
大きく分けると2つだけですね。AWSの設定はIAMロールの作成など色々やっていますがドキュメント通りに進めれば出来るので特に難しい事はありません。
-
アプリケーションに
instrumentation.ts
を作成しinstrumentationの機能を有効化するimport { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray'; import { registerOTel } from '@vercel/otel' export function register() { registerOTel({ serviceName: 'next-app', propagators: [new AWSXRayPropagator()], instrumentationConfig: { fetch: { propagateContextUrls: [new RegExp(`${process.env.SERVER_HOST_URL}/*`)], // 追加 } } }); }
import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { instrumentationHook: true, }, } export default nextConfig
-
aws-otel-collector をアプリケーションのECSサイドカーコンテナとして起動
https://aws-otel.github.io/docs/setup/ecs
おわりに
Next.js製アプリケーションのトレース情報をX-Rayに送信する事に成功しました。AWS X-Ray SDK for Node.js を見ていた時はもう無理かも知れないと思いましたが @vercel/otel
を情報提供してくれたSさんのおかげで何とかなりました、本当にありがとうございます。
参考資料
AWS X-Ray の概念
AWS Node.js SDK用 X-Ray
aws-xray-sdk-node
Setting up AWS Distro for OpenTelemetry Collector in Amazon Elastic Container Service
Next.js - instrumentation
Next.js - OpenTelemetry
@vercel/otel
Next.jsのサーバーサイド処理をX-Rayでトレースしてみた