AWS Lambda 関数 から RDS Data API による Aurora データベースへのアクセスを X-Ray で可視化する
こんにちは、製造ビジネステクノロジー部の若槻です。
マイクロサービスやサーバーレスアーキテクチャが普及する中、複数の AWS サービスが連携するアプリケーションで、どこがパフォーマンスのボトルネックになっているかを特定するのに苦労した方も多いのではないでしょうか。かく言う私も、Lambda 関数から Aurora や外部システムなど複数のサービスを呼び出すアプリケーションの開発で、パフォーマンスの問題に直面したことがありました。
そんな課題を解決してくれるのが AWS X-Ray です。X-Ray を使うことで、リクエストがどのサービスを経由してどれくらいの時間を要しているかを視覚的に把握することができます。
今回は、API Gateway と Lambda を組み合わせた構成で Lambda 関数から RDS Data API による Aurora データベースへのアクセスを X-Ray で可視化する実装を試してみました。
やってみた
Lambda ハンドラーコード
次のように AWS SDK を使って RDS Data API で Aurora データベースにアクセスする Lambda ハンドラーを実装します。X-Ray AWS SDK for Node.js を使って RDS Data API クライアントをラップすることにより、Lambda 関数から更にダウンストリームの Aurora データベースへのアクセスを可視化することができます。
import serverlessExpress from "@codegenie/serverless-express";
import cors from "cors";
import express, { Request, Response } from "express";
import {
RDSDataClient,
ExecuteStatementCommand,
} from "@aws-sdk/client-rds-data";
import { captureAWSv3Client } from "aws-xray-sdk";
const app = express();
app.use(cors());
app.use(express.json());
// 環境変数の取得
const DATABASE_NAME = process.env.DATABASE_NAME;
const CLUSTER_ARN = process.env.CLUSTER_ARN;
const SECRET_ARN = process.env.SECRET_ARN;
// RDS Data API クライアントの初期化
const rdsDataClient = captureAWSv3Client(new RDSDataClient({})); // captureAWSv3Client を使うことで X-Ray によるトレースを有効化
app.get("/hello", async (_: Request, res: Response): Promise<void> => {
try {
// テーブルのカウントを取得するだけのクエリを実行
const command = new ExecuteStatementCommand({
resourceArn: CLUSTER_ARN,
secretArn: SECRET_ARN,
database: DATABASE_NAME,
sql: "SELECT COUNT(*) as table_count FROM information_schema.tables",
});
// 1回目のクエリ実行
const result = await rdsDataClient.send(command);
// カウント結果を取得
const tableCount = result.records?.[0]?.[0]?.longValue || 0;
res.status(200).json({
table_count: tableCount,
});
} catch (error) {
console.error("Error counting tables:", error);
res.status(500).json({
error: "Internal server error",
message: "Failed to count tables",
});
}
});
export const handler = serverlessExpress({ app });
CDK コード
続いて AWS CDK を使って AWS リソースを実装します。Lambda 関数へは API Gateway から Lambda プロキシ統合でアクセスする Lambda-lith 構成にしています。
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as aws_lambda from "aws-cdk-lib/aws-lambda";
import * as rds from "aws-cdk-lib/aws-rds";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";
export class MainStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
const DEFAULT_DATABASE_NAME = "mydatabase";
/**
* VPC の作成
*/
const vpc = new ec2.Vpc(this, "VPC", {
subnetConfiguration: [
{
cidrMask: 24,
name: "PrivateIsolated",
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
natGateways: 0,
restrictDefaultSecurityGroup: true,
});
/**
* RDS Aurora PostgreSQL クラスターの作成
*/
const auroraCluster = new rds.DatabaseCluster(this, "AuroraCluster", {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_17_4,
}),
writer: cdk.aws_rds.ClusterInstance.serverlessV2("Writer"),
vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
defaultDatabaseName: DEFAULT_DATABASE_NAME,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
/**
* Lambda-lith 関数ハンドラー
*/
const handler = new lambda_nodejs.NodejsFunction(this, "Handler", {
entry: "src/rest-api-router.ts",
tracing: aws_lambda.Tracing.ACTIVE, // AWS X-Ray によるトレースを有効化
environment: {
DATABASE_NAME: DEFAULT_DATABASE_NAME,
CLUSTER_ARN: auroraCluster.clusterArn,
SECRET_ARN: auroraCluster.secret?.secretArn || "",
},
});
auroraCluster.grantDataApiAccess(handler); // Lambda 関数に RDS Data API アクセス権限を付与
/**
* API Gateway REST API
*/
new apigateway.LambdaRestApi(this, "RestApi", {
handler,
deployOptions: {
tracingEnabled: true, // AWS X-Ray によるトレースを有効化
},
});
}
}
上記を CDK デプロイします。
動作確認
API Gateway のエンドポイントにリクエストします。
$ curl https://jaezq1ier7.execute-api.ap-northeast-1.amazonaws.com/prod/hello
{"table_count":200}
X-Ray コンソールでトレースを確認すると、次のように Aurora データベースへのアクセスが可視化されていることが確認できました。Lambda 関数の実行時間 1.47 秒のうち、Aurora へのアクセスに要した時間が 1.12 秒であることがわかります。また API Gateway から Lambda 関数へのアクセスも含めて、全体のレイテンシーが 1.99 秒であることもわかります。
このように、X-Ray を使うことで、API Gateway から Lambda 関数、さらに Lambda 関数から Aurora データベースへのアクセスのトレースを可視化することができました。
比較
Aurora に複数回アクセスした場合
先ほどは、Lambda 関数から Aurora データベースに 1 回だけアクセスしましたが、Lambda 関数から Aurora に複数回アクセスした場合は、X-Ray でのトレースは次のようになりました。1回目の Aurora へのアクセスが 1.06 秒、2 回目の Aurora へのアクセスが 540 ミリ秒であることがわかります。2回目のアクセスが速くなっているのは1回目のアクセスのコネクション再利用によるものと思われます。
ハンドラーコード
import serverlessExpress from "@codegenie/serverless-express";
import cors from "cors";
import express, { Request, Response } from "express";
import {
RDSDataClient,
ExecuteStatementCommand,
} from "@aws-sdk/client-rds-data";
import { captureAWSv3Client } from "aws-xray-sdk";
const app = express();
app.use(cors());
app.use(express.json());
// 環境変数の取得
const DATABASE_NAME = process.env.DATABASE_NAME;
const CLUSTER_ARN = process.env.CLUSTER_ARN;
const SECRET_ARN = process.env.SECRET_ARN;
// RDS Data API クライアントの初期化
const rdsDataClient = captureAWSv3Client(new RDSDataClient({}));
app.get("/hello", async (_: Request, res: Response): Promise<void> => {
try {
// テーブルのカウントを取得するだけのクエリを実行
const command = new ExecuteStatementCommand({
resourceArn: CLUSTER_ARN,
secretArn: SECRET_ARN,
database: DATABASE_NAME,
sql: "SELECT COUNT(*) as table_count FROM information_schema.tables",
});
// 1回目のクエリ実行
const result = await rdsDataClient.send(command);
// カウント結果を取得
const tableCount = result.records?.[0]?.[0]?.longValue || 0;
// 2回目のクエリ実行
await rdsDataClient.send(command);
res.status(200).json({
table_count: tableCount,
});
} catch (error) {
console.error("Error counting tables:", error);
res.status(500).json({
error: "Internal server error",
message: "Failed to count tables",
});
}
});
export const handler = serverlessExpress({ app });
captureAWSv3Client を使わない場合
一応、Lambda から Aurora へアクセスはするが、X-Ray のトレースは取得しない場合のトレース結果およびコードも載せておきます。当然ですが X-Ray コンソールでは Aurora へのアクセスは可視化されません。
ハンドラーコード
import serverlessExpress from "@codegenie/serverless-express";
import cors from "cors";
import express, { Request, Response } from "express";
import {
RDSDataClient,
ExecuteStatementCommand,
} from "@aws-sdk/client-rds-data";
const app = express();
app.use(cors());
app.use(express.json());
// 環境変数の取得
const DATABASE_NAME = process.env.DATABASE_NAME;
const CLUSTER_ARN = process.env.CLUSTER_ARN;
const SECRET_ARN = process.env.SECRET_ARN;
// RDS Data API クライアントの初期化
const rdsDataClient = new RDSDataClient({}); // captureAWSv3Client を使わない
app.get("/hello", async (_: Request, res: Response): Promise<void> => {
try {
// テーブルのカウントを取得するだけのクエリを実行
const command = new ExecuteStatementCommand({
resourceArn: CLUSTER_ARN,
secretArn: SECRET_ARN,
database: DATABASE_NAME,
sql: "SELECT COUNT(*) as table_count FROM information_schema.tables",
});
const result = await rdsDataClient.send(command);
// カウント結果を取得
const tableCount = result.records?.[0]?.[0]?.longValue || 0;
res.status(200).json({
table_count: tableCount,
});
} catch (error) {
console.error("Error counting tables:", error);
res.status(500).json({
error: "Internal server error",
message: "Failed to count tables",
});
}
});
export const handler = serverlessExpress({ app });
おわりに
API Gateway と Lambda を組み合わせた構成で Lambda 関数から RDS Data API による Aurora データベースへのアクセスを X-Ray で可視化する実装を試してみました。
Lambda ハンドラーコードの修正が要るとはいえ、X-Ray のラッパーライブラリの captureAWSv3Client
を使うだけで、Lambda 関数から Aurora データベースへのアクセスを可視化できるのはとても便利です。環境へ未導入の方は、ぜひ試してみてください。
参考
以上