Node.js ランタイムの Lambda 関数で、export された AWS SDK クライアントが再利用されることを確認してみた

2023.11.02

こんにちは、CX 事業本部 Delivery 部の若槻です。

Node.js ランタイムの Lambda 関数から AWS サービスにアクセスしたい場合は、AWS SDK for JavaScript v3 を使用します。

基本的にはサービスごとに AWS SDK クライアントを初期化して利用することになるのですが、この初期化処理には若干のオーバーヘッドが発生します。

そこで、初期化時のオーバーヘッド発生を抑制するために、handler 外で定義して export した AWS SDK クライアントであれば再利用されるようになるのかを確認してみました。

確認してみた

Amazon DynamoDB の AWS SDK クライアントで試してみます。

CDK コード

DynamoDB テーブル 2 つと、2 つのテーブルへ書き込みを行う Lambda 関数を作成します。

lib/cdk-sample-stack.ts

import {
  aws_lambda,
  aws_lambda_nodejs,
  aws_dynamodb,
  Stack,
  StackProps,
  RemovalPolicy,
  CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const table1 = new aws_dynamodb.Table(this, 'Table1', {
      partitionKey: {
        name: 'id',
        type: aws_dynamodb.AttributeType.STRING,
      },
      billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const table2 = new aws_dynamodb.Table(this, 'Table2', {
      partitionKey: {
        name: 'id',
        type: aws_dynamodb.AttributeType.STRING,
      },
      billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // 2つのテーブルへ put-item を1回ずつ行う Lambda 関数
    const func = new aws_lambda_nodejs.NodejsFunction(this, 'Func', {
      architecture: aws_lambda.Architecture.ARM_64,
      runtime: aws_lambda.Runtime.NODEJS_18_X,
      environment: {
        TABLE_1_NAME: table1.tableName,
        TABLE_2_NAME: table2.tableName,
      },
    });
    table1.grantWriteData(func);
    table2.grantWriteData(func);

    // Lambda 関数名を出力
    new CfnOutput(this, 'FunctionName', {
      value: func.functionName,
    });
  }
}

Lambda 関数コード

AWS SDK クライアントを初期化する処理を別ファイルで定義し、初期化後に export しています。

lib/src/ddb-client.ts

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { captureAWSv3Client } from 'aws-xray-sdk';

const region = process.env.AWS_REGION || 'ap-northeast-1';
const apiVersion = '2012-08-10';

const dynamodbClient = captureAWSv3Client(
  new DynamoDBClient({
    region,
    apiVersion,
    requestHandler: new NodeHttpHandler({
      connectionTimeout: 200,
      requestTimeout: 1000,
    }),
  })
);

console.log('DynamoDBClient initialized.');
export const dynamoDBDocument = DynamoDBDocument.from(dynamodbClient);

初期化された AWS SDK クライアントを import して利用する処理のモジュールです。処理それぞれテーブル 1 および 2 に対して 1 回ずつ put-item を行います。

lib/src/table1.ts

import { dynamoDBDocument } from './ddb-client';

const TABLE_1_NAME = process.env.TABLE_1_NAME || '';

export const putTable1Item = async (): Promise<void> => {
  await dynamoDBDocument.put({
    TableName: TABLE_1_NAME,
    Item: {
      id: '1',
      name: 'name1',
    },
  });
  console.log('putTable1Item succeeded.');
};

lib/src/table2.ts

import { dynamoDBDocument } from './ddb-client';

const TABLE_2_NAME = process.env.TABLE_2_NAME || '';

export const putTable2Item = async (): Promise<void> => {
  await dynamoDBDocument.put({
    TableName: TABLE_2_NAME,
    Item: {
      id: '1',
      name: 'name1',
    },
  });
  console.log('putTable2Item succeeded.');
};

上記 2 つのモジュールを実行する handler です。

lib/cdk-sample-stack.Func.ts

import { putTable1Item } from './src/table1';
import { putTable2Item } from './src/table2';

export const handler = async (): Promise<void> => {
  await putTable1Item();
  await putTable2Item();
};

動作確認

CDK でデプロイした前述の Lambda 関数を 2 回実行します。

$ aws lambda invoke --function-name ${functionName} outputfile.txt
$ aws lambda invoke --function-name ${functionName} outputfile.txt

1 回目の実行では、AWS SDK クライアントの初期化処理が 1 回のみ実行され、put-item は各モジュールで計 2 回実行されています。

2 回目の実行では、初期化処理は 1 回も実行されず、一方で put-item は各モジュールで計 2 回実行されています。

export された AWS SDK クライアントが、同じ Lambda 関数の実行内および、別の実行間で再利用されていることが確認できました。

おわりに

Node.js ランタイムの Lambda 関数で、export された AWS SDK クライアントが再利用されることを確認してみました。

クライアントを再利用することにより初期化時のオーバーヘッドの発生を抑えることができます。ただし TCP コネクションの確立や TLS のネゴシエーション処理など対象のサービスやリソースへの接続に必要な処理に関しては、Keep-Alive によるコネクション再利用時を除き、引き続きクライアントが使われるたびに発生する点にはご注意下さい。

以上