
Grafana OSS版でLoki / Tempo / AMPをテナントごとに分けて、見えるデータを分離してみた
こんにちは、ゲームソリューション部のsoraです。
今回は、Loki / Tempo / AMPの保存先をテナントごとに分け、Grafana OSS版のOrganizationと結びつけて、見えるデータを分離できるか試してみました。
先に結論
- Grafana・Loki / Tempo・S3はテナント間で共有のままでも、保存先の側でテナントを分けてOrganizationごとのデータソースに結びつければ、他テナントのデータは画面にもAPIにも出なかった
- テナント側のユーザーをEditorまでにしておけば、データソースを変更できないので分離が保たれた
- 運用者用のOrganizationを作れば、両テナントのデータを横断して見られた
構成
今回構築したのは以下の構成です。

以下の記事で、GrafanaのOrganizationで分けても、CloudWatch / X-Rayを直接読む構成では同じAWSアカウントのメトリクスとトレースを分けられないことを確認しました。
今回は保存先をLoki / Tempo / AMPにして、保存先の側でテナントを分けます。
サンプルアプリはテナントA用(app-A)とテナントB用(app-B)の2組を置き、サイドカーからログ・トレース・メトリクスを送っています。
Grafana・Loki / Tempo・S3は両テナントで共有し、AMPだけはテナントごとにワークスペースを分けています。
使用したバージョンは以下のとおりです。
- Grafana: 13.2.1
- Loki: 3.7.7
- Tempo: 3.0.3
- ADOT Collector: v0.50.0
構築
今回もTerraformで構築しました。
基本的なリソースが多いのでコードは割愛しますが、テナントを分けている箇所だけ解説します。
Loki / Tempoのテナント設定
Lokiはauth_enabled: trueにすると、すべてのリクエストにX-Scope-OrgIDヘッダーが必要になり、その値がテナントIDになります。
auth_enabled: true
querier:
# X-Scope-OrgID: tenant-a|tenant-b のように、複数テナントをまとめて検索できるようにする
multi_tenant_queries_enabled: true
Tempoはmultitenancy_enabled: trueで同じ動きになります。
multitenancy_enabled: true
query_frontend:
multi_tenant_queries_enabled: true
multi_tenant_queries_enabledは、後述の運用者用Organizationで両テナントを横断して見るための設定です。
Lokiは既定がfalse、Tempoは既定でtrueですが、明示しています。
GrafanaのタスクからLoki / Tempoを直接叩いてみると、ヘッダーが無いリクエストは401で弾かれます。
$ wget -S -qO- http://loki.o11y.local:3100/loki/api/v1/labels
HTTP/1.1 401 Unauthorized
$ wget -S -qO- --header "X-Scope-OrgID: tenant-a" http://loki.o11y.local:3100/loki/api/v1/labels
HTTP/1.1 200 OK
{"status":"success","data":["container_name","job","service_name"]}
$ wget -S -qO- http://tempo.o11y.local:3200/api/search
HTTP/1.1 401 Unauthorized
$ wget -S -qO- --header "X-Scope-OrgID: tenant-a" http://tempo.o11y.local:3200/api/search
HTTP/1.1 200 OK
サイドカーのテナント設定
ログは、FireLensのLoki出力にtenant_idを付けています。
これがX-Scope-OrgIDヘッダーになります(タスク定義の抜粋です)。
logConfiguration = {
logDriver = "awsfirelens"
options = {
Name = "loki"
host = "loki.o11y.local"
port = "3100"
tenant_id = "tenant-a" # テナントBのタスクでは tenant-b
labels = "job=app"
}
}
トレースとメトリクスは、ADOT Collectorの設定をテナントごとに分けています。
違うのは、Tempoに付けるヘッダーと、AMPの書き込み先のワークスペースの2か所だけです。
exporters:
otlp_grpc/tempo:
endpoint: tempo.o11y.local:4317
tls:
insecure: true
headers:
X-Scope-OrgID: tenant-a # テナントBでは tenant-b
prometheusremotewrite:
# テナントAのワークスペースに書く
endpoint: https://aps-workspaces.ap-northeast-1.amazonaws.com/workspaces/<ワークスペースAのID>/api/v1/remote_write
auth:
authenticator: sigv4auth
resource_to_telemetry_conversion:
enabled: true
アプリのタスクロールもテナントごとに分けていて、app-Aのタスクロールにはワークスペースへのaps:RemoteWriteをテナントAの分だけ付けています(app-BはテナントBの分だけ)。
resource_to_telemetry_conversionは、service.nameなどの属性をメトリクスのラベルにする設定です。
これが無いとservice_nameで絞り込めません。
S3のプレフィックス
しばらくアプリにアクセスしたあとにS3を見ると、Loki / Tempoのどちらのバケットもテナントごとのプレフィックスに分かれていました。
$ aws s3 ls s3://grafana-o11y-loki-<アカウントID>/
PRE tenant-a/
PRE tenant-b/
$ aws s3 ls s3://grafana-o11y-tempo-<アカウントID>/
PRE tenant-a/
PRE tenant-b/
2026-09-24 09:57:02 3084 work.json
Lokiのindexは十数分後に上がり、こちらもテナントごとに分かれていました。
$ aws s3 ls s3://grafana-o11y-loki-<アカウントID>/ --recursive | grep index/
2026-09-24 10:06:12 index/index_20720/tenant-a/...tsdb.gz
2026-09-24 10:06:12 index/index_20720/tenant-b/...tsdb.gz
AMPを読むIAMロール
GrafanaがAMPを読むためのIAMロールは、テナントごとに1つと、運用者用に1つの計3つ作っています。
| ロール | 読めるワークスペース |
|---|---|
grafana-tenant-a |
テナントA |
grafana-tenant-b |
テナントB |
grafana-ops |
テナントA / テナントB |
data "aws_iam_policy_document" "grafana" {
for_each = local.grafana_roles
statement {
actions = [
"aps:QueryMetrics",
"aps:GetLabels",
"aps:GetSeries",
"aps:GetMetricMetadata",
]
resources = each.value.workspaces
}
}
Grafanaのタスクロールには、AMPの読み取り権限を付けず、この3つのロールへのsts:AssumeRoleだけを付けています。
タスクロールに読み取り権限を付けると、Assume Roleを指定しなくても全ワークスペースが読めてしまうためです。
Organizationとデータソースの設定
Organizationとユーザーの作成
Organizationは、テナントA用・テナントB用・運用者用の3つを作りました。
| Organization | ユーザー | ロール |
|---|---|---|
| OrgA | usera3 |
Editor |
| OrgB | userb3 |
Editor |
| Org-ops | ops3 |
Admin |
各ユーザーは1つのOrganizationにだけ所属させ、Main Org.には入れていません。
Organizationとユーザーの作り方は、以下の記事と同じです。
Lokiデータソースの設定
OrgAに切り替えて、Connections → Data sourcesからLokiのデータソースを作ります。
URLはLokiのCloud Mapの名前で、どのOrganizationでも同じです。

テナントを決めているのは、HTTP headersのX-Scope-OrgIDです。
OrgAでは値をtenant-a、OrgBではtenant-bにします。
値は保存するとconfiguredと伏せ字になり、画面からもAPIからも読めません。
Tempoデータソースの設定
TempoもLokiと同じで、HTTP headersにX-Scope-OrgIDを入れます。

AMPデータソースの設定
AMPは、Prometheus server URLにテナントのワークスペースのURL(https://aps-workspaces.ap-northeast-1.amazonaws.com/workspaces/<ワークスペースID>/)を入れます。
OrgAにはワークスペースA、OrgBにはワークスペースBのURLです。

Authentication ProviderはAWS SDK Defaultにして、Assume Role ARNにテナントのロール(OrgAならgrafana-tenant-a)を入れます。
Authentication Providerは、ほかにAccess & secret keyとCredentials fileが選べます。

Assume Role ARNを空にしてSave & testを押すと、403 Forbiddenになりました。
Grafanaのタスクロールだけでは、どのワークスペースも読めない状態になっています。
なお、データソースをAPIやプロビジョニングで入れる場合、Assume Role ARNはjsonDataのsigV4AssumeRoleArnに入れます。
公式ドキュメントのプロビジョニングの説明にはassumeRoleArnとありますが、こちらに入れた場合はAssumeRoleされず、ARNを入れないときと同じ403 Forbiddenになりました。
動作確認
テナントA(app-A)には正常なリクエストだけ、テナントB(app-B)には正常なリクエストに加えてエラーを返すエンドポイントへのリクエストも流しておきました。
画面で見たときに、どちらのテナントのデータかがエラーの有無で見分けられるようにするためです。
usera3での確認
usera3でログインし、ExploreでLokiに以下のクエリを投げてみます。
sum by (ecs_task_definition) (count_over_time({job="app"} | json [1m]))

grafana-o11y-app-a-frontendとgrafana-o11y-app-a-backendの2系列だけが出ていて、app-Bのログは出てきません。
Drilldown → Tracesを開くと、右上のerror/sは0のままです。

AMPも見てみます。
sum by (http_response_status_code) (rate(app_requests_total[1m]))

ステータスコードは200だけで、テナントAのワークスペースの値だけが返っています。
userb3での確認
同じことをuserb3でやってみます。

Lokiにはgrafana-o11y-app-b-frontendとgrafana-o11y-app-b-backendだけが出ています。

テナントBにはエラーを流しているので、error/sに山が出ています。

AMPにも500の系列が出ています。
同じGrafana、同じLoki / Tempoを使っていても、Organizationごとに見えるデータが分かれていることが確認できました。
Editorによるデータソース変更の確認
テナントを分けているのはデータソースの設定なので、ここを書き換えられると分離が崩れます。
usera3はEditorなので、そもそもデータソースを変更できるかを確認します。

左メニューにConnectionsが無く、Administration → Plugins and dataの中もCorrelationsだけで、データソースの設定画面への入口がありません。
データソースの編集画面のURLを直接開いても、ホームに戻されました。
APIでヘッダーの値をtenant-bに書き換えようとしても、403になります。
$ curl -s -u usera3:<password> -H 'Content-Type: application/json' \
-X PUT https://<GrafanaのURL>/api/datasources/uid/loki-a \
-d '{"name":"loki","type":"loki","access":"proxy","url":"http://loki.o11y.local:3100","jsonData":{"httpHeaderName1":"X-Scope-OrgID"},"secureJsonData":{"httpHeaderValue1":"tenant-b"}}'
{"message":"You'll need additional permissions to perform this action. Permissions needed: datasources:write","title":"Access denied"}
新しくデータソースを作ろうとしても、同じくdatasources:createが無いので403でした。
一方、Organization Adminはデータソースを変更できるので、ヘッダーの値やAssume Role ARNを書き換えれば、他テナントのデータも見えてしまいます。
テナント側のユーザーにはOrganization Adminを渡さず、データソースは運用側が作る、という運用がこの分離の前提になります。
運用者用Organizationでの横断
Organizationで分けると、1つのOrganizationの中からは自分のテナントしか見えません。
全テナントを見たい運用者向けに、Org-opsには以下のデータソースを作っています。
| データソース | 設定 |
|---|---|
loki |
HTTP headers: X-Scope-OrgID = tenant-a|tenant-b |
tempo |
HTTP headers: X-Scope-OrgID = tenant-a|tenant-b |
amp-tenant-a |
ワークスペースAのURL + Assume Role ARN: grafana-ops |
amp-tenant-b |
ワークスペースBのURL + Assume Role ARN: grafana-ops |
Loki / Tempoは、ヘッダーの値に|区切りでテナントIDを並べると、複数のテナントをまとめて検索できます。
構築の節で入れたmulti_tenant_queries_enabledがこのための設定です。
AMPは1つのデータソースで1つのワークスペースしか指定できないので、ワークスペースの数だけデータソースを作ります。
ops3でログインし、ExploreでLokiに以下のクエリを投げてみます。
sum by (__tenant_id__) (count_over_time({job="app"}[1m]))

tenant-aとtenant-bの2系列が出ました。
__tenant_id__は、複数テナントをまとめて検索したときにLokiが付けるラベルで、どのテナントのログかがわかります。
Drilldown → Tracesも見てみます。

テナントBのエラーがerror/sに出ていて、frontendのRateは約3.1と、userb3で見たとき(約1.6)のほぼ倍になっています。
両テナントのトレースがまとめて見えています。
ちなみに、Lokiの横断検索では、検索する期間によって片方のテナントのログしか返らないことがありました。
これはLokiの既知の不具合として報告されていて、検索範囲がLokiのメモリ上のデータとS3上のデータの両方にまたがると、2つ目のテナントの結果が欠けます(2026年9月時点で未修正です)。
エラーは出ずに一部のテナントの結果だけが欠けるので、横断検索の結果を集計などに使う場合は注意が必要です。
補足:Loki / TempoのBasic認証とForward OAuth identity
Loki / TempoのデータソースのAuthenticationでは、Basic authenticationとForward OAuth identityも選べます。
| 選択肢 | 送るもの |
|---|---|
| Basic authentication | データソースに入れたユーザー名とパスワード |
| Forward OAuth identity | Grafanaにログインしている人のOAuthのアクセストークン(OIDCのIDトークン) |
ただし、Loki / Tempoには認証の仕組みが無いので、送られてきた認証情報は確かめられず、テナントはX-Scope-OrgIDだけで決まります。
Lokiのドキュメントでは、認証するリバースプロキシを前に置き、X-Scope-OrgIDはそのプロキシが付けるものとされています。
Grafana Loki does not come with any included authentication layer. You must run an authenticating reverse proxy in front of your services.
リバースプロキシで認証してからX-Scope-OrgIDを付ける構成にすれば、テナントはGrafanaのデータソースに入れたヘッダーではなく、認証したユーザーやトークンから決まります。
Basic認証のユーザーとテナントを対応付けるプロキシは、OSSでも公開されています。
複数のプロジェクトで使う場合の分け方
今回の構成では、テナントを分けているのはデータソースの設定です。
Organizationの分け方とどのロールを渡すかの組み合わせで、今回の検証結果をもとに以下の3つのパターンに整理しました。
- パターン1: Organization分離 + テナント側はEditor(今回の構成)
- Organization: テナントごとに作る
- テナント側のユーザー: Editor
- データソース: 運用側のOrganization Adminが設定する
- 運用者: 運用者用のOrganizationで、
X-Scope-OrgIDに複数テナントを並べたデータソースから横断する - テナント側のユーザーはデータソースを変更できないので、他テナントのデータは見えません
- データソースやユーザーの追加は運用側で行います
- パターン2: Organization分離 + テナント側にOrganization Admin
- Organization: テナントごとに作る
- テナント側のユーザー: Organization Admin
- データソース: テナント側が設定する
- 運用者: パターン1と同じ
- テナント側でデータソースやユーザーを追加できますが、ヘッダーの値やAssume Role ARNを書き換えれば他テナントのデータも見えます
- 防ぐには、Loki / Tempoの前にリバースプロキシを置いて
X-Scope-OrgIDを上書きするなどの設定が必要です
- 防ぐには、Loki / Tempoの前にリバースプロキシを置いて
- パターン3: テナントごとにGrafanaを分ける
- Grafana: テナントごとに1つ立てる
- テナント側のユーザー: Organization Admin(Grafana Server Adminでもよい)
- データソース: テナント側が設定する
- Grafanaのタスクロール: そのテナントのワークスペースだけ読める権限を直接付ける
- 運用者: 全テナントのデータソースを持つ運用者用のGrafanaを別に立てる
- Grafanaの中に他テナントのデータソースが無く、タスクロールも他テナントのワークスペースを読めないので、管理者権限を渡しても他テナントは見えません
- Grafanaの台数分のコストと運用がかかります
最後に
今回は、Loki / Tempo / AMPの保存先をテナントごとに分け、Grafana OSS版のOrganizationと結びつけて、見えるデータを分離できるかを試してみました。
この記事がどなたかの参考になれば幸いです。









