ストレージのテナント分離 ~S3バケット単位のサイロ化を試してみた~
みなさんはどのようにストレージをテナント分離していますか?
また、正しくテナント分離を実装できていますでしょうか。
このブログでは、以前に「テナント分離のための過剰なサイロ化はやめよう」という内容を取り上げたことがあります。
一方で、データの保存要件などによっては、サイロ化を余儀なくされることもあると思います。
今回はAmazon S3を用いた「S3バケット単位のサイロ化」をテーマとし、テナント分離について考察していきます。
S3バケット単位のサイロ化は現実的か
まず、S3バケット単位のサイロ化とはどういうことでしょうか?
今回は、1つのAWSアカウントの中に、テナントごとのS3バケットが1つずつ作成される、という状況を想定します。
また、コンピューティング層 (以下の図ではEC2が該当します) が共有されているアーキテクチャも、今回の内容に含みます。
ここで、サイロ化にあたって考慮するべき内容の1つに、AWSの「クォータ」という概念があります。
AWSのクォータを確認する
AWSのクォータとは、作成できるリソース数やAPIリクエスト数の上限を定めたものです。
今回は1つのAWSアカウント内に、テナントごとにS3バケットを作成するため、「AWSアカウントごとに作成できるS3バケット数」が問題になってきます。
以前は、デフォルトで100、上限緩和により1,000の汎用バケットが作成できるという、かなり小さめのクォータの内容だったため、S3バケット単位のサイロ化を諦めたSaaSもあったのではないかと思います。
しかしながら、2024年のアップデートにより、クォータが以下のように大幅に緩和されました。
- デフォルト: 10,000
- 上限緩和時の最大: 100万
このアップデートを受けて、テナント数が100万以下のSaaSであれば、理論上はS3バケット単位のサイロ化が可能ということになります。
なお、該当のクォータはAWSマネジメントコンソールの 「Service Quotas」から確認できます。以下の画像では、作成できる汎用バケットの上限が10,000であることを表しています。
運用可能性を確認する
それでは、実際に100万のテナントがSaaSに登録された際、サービスは運用可能でしょうか?
SaaSの特性の1つに「俊敏性」が挙げられますが、サイロ化によって運用効率が下がり、俊敏性が損なわれるケースは少なくありません。
例えば、テナントのオンボーディング時にはどのように対応するべきでしょうか?
エンジニアが1つ1つのバケットを手動作成することのないように、あらかじめIaCなどを用いた自動化を検討しておくことが望ましいです。
実際にテナント分離を試してみる
ここまででS3バケット単位のサイロ化を見てきましたが、サイロ化だけではテナント分離は達成されません。
重要なのは、テナントごとに分けたバケットに対して、他テナントからアクセスできないようにすることです。
今回は以下の2ステップでテナント分離を実現していきます。
- バケットの命名 (バケット名にテナントIDを含める)
- 権限を動的に生成する (IAMポリシーのテンプレート作成)
バケットの命名 (バケット名にテナントIDを含める)
まずはテナントごとのバケットを作成していきます。
ステップ2の権限管理のため、どのテナントのバケットなのかを識別できる命名にする必要があります。
これには複数の方法が考えられますが、最もシンプルなのが「バケット名にテナントIDを含める」という方法です。
今回は以下のような命名とします。
{{テナントID}}-{{アカウントリージョナル名前空間用のサフィックス}}上記の「アカウントリージョナル名前空間」は、2026年3月に発表されたS3の機能です。
従来はグローバルで一意なバケット名を設定する必要がありましたが、この機能を使うことで該当アカウントの該当リージョン内で一意であれば良くなります。
実際に tenant001 というテナントIDでバケットを作成してみます。
同様に、 tenant002 でもバケットを作成しました。
また、アクセスの検証用に sample.txt というテキストファイルを両バケットにアップロードしました。
権限を動的に生成する (IAMポリシーのテンプレート作成)
次に、IAMポリシーのテンプレートを作成します。
ここでポイントとなるのが、AWS STSのセッションポリシーという仕組みの活用です。
セッションポリシーは、ベースとなるIAMロールが持つ広範な権限に対し、実行時に特定のテナントIDに一致するリソースのみを許可する「フィルター」として機能します。これにより、単一のコードで全テナントのバケットを安全に扱い分けることが可能です。
今回は、上で作成したsample.txtにアクセスできる権限を付与しますが、バケット名のテナントID部分をプレースホルダーとしておきます。具体的には以下の通りです:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::{{TENANT_ID}}-{{ACCOUNT_ID}}-ap-northeast-1-an",
"arn:aws:s3:::{{TENANT_ID}}-{{ACCOUNT_ID}}-ap-northeast-1-an/*"
]
}
]
}デモ用に、AWSアカウントのIDもプレースホルダーとしています。
また、AssumeRoleを行うため、ベースとなるIAMロールを事前に作成しておきます。
名前は tenant-isolation-demo-role とし、以下のポリシーを与えています(アカウントIDは仮です)。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::tenant*-012345678901-ap-northeast-1-an",
"arn:aws:s3:::tenant*-012345678901-ap-northeast-1-an/*"
]
}
]
}それでは、以下のシェルスクリプトを実行して、バケットへのアクセスを検証します。
このシェルスクリプトは、ログインしたユーザーがS3バケットにアクセスする挙動を模したものです。
使い方は、
./demo.sh {{自身のテナントID}} {{アクセス先S3バケットのテナントID}}となります(コードは読み飛ばして良いです)。
#!/bin/bash
set -euo pipefail
TENANT_ID=${1:?Usage: $0 <policy-tenant-id> <access-tenant-id>}
TARGET_TENANT_ID=${2:?Usage: $0 <policy-tenant-id> <access-tenant-id>}
REGION="ap-northeast-1"
ROLE_NAME="tenant-isolation-demo-role"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}"
BUCKET="${TARGET_TENANT_ID}-${ACCOUNT_ID}-${REGION}-an"
# テンプレートからセッションポリシーを動的生成
POLICY=$(sed -e "s/{{TENANT_ID}}/${TENANT_ID}/g" -e "s/{{ACCOUNT_ID}}/${ACCOUNT_ID}/g" policy-template.json)
echo "🔐 セッションポリシー (許可テナント: ${TENANT_ID}):"
echo "${POLICY}"
echo ""
echo "🪣 アクセス先: s3://${BUCKET}/sample.txt"
# AssumeRoleでテナント制限付き認証情報を取得
CREDS=$(aws sts assume-role \
--role-arn "${ROLE_ARN}" \
--role-session-name "tenant-${TENANT_ID}" \
--policy "${POLICY}" \
--query 'Credentials' --output json)
# 一時認証情報でS3にアクセス
OUTPUT=$(mktemp)
ERROR=$(mktemp)
if AWS_ACCESS_KEY_ID=$(echo "$CREDS" | jq -r .AccessKeyId) \
AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | jq -r .SecretAccessKey) \
AWS_SESSION_TOKEN=$(echo "$CREDS" | jq -r .SessionToken) \
aws s3api get-object --bucket "${BUCKET}" --key sample.txt "$OUTPUT" > /dev/null 2>"$ERROR"; then
echo "✅ 成功: $(cat "$OUTPUT")"
else
echo "❌ 拒否: $(cat "$ERROR")"
fi
rm -f "$OUTPUT" "$ERROR"まずは
./demo.sh tenant001 tenant001としたときの挙動を試します。つまり、tenant001のユーザーがtenant001のバケットにアクセスする場合です。
tenant001用のポリシーが生成され、テキストを取得することができました。
次に
./demo.sh tenant001 tenant002とし、tenant001のユーザーがtenant002のバケットにアクセスを試みます。
tenant001からはtenant002のバケットへアクセスできませんでした。
このようにして、動的にポリシーを生成することで、テナントごとに作成されたバケットへセキュアにアクセスすることが可能であることがわかりました。
まとめ
この記事では以下の2点を検証しました。
- AWSの制限が緩和され、テナントごとにS3バケットを作成することは現実的になってきた
- 動的にIAMポリシーを生成することで、サイロ化されたS3バケットにセキュアにアクセスすることができた
また、「運用可能性を確認する」の項目でも述べましたが、テナント数が増えても運用に支障が出ないような体制や仕組みが重要であることにも注意してください。
Spread the word:

