Grafana OSS版でLoki / Tempo / AMPをテナントごとに分けて、見えるデータを分離してみた

Grafana OSS版でLoki / Tempo / AMPをテナントごとに分けて、見えるデータを分離してみた

GrafanaのOrganizationとLoki / Tempo / AMPを組み合わせて、複数テナントのデータを完全に分離できるか試してみました。結果、データソースのヘッダー設定でテナントIDを明示することで、同一のGrafana・ストレージを共有しながらも、他テナントのデータを完全に隔離できることが確認できました。
2026.09.24

こんにちは、ゲームソリューション部のsoraです。
今回は、Loki / Tempo / AMPの保存先をテナントごとに分け、Grafana OSS版のOrganizationと結びつけて、見えるデータを分離できるか試してみました。

先に結論

  • Grafana・Loki / Tempo・S3はテナント間で共有のままでも、保存先の側でテナントを分けてOrganizationごとのデータソースに結びつければ、他テナントのデータは画面にもAPIにも出なかった
  • テナント側のユーザーをEditorまでにしておけば、データソースを変更できないので分離が保たれた
  • 運用者用のOrganizationを作れば、両テナントのデータを横断して見られた

構成

今回構築したのは以下の構成です。

sr-grafana-multitenant-00-architecture

以下の記事で、GrafanaのOrganizationで分けても、CloudWatch / X-Rayを直接読む構成では同じAWSアカウントのメトリクスとトレースを分けられないことを確認しました。
https://dev.classmethod.jp/articles/grafana-tenant-separation-team-organization/

今回は保存先を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ですが、明示しています。
https://grafana.com/docs/loki/latest/operations/multi-tenancy/
https://grafana.com/docs/tempo/latest/operations/manage-advanced-systems/multitenancy/

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とユーザーの作り方は、以下の記事と同じです。
https://dev.classmethod.jp/articles/grafana-tenant-separation-team-organization/

Lokiデータソースの設定

OrgAに切り替えて、Connections → Data sourcesからLokiのデータソースを作ります。
URLはLokiのCloud Mapの名前で、どのOrganizationでも同じです。

sr-grafana-multitenant-01

テナントを決めているのは、HTTP headersX-Scope-OrgIDです。
OrgAでは値をtenant-a、OrgBではtenant-bにします。
値は保存するとconfiguredと伏せ字になり、画面からもAPIからも読めません。

Tempoデータソースの設定

TempoもLokiと同じで、HTTP headersX-Scope-OrgIDを入れます。

sr-grafana-multitenant-02

AMPデータソースの設定

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

sr-grafana-multitenant-03

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

sr-grafana-multitenant-04

Assume Role ARNを空にしてSave & testを押すと、403 Forbiddenになりました。
Grafanaのタスクロールだけでは、どのワークスペースも読めない状態になっています。

なお、データソースをAPIやプロビジョニングで入れる場合、Assume Role ARNはjsonDatasigV4AssumeRoleArnに入れます。
公式ドキュメントのプロビジョニングの説明にはassumeRoleArnとありますが、こちらに入れた場合はAssumeRoleされず、ARNを入れないときと同じ403 Forbiddenになりました。
https://grafana.com/docs/plugins/grafana-amazonprometheus-datasource/latest/configure/

動作確認

テナントA(app-A)には正常なリクエストだけ、テナントB(app-B)には正常なリクエストに加えてエラーを返すエンドポイントへのリクエストも流しておきました。
画面で見たときに、どちらのテナントのデータかがエラーの有無で見分けられるようにするためです。

usera3での確認

usera3でログインし、ExploreでLokiに以下のクエリを投げてみます。

sum by (ecs_task_definition) (count_over_time({job="app"} | json [1m]))

sr-grafana-multitenant-05

grafana-o11y-app-a-frontendgrafana-o11y-app-a-backendの2系列だけが出ていて、app-Bのログは出てきません。

Drilldown → Tracesを開くと、右上のerror/sは0のままです。

sr-grafana-multitenant-06

AMPも見てみます。

sum by (http_response_status_code) (rate(app_requests_total[1m]))

sr-grafana-multitenant-07

ステータスコードは200だけで、テナントAのワークスペースの値だけが返っています。

userb3での確認

同じことをuserb3でやってみます。

sr-grafana-multitenant-08

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

sr-grafana-multitenant-09

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

sr-grafana-multitenant-10

AMPにも500の系列が出ています。
同じGrafana、同じLoki / Tempoを使っていても、Organizationごとに見えるデータが分かれていることが確認できました。

Editorによるデータソース変更の確認

テナントを分けているのはデータソースの設定なので、ここを書き換えられると分離が崩れます。
usera3はEditorなので、そもそもデータソースを変更できるかを確認します。

sr-grafana-multitenant-11

左メニューに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]))

sr-grafana-multitenant-12

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

Drilldown → Tracesも見てみます。

sr-grafana-multitenant-13

テナントBのエラーがerror/sに出ていて、frontendのRateは約3.1と、userb3で見たとき(約1.6)のほぼ倍になっています。
両テナントのトレースがまとめて見えています。

ちなみに、Lokiの横断検索では、検索する期間によって片方のテナントのログしか返らないことがありました。
これはLokiの既知の不具合として報告されていて、検索範囲がLokiのメモリ上のデータとS3上のデータの両方にまたがると、2つ目のテナントの結果が欠けます(2026年9月時点で未修正です)。
https://github.com/grafana/loki/issues/21936

エラーは出ずに一部のテナントの結果だけが欠けるので、横断検索の結果を集計などに使う場合は注意が必要です。

補足:Loki / TempoのBasic認証とForward OAuth identity

Loki / TempoのデータソースのAuthenticationでは、Basic authenticationForward 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.

https://grafana.com/docs/loki/latest/operations/authentication/

リバースプロキシで認証してからX-Scope-OrgIDを付ける構成にすれば、テナントはGrafanaのデータソースに入れたヘッダーではなく、認証したユーザーやトークンから決まります。
Basic認証のユーザーとテナントを対応付けるプロキシは、OSSでも公開されています。
https://github.com/giantswarm/grafana-multi-tenant-proxy

複数のプロジェクトで使う場合の分け方

今回の構成では、テナントを分けているのはデータソースの設定です。
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を上書きするなどの設定が必要です
  • パターン3: テナントごとにGrafanaを分ける
    • Grafana: テナントごとに1つ立てる
    • テナント側のユーザー: Organization Admin(Grafana Server Adminでもよい)
    • データソース: テナント側が設定する
    • Grafanaのタスクロール: そのテナントのワークスペースだけ読める権限を直接付ける
    • 運用者: 全テナントのデータソースを持つ運用者用のGrafanaを別に立てる
    • Grafanaの中に他テナントのデータソースが無く、タスクロールも他テナントのワークスペースを読めないので、管理者権限を渡しても他テナントは見えません
      • Grafanaの台数分のコストと運用がかかります

最後に

今回は、Loki / Tempo / AMPの保存先をテナントごとに分け、Grafana OSS版のOrganizationと結びつけて、見えるデータを分離できるかを試してみました。
この記事がどなたかの参考になれば幸いです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事