1 つの Lambda 関数でテナントごとに実行環境を分離する新機能を試してみた

記事タイトルとURLをコピーする

はじめに

アプリケーションサービス本部ディベロップメントサービス1課の森山です。

2025/11/19 のアップデートで、AWS Lambda にテナント分離機能(Tenant Isolation Mode)が追加されました。

aws.amazon.com

この機能を使うと、テナントごとに実行環境を分離することができ、新たなテナント分離のパターンが実現できます。

本記事では、AWS SAM を使ってテナント分離機能を実装し、挙動を確認してみます。

テナント分離機能とは

マルチテナント環境では、複数の顧客(テナント)が同じアプリケーションを共有しますが、この際、各テナントのデータやリソースを適切に分離することが重要です。

テナントが他のテナントのリソースにアクセスできてしまうことはビジネスに非常に大きな損害を与える可能性があり、マルチテナント環境構築時には、テナント分離戦略を検討しておく必要があります。

docs.aws.amazon.com

これまで Lambda を使用し、実行環境単位でテナント分離を行う場合は、テナントごとに Lambda 関数を用意する必要がありました。

API Gateway を一つにする方法など、いくつか方法はありますが、このような例です。

この方法は完全な分離を実現できますが、テナント数分の関数管理(ルーティング、デプロイ、監視等)が必要になります。

今回新規追加された Lambda のテナント分離機能を使うと、1 つの Lambda 関数でテナントごとに独立した実行環境を作成できます。

通常の Lambda では実行環境が再利用されるため前回の呼び出しのデータが残る可能性がありますが、テナント分離を有効にすると異なるテナント間で実行環境が共有されることがありません。

やってみた

では、動作確認をしていきます。 今回は API Gateway 経由で Lambda を呼び出すパターンで検討してみます。

完成品は下記で公開しております。

github.com

ソースの準備

今回は手軽に環境を構築できる、AWS SAM の TypeScript 版Hello World Exampleをベースにテナント分離を実施していきます。

github.com

sam initコマンドにて作成した結果は以下のとおりです。

    -----------------------
    Generating application:
    -----------------------
    Name: lambda-tenant-isolate
    Runtime: nodejs22.x
    Architectures: x86_64
    Dependency Manager: npm
    Application Template: hello-world-typescript
    Output Directory: .
    Configuration file: lambda-tenant-isolate/samconfig.toml

    Next steps can be found in the README file at lambda-tenant-isolate/README.md

TenancyConfig の設定

まずは、Lambda 関数にテナント分離を設定します。

SAM テンプレートにTenancyConfigプロパティを追加します。

docs.aws.amazon.com

HelloWorldFunction:
  Type: AWS::Serverless::Function
  Properties:
    TenancyConfig:
      TenantIsolationMode: PER_TENANT

この設定は、新規作成時のみ可能です。

既存の Lambda を後からテナント分離モードに変更することはできません。

なお、2025/11/26 時点の SAM CLI 最新バージョン、 v1.148.0 ではsam validate時にエラーが出ますが、sam deployは正常に動作します。

[[E3002: Resource properties are invalid] (Additional properties are not allowed ('TenancyConfig' was unexpected)) matched 14] Error: Linting failed. At least one linting rule was matched to the provided template.

なお、この時点で、一度ローカル上で、テナント分離の動作確認することも可能です。

シンプルなsam local invokeコマンドを実行すると、The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again.のエラーが返ってきます。

% sam local invoke
No current session found, using default AWS::AccountId
Invoking app.lambdaHandler (nodejs22.x)
Error: The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again.

テナント分離をした場合、必ず指定された方法でテナントコンテキストをリクエスト情報に含める必要があります。

docs.aws.amazon.com

マニュアルの通り、sam local invoke --tenant-id test-tenant-001のように、テナント ID を指定することで、Lambda までリクエストが届きます。

% sam local invoke --tenant-id test-tenant-001
Invoking app.lambdaHandler (nodejs22.x)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/nodejs:22-rapid-x86_64.

SAM_CONTAINER_ID: 5d5919b93a1d6010109e6d9a2e7d491cf8cc18c291cfa82c44dd3f3079a71e96
START RequestId: 0884c8cc-8c41-4b53-bf12-da4e5683158e Version: $LATEST
END RequestId: 4f614492-dd50-410a-9d0e-591a91f9e855
REPORT RequestId: 4f614492-dd50-410a-9d0e-591a91f9e855  Init Duration: 0.27 ms  Duration: 624.18 ms     Billed Duration: 625 ms Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\":\"hello world\"}"}

API Gateway の設定

次に API Gateway 経由で動作確認してみます。

API Gateway 経由で、テナント ID を渡すためには、カスタムヘッダーからX-Amz-Tenant-Idヘッダーへのマッピングが必要です。

少し手間なのですが、X-Amz-*は AWS 側で予約されているヘッダーのため、クライアントから直接指定できません。(ドキュメントが REST ではなく、HTTP API のものなのですが、おそらく同じ挙動だと思います。)

docs.aws.amazon.com

AWS SAM では、AWS::Serverless::Functionのリソース内で API Gateway も作成できるのですが、統合リクエストマッピングが設定できません。

このため、コードが増えてしまいますが、CloudFormation で明示的に API Gateway リソースを定義します。

今回はx-tenant-idX-Amz-Tenant-Idにマッピングするように設定します。

ApiGateway:
  Type: AWS::ApiGateway::RestApi
  Properties:
    Name: !Sub ${AWS::StackName}-api

ApiMethod:
  Type: AWS::ApiGateway::Method
  Properties:
    RequestParameters:
      method.request.header.x-tenant-id: true
    Integration:
      Type: AWS_PROXY
      RequestParameters:
        integration.request.header.X-Amz-Tenant-Id: method.request.header.x-tenant-id

では curl で動作確認していきます。

まずは、テナント ID を指定せずにリクエストします。

curl https://<API-ID>.execute-api.ap-northeast-1.amazonaws.com/Prod/hello

先ほどと同じエラーが返ってきますね。

{
  "message": "The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again."
}

次にリクエストヘッダx-tenant-id: test-tenant-001を指定し、動作確認してみます。

curl -H "x-tenant-id: test-tenant-001" \
  https://<API-ID>.execute-api.ap-northeast-1.amazonaws.com/Prod/hello

動作しました!

{
  "message": "hello world"
}

なお、今回のサンプルでは、リクエストヘッダーx-tenant-idを使用してテナント ID を指定していますが、実際の運用では、JWT トークンなどを用いた認証認可の仕組みを別途実装し、リクエストヘッダーの改ざんを防ぐ対応が別途必要です。

実行環境分離の確認

では、本当に実行環境が分離されているか、ソースを修正して動作確認してみます。

app.ts

以下のようなソースを準備しました。

実行環境が分離されていることを確認するため、以下の 2 つのグローバル変数を使用します:

  1. 実行環境 ID(executionEnvironmentId): 実行環境作成時に一度だけ生成される UUID。リクエストを跨いで保持される
  2. 呼び出し回数(invocationCount): リクエストのたびにカウントアップされる値

これらをレスポンスに含めることで、テナントごとに実行環境が分離されているかを確認してみます。

import {
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
  Context,
} from "aws-lambda";
import { randomUUID } from "crypto";
  
// グローバル変数(実行環境ごとに1回だけ初期化される)
const executionEnvironmentId = randomUUID();
let invocationCount = 0;
  
  
export const lambdaHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  try {
    invocationCount++;
  
    console.log("Event:", JSON.stringify(event, null, 2));
    console.log("Context:", JSON.stringify(context, null, 2));
 
    const tenantId = context.tenantId;
    console.log("Tenant ID:", tenantId);
    console.log("Execution Environment ID:", executionEnvironmentId);
    console.log("Invocation Count:", invocationCount);
 
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: "hello world",
        tenantId: tenantId,
        executionEnvironmentId: executionEnvironmentId,
        invocationCount: invocationCount,
      }),
    };
  } catch (err) {
    console.log(err);
    return {
      statusCode: 500,
      body: JSON.stringify({
        message: "some error happened",
      }),
    };
  }
};

また、検証とは関係ありませんが、Lambda 側でテナント ID を識別するための情報context.tenantIdもレスポンスに含めました。

では、テナント ID が異なる2つのリクエストを同時に発信し、UUID や呼び出し回数を確認してみます。

以下コマンドを2回実施します。

curl -H "x-tenant-id: test-tenant-001" https://<API-ID>.execute-api.ap-northeast-1.amazonaws.com/Prod/hello & \
curl -H "x-tenant-id: test-tenant-002" https://<API-ID>.execute-api.ap-northeast-1.amazonaws.com/Prod/hello &
wait

以下の結果が返ってきました。

{"message":"hello world","tenantId":"test-tenant-001","executionEnvironmentId":"2ee2c5e5-f9b9-4ed5-b015-327902c53c3d","invocationCount":1}[1]
{"message":"hello world","tenantId":"test-tenant-002","executionEnvironmentId":"baaad6c0-b7a7-4a6b-9f35-e7d97a7ba8af","invocationCount":1}[2]
  
{"message":"hello world","tenantId":"test-tenant-001","executionEnvironmentId":"2ee2c5e5-f9b9-4ed5-b015-327902c53c3d","invocationCount":2}[1]
{"message":"hello world","tenantId":"test-tenant-002","executionEnvironmentId":"baaad6c0-b7a7-4a6b-9f35-e7d97a7ba8af","invocationCount":2}[2]

テナント ID 単位で、UUID や、呼び出し回数が個別に管理されており、実行環境が別で動いていることがわかりますね!

CloudWatch Logs での確認

テナント分離を有効にすると、CloudWatch Logs に送信されるログにテナント ID の情報が含まれるようになります。

出力箇所は以下のような2パターンがありました。

{
  "time": "2025-11-25T20:13:17.777Z",
  "type": "platform.start",
  "record": {
    "requestId": "9b1af9f2-1656-418f-9ab7-2c8cac22a1f3",
    "functionArn": "arn:aws:lambda:ap-northeast-1:111122223333:function:lambda-tenant-isolate-HelloWorldFunction-MURVicomHdwX",
    "version": "$LATEST",
    "tenantId": "test-tenant-001"
  }
}
{
  "timestamp": "2025-11-25T20:13:17.779Z",
  "level": "INFO",
  "requestId": "9b1af9f2-1656-418f-9ab7-2c8cac22a1f3",
  "tenantId": "test-tenant-001",
  "message": "xxxxxx"
}

このため、以下のような形で CloudWatch Logs Insights を利用し、テナント別にログをフィルタリングできたりします。

fields @timestamp, @message, tenantId
| filter tenantId = "test-tenant-001"
| sort @timestamp desc

考慮点

最後にいくつかテナント分離モードを利用するにあたり、考慮すべき点をまとめておきます。

docs.aws.amazon.com

SnapStart、Provisioned Concurrency 、関数 URL が利用できない

テナント分離モードを有効にした場合、SnapStart,Provisioned Concurrency や 関数 URL が利用できません。

マネージドコンソールで確認してみると、以下のような形で設定できなくなっています。

特にコールドスタート対策の SnapStart,Provisioned Concurrency の両方が利用できないのは問題になる可能性があります。

実行ロールは個別に設定できない

テナント分離モードを有効にした場合においても、すべてのテナントが同じ実行ロールを使用します。

Aurora、DynamoDB へのアクセス等、のデータパーティショニングをロールで制御する場合などは考慮が必要です。

追加料金の発生

テナント分離モードを有効にした場合、追加料金が発生します。

aws.amazon.com

Lambda がリクエストを処理するために新しいテナント分離実行環境を作成すると、メモリ使用量に応じて課金が発生します。

以下は東京リージョンの料金例です。

アーキテクチャ 料金
x86 $0.000167 per 1 GB environment
ARM $0.000133 per 1 GB environment

このため、コールドスタートの発生が多くなるほど、コスト効率は下がっていく形になりそうです。

まとめ

Lambda のテナント分離機能を使うことで、マルチテナントアプリケーションのテナント分離を新しい形で実現できました。

制約や追加料金があるものの、厳格に実行環境の分離を行うことができるかつ、サイロ型で発生するルーティング、デプロイ、監視等の複雑さを解決できそうな感じでした!

誰かのお役に立てば幸いです!

森山 智史 (記事一覧)

アプリケーションサービス本部ディベロップメントサービス1課

2025年10月中途入社。