[アップデート]AWS SAM CLIのローカル実行もLambda関数のテナント分離機能に対応していたので試してみました
はじめに
先日AWS Lambdaにテナント分離の機能が追加されました。
すでに弊社ブログで別の方が書いているので詳細はこちらを確認いただければと思いますが、Lambdaの実行環境を指定に応じ分離する機能となります。
この機能ですがAWS SAM CLI側も対応し、本日リリースされたv1.147.1よりローカル環境での実行時の制御に利用できるようになりましたので試してみます。
なおAWS SAM CLIでのローカルでのテナント分離制御機能については、テナントIDの有無による実行可否の制御、テナントIDの引き渡しといった表側の制御部分の対応が主であり、あくまでLambda関数が受け渡された値を利用して処理ができるかという部分までとなります。
ローカル環境上で実行VMが隔離されるというような制御対応はないのでご注意ください。
実装
この機能を使うにあたり対象の関数のマルチテナントモードを有効化する必要があります。
SAMでテナント分離を有効にするにはAWS::Lambda::FunctionのTenancyConfig直下のTenantIsolationModeにPER_TENANTという値を設定すればOKです。
今回はHello Worldのサンプルテンプレートを拡張し以下のような形にしておきます。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.12
MemorySize: 256
Architectures:
- arm64
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: post
TenancyConfig:
TenantIsolationMode: PER_TENANT
Lambda関数側自体の設定としては有無効までとなります。
テナントIDが想定するものかどうか判別したいような場合は関数内のコードで実装する必要があります。
今回は値に応じて返却メッセージを出し分けるようなコードにしておきます。
def lambda_handler(event, context):
## tenant-idがallow-tenantであればSuccess !!!を返却。それ以外であれば Fail !!!
message = "Success !!!" if "allow-tenant" == context.tenant_id else "Fail !!!"
print("tenant_id: {}".format(context.tenant_id))
return {
"statusCode": 200,
"body": message
}
現時点ではマルチテナントモードは実環境でもテナントIDがあれば起動、なければ起動しない程で具体的な値に応じた処理の継続判定のような機能は持たないため、もし必要であれば今回のようにLambda関数上で自前で実装する必要があります。
実行
さて実際に動かしてみましょう。
local invoke
まずマルチテナントモードで実行するとテナントIDがないと起動しなくなるためその点を確認してみます。
% sam local invoke
Invoking app.lambda_handler (python3.12)
Error: The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again.
テナントIDがないので起動自体に失敗します。実際のLambda関数と同じ挙動ですので想定通りです。
次は値の引き渡しがうまく判定されるか確認しましょう。
## 許可しない(else)ケース
% sam local invoke --tenant-id invalid-id
...
SAM_CONTAINER_ID: 4c2d3e2c63516973bcb49e3211608b01b131754c4084aed2dc6c8fbfa0450242
START RequestId: 4b5e06a1-6224-4265-b0cf-e15b1e9a19b0 Version: $LATEST
tenant_id: invalid-id
END RequestId: f5364ba7-de3c-42ba-87d1-b3a75e8d129d
REPORT RequestId: f5364ba7-de3c-42ba-87d1-b3a75e8d129d Init Duration: 0.04 ms Duration: 131.12 ms Billed Duration: 132 ms Memory Size: 256 MB Max Memory Used: 256 MB
{"statusCode": 200, "body": "Fail !!!"}
## 許可ケース
% sam local invoke --tenant-id allow-tenant
Invoking app.lambda_handler (python3.12) ...
SAM_CONTAINER_ID: b5b8681e2787e8aa9c89a88d59ba214ecbdd510c43105d7e3dea87263ac5ad14
START RequestId: 6db89e38-982b-4225-a6ad-4f235530d5e5 Version: $LATEST
tenant_id: allow-tenant
END RequestId: 06a1b05a-f1a2-42d7-91b4-8a8f743f3986
REPORT RequestId: 06a1b05a-f1a2-42d7-91b4-8a8f743f3986 Init Duration: 0.10 ms Duration: 72.44 ms Billed Duration: 73 ms Memory Size: 256 MB Max Memory Used: 256 MB
{"statusCode": 200, "body": "Success !!!"}
良さそうですね。AWS上のLambda関数同様context.tenant_idで値が渡ってきおりコードの中でテナントIDを制御する処理は実環境同様にエミュレートできそうです。
local start-api
API Gateway経由側のローカル実行も対応してますので確認してみます。
まずは何も付与せずにcurlでアクセスします。
## 別ウィンドウで実行
% curl -X POST http://127.0.0.1:3000/hello
{"message":"Internal server error"}
## SAM側の出力
2025-11-21 18:22:09 127.0.0.1 - - [21/Nov/2025 18:22:09] "POST /hello HTTP/1.1" 502 -
Invoking app.lambda_handler (python3.12)
Exception on /hello [POST]
Traceback (most recent call last):
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/samcli/local/apigw/local_apigw_service.py", line 741, in _request_handler
lambda_response = self._invoke_lambda_function(route.function_name, route_lambda_event, tenant_id)
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/samcli/local/apigw/local_apigw_service.py", line 624, in _invoke_lambda_function
self.lambda_runner.invoke(
~~~~~~~~~~~~~~~~~~~~~~~~~^
lambda_function_name, event_str, stdout=stdout_writer, stderr=self.stderr, tenant_id=tenant_id
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/opt/homebrew/Cellar/aws-sam-cli/1.147.1/libexec/lib/python3.13/site-packages/samcli/commands/local/lib/local_lambda.py", line 167, in invoke
raise TenantIdValidationError(
...<2 lines>...
)
samcli.commands.local.lib.exceptions.TenantIdValidationError: The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again.
2025-11-21 18:23:11 127.0.0.1 - - [21/Nov/2025 18:23:11] "POST /hello HTTP/1.1" 502 -
...スタックトレースが出力されていますが発生しているTenantIdValidationErrorではテナントIDがない旨が出力されているので判定自体はうまく走ってそうです。SAM側の取り回しの問題でしょうか...。
さて、今度は正常ケースとしてテナントIDを渡してみましょう。
本来であればAPI Gateway側で制御すべきかとは思いますが、今回は検証なのでcurl側で直接付与してリクエストします。
## 許可しない(else)ケース
% curl -X POST http://127.0.0.1:3000/hello -H "X-Amz-Tenant-Id: invalid-id"
Fail !!!%
...
Invoking app.lambda_handler (python3.12)
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 2d89c41f-b384-47ba-b7c5-389104e06577 Version: $LATEST
tenant_id: invalid-id
END RequestId: f32a6dbd-3d06-470a-a358-d3bec2f7fd80
REPORT RequestId: f32a6dbd-3d06-470a-a358-d3bec2f7fd80 Duration: 8.67 ms Billed Duration: 9 ms Memory Size: 256 MB Max Memory Used: 256 MB
No Content-Type given. Defaulting to 'application/json'.
2025-11-21 18:35:51 127.0.0.1 - - [21/Nov/2025 18:35:51] "POST /hello HTTP/1.1" 200 -
Invoking app.lambda_handler (python3.12)
## 許可するケース
% curl -X POST http://127.0.0.1:3000/hello -H "X-Amz-Tenant-Id: allow-tenant"
Success !!!%
...
Reuse the created warm container for Lambda function 'HelloWorldFunction'
Lambda function 'HelloWorldFunction' is already running
START RequestId: 7b644168-0276-4c84-b280-b3fe24e9b265 Version: $LATEST
tenant_id: allow-tenant
END RequestId: fce169ad-455a-464b-8384-974f71d4848d
REPORT RequestId: fce169ad-455a-464b-8384-974f71d4848d Duration: 4.73 ms Billed Duration: 5 ms Memory Size: 256 MB Max Memory Used: 256 MB
No Content-Type given. Defaulting to 'application/json'.
2025-11-21 18:35:57 127.0.0.1 - - [21/Nov/2025 18:35:57] "POST /hello HTTP/1.1" 200 -
X-Tenant-IDで渡された値がうまくcontext.tenant-idにマッピングされて処理されてそうです。
remote invoke
嬉しいことにremote invokeも今回対応しています。
## tenant-idなし
% sam remote invoke
Invoking Lambda Function HelloWorldFunction
Error: An error occurred (InvalidParameterValueException) when calling the Invoke operation: The invoked function is enabled with tenancy configuration. Add a valid tenant ID in your request and try again.
## tenant-idあり
% sam remote invoke --tenant-id invalid-id
Invoking Lambda Function HelloWorldFunction
{"time":"2025-11-21T09:46:33.054Z","type":"platform.initStart","record":{"initializationType":"on-demand","phase":"init","runtimeVersion":"python:3.12.v94","runtimeVersionArn":"arn:aws:lambda:ap-northeast-1::runtime:0eadbb902598f7fa741f3b9a85af61db6bb8936491d8ec0bb427e5cba30a722a","functionName":"sam-app-HelloWorldFunction-GjxrzE8780J3","functionVersion":"$LATEST","instanceId":"2025/11/21/[$LATEST]9479d23382634c6f99915a5cfdeb534b","instanceMaxMemory":268435456}}
{"time":"2025-11-21T09:46:33.147Z","type":"platform.initRuntimeDone","record":{"initializationType":"on-demand","phase":"init","status":"success"}}
{"time":"2025-11-21T09:46:33.147Z","type":"platform.initReport","record":{"initializationType":"on-demand","phase":"init","status":"success","metrics":{"durationMs":93.106}}}
{"time":"2025-11-21T09:46:33.151Z","type":"platform.start","record":{"requestId":"92a9ceb0-98be-4449-b5d1-e096af6c64b9","functionArn":"arn:aws:lambda:ap-northeast-1:xxxxx:function:sam-app-HelloWorldFunction-GjxrzE8780J3","version":"$LATEST","tenantId":"invalid-id"}}
tenant_id: invalid-id
{"time":"2025-11-21T09:46:33.153Z","type":"platform.runtimeDone","record":{"requestId":"92a9ceb0-98be-4449-b5d1-e096af6c64b9","status":"success","spans":[{"name":"responseLatency","start":"2025-11-21T09:46:33.151Z","durationMs":1.171},{"name":"responseDuration","start":"2025-11-21T09:46:33.152Z","durationMs":0.088},{"name":"runtimeOverhead","start":"2025-11-21T09:46:33.153Z","durationMs":0.674}],"metrics":{"durationMs":2.079,"producedBytes":39},"tenantId":"invalid-id"}}
{"time":"2025-11-21T09:46:33.166Z","type":"platform.report","record":{"requestId":"92a9ceb0-98be-4449-b5d1-e096af6c64b9","metrics":{"durationMs":2.677,"billedDurationMs":96,"memorySizeMB":256,"maxMemoryUsedMB":35,"initDurationMs":93.107},"status":"success","tenantId":"invalid-id"}}
{"statusCode": 200, "body": "Fail !!!"}%
% sam remote invoke --tenant-id allow-tenant
Invoking Lambda Function HelloWorldFunction
{"time":"2025-11-21T09:45:18.795Z","type":"platform.initStart","record":{"initializationType":"on-demand","phase":"init","runtimeVersion":"python:3.12.v94","runtimeVersionArn":"arn:aws:lambda:ap-northeast-1::runtime:0eadbb902598f7fa741f3b9a85af61db6bb8936491d8ec0bb427e5cba30a722a","functionName":"sam-app-HelloWorldFunction-GjxrzE8780J3","functionVersion":"$LATEST","instanceId":"2025/11/21/[$LATEST]caebd53d86324ab1a23f25edf532c134","instanceMaxMemory":268435456}}
{"time":"2025-11-21T09:45:18.892Z","type":"platform.initRuntimeDone","record":{"initializationType":"on-demand","phase":"init","status":"success"}}
{"time":"2025-11-21T09:45:18.892Z","type":"platform.initReport","record":{"initializationType":"on-demand","phase":"init","status":"success","metrics":{"durationMs":97.145}}}
{"time":"2025-11-21T09:45:18.896Z","type":"platform.start","record":{"requestId":"e1de8c57-8191-456a-a920-145bc882f1f0","functionArn":"arn:aws:lambda:ap-northeast-1:xxxxxx:function:sam-app-HelloWorldFunction-GjxrzE8780J3","version":"$LATEST","tenantId":"allow-tenant"}}
tenant_id: allow-tenant
{"time":"2025-11-21T09:45:18.898Z","type":"platform.runtimeDone","record":{"requestId":"e1de8c57-8191-456a-a920-145bc882f1f0","status":"success","spans":[{"name":"responseLatency","start":"2025-11-21T09:45:18.896Z","durationMs":1.166},{"name":"responseDuration","start":"2025-11-21T09:45:18.897Z","durationMs":0.095},{"name":"runtimeOverhead","start":"2025-11-21T09:45:18.898Z","durationMs":0.573}],"metrics":{"durationMs":2.004,"producedBytes":42},"tenantId":"allow-tenant"}}
{"time":"2025-11-21T09:45:18.905Z","type":"platform.report","record":{"requestId":"e1de8c57-8191-456a-a920-145bc882f1f0","metrics":{"durationMs":2.527,"billedDurationMs":100,"memorySizeMB":256,"maxMemoryUsedMB":35,"initDurationMs":97.146},"status":"success","tenantId":"allow-tenant"}}
{"statusCode": 200, "body": "Success !!!"}%
test-event putは未対応
横道ですが先日「実行タイプ」をメタデータとして渡せるようなアップデートがありました。
テナントIDもマネジメントコンソール上、同画面で指定できるのでテナントIDも合わせてアップロードできる機能がないかなと思ったのですがこちらはまだ未対応でした。
試しにマネジメントコンソールで直接イベントを保存してみましたが、Lambdaの画面側で保存後に画面更新を行ったら値が消滅し、「テナントID」の値自体もEventBridgeのスキーマ情報に保存されないのでSAMというよりはLambda側の対応待ちにはなりそうです。
終わりに
新しく実装されたLambda関数のテナント分離の制御をSAMのローカル実行で試してみました。
こういった制御系のアップデートはSAM CLI側には遅れてくることもありますが、今回は2日後となかなか早いタイミングでの対応でありがたい限りです。
ローカルだけではなくremote invokeでも実行できますので、CI/CDパイプラインで利用する場合は、ダミーデータを用いたテストはlocal invokeを利用、実際のデータを利用する関係で分離した環境で実行したい場合はデプロイしてremote invokeを利用すると使い分けていくのも良いかもしれません。






