こんにちは、SREグループのカンタンです!
GO株式会社にある様々なサービスは、SREが提供しているKubernetes基盤上で動いています。ログ検索基盤として長年BigQueryを利用していましたが、利用体験と効率を向上させるためにGrafanaをベースとした新しいオブザーバビリティ基盤に移行しました。ログに加えてメトリックスとトレース情報を扱えるようになり、可視化が楽になり、ログベースのアラートなど様々な機能が使えるようになりました。
複数の記事に分けて説明させていただきますがこの記事ではSREが作ったオブザーバビリティ基盤の背景と全体像をご紹介できればと思います。
オブザーバビリティ基盤をリニューアルする前はKubernetes (AWS EKS と GCP GKE)で動いているアプリケーションのログをfluent-bitで収集しリアルタイムでBigQueryに送っていました。大量のログデータを検索しても結果が素早い時間で返ってきたり、集計も簡単にできたり、テーブルを分けることでアプリケーション単位の細かい権限管理も可能になったりしていてメリットがそれなりにありましたが以下の課題もありました。
障害の原因特定と解決を早めるため、または問い合わせ対応の効率を向上させるため、基盤をリニューアルすることになりました。ただのログ基盤だけではなく、ログ、メトリックス、トレース情報を全て扱えて、ダッシュボードやアラートを簡単に作れる本格的なオブザーバビリティ基盤が目標でした。
基盤のリニューアルにあたって検証や移行作業が発生しそれなりの工数がかかってしまうため、世の中にあるプロダクトをしっかり検証し、これから長年GO Inc.を支える仕組みを選ぶようにしました。
実データを流しながら次のプロダクトを様々な観点で評価しました:OpenSearch、Grafana、New Relic、Google Cloud Operations (旧Stackdriver)、Amazon CloudWatch。どのプロダクトも印象がとても良くて選ぶことが大変でした。参考まで評価の結果を以下にまとめますが、絶対的な評価ではなくあくまでもGO Inc.の今の状況の中でのSREの評価だけであって、ニーズによって異なります。
次の項目ごとに1〜5のスコアを付けて評価しました (1があまり良くない、5がとても良い):検索の速さ (Search Speed)、ユーザビリティ (Usability)、可視化の良さ (Visualization)、メトリックの扱い (Metrics)、トレースの扱い (Traces)、ログからトレースに飛ぶなどオブザーバビリティ基盤としての一体感 (Observability Platform)、セキュリティ (Security)、ログの集計と分析 (Aggregation)、運用の楽さ (Maintenance)、料金 (Pricing)
プロダクトごとのメリットデメリット:
どのプロダクトもメリットとデメリットがあって、圧倒的に優位なものがなかったです。一方で検証することで以下の2つのユースケースの利用パターンが異なりすぎて分けて考えた方がいいとわかりました。
その気づきを踏まえて検証し、弊社エンジニアからのフィードバックをもらい、Grafanaをオブザーバビリティ基盤として採用することにしました。独自サービスを導入することでGrafanaのセキュリティを強化し、ダッシュボードとドキュメントを事前に用意することでユーザビリティを向上させ、準リアルタイムなBigQuery連携の仕組みを導入することで長期間のログ集計も対応できました。
最終的な基盤の評価が以下のようになりました。
運用以外、全ての項目はかなり高いスコアになりました。自前運用の手間が気になるポイントでしたが、運用してみて半年以上経っていて、手間があまりかからないことがわかりました!そして前の基盤と比べて大凡30%のコスト削減を達成しながら本格的なオブザーバビリティ基盤を提供できました!ここからはGrafanaとSREのオブザーバビリティ基盤をご紹介します。
Grafanaとは、Grafana Labs社が開発したOSSのデータ可視化ツールです。Grafana自体はあくまでもGUIですが、以下のツールと組み合わせることでオブザーバビリティ機能を提供できます (LGTMスタック)
他に連携できるデータソースがたくさんあり、一つの可視化ツールで様々なデータを閲覧したり可視化したりできます。
Grafana関連のプロダクトがOSSのため、自前で運用することが可能です。Grafana Cloudというマネージドサービスも存在していて運用が大変な場合はそういう選択肢もあります。ただしデータ量とアクティブユーザ数に比例する課金モデルになってしまうため、GO Inc.では自前で運用することにしました。
自前運用でもGrafana Enterpriseに加入すればデータソースの権限管理などセキュリティ関連の機能が増えるのとGrafanaの方が運用のサポートをしてくれます。毎月基本料金$3500+$36/アクティブユーザかかりますのでコストを抑えるために加入しませんでした。その代わりに、セキュリティを向上させるためにGrafanaと連携する独自のマイクロサービスを開発しました。
Grafana、Loki、Mimir、Tempoを動かすための専用のAWS EKSオブザーバビリティクラスタを構築しました。タクシーアプリ『GO』のサービスと同じクラスタでGrafanaを動かしてしまうと、KubernetesやGrafanaのアップデートなどメンテナンスの際に万が一問題が発生した場合はサービスの問題なのかオブザーバビリティの問題なのか切り分けが難しくなり混乱してしまうため専用クラスタにしました。タクシーアプリ『GO』のサービスがAWS EKSとGCP GKEを両方利用していることもあって、オブザーバビリティ用の専用のクラスタがあった方が構成がシンプルになります。
全体のアーキテクチャが以下のようになります。
プロダクトごとに別記事で詳細に説明しようと思いますので、今回はざっくりと説明します。
右側にオブザーバビリティクラスタがあります
左側にサービス用のAWS EKSとGCP GKEクラスタがあります
コンポーネントが少し多いですがサービスクラスタで動くものがエージェントのみになっているのとEKSとGKEの環境の差がほとんどないため運用が楽です。オブザーバビリティサービスが全て一箇所にまとめられているためサービスと分けて管理しやすいです。
GO Inc.ではそれぞれのサービスへのアクセスを細かく管理しています。具体的にグループごとに権限管理を行なっていて、エンジニアが所属しているグループのサービスにしかアクセスできないようにしています。オブザーバビリティ基盤もログ、メトリックス、トレース情報へのアクセス権限も細かく管理する必要があります。
Mimir、Loki、Tempoはマルチテナントなシステムのため細かい権限管理が可能になっています。Promtail、Prometheus、OpenTelemetry Collectorそれぞれのエージェントがデータを送る際に、データのテナント(持ち主)を X-Scope-OrgID リクエストヘッダとして送っています。データが最終的にS3に保存されますがテナントごとにフォルダが分かれていて混ざらないようになっています。データのアクセス権限が基本的にテナント単位になりますので、データを送る時に適切なテナントを指定することが重要です。Kubernetesで動いているサービスの場合はKubernetesのネームスペースをテナントとして利用することが一番自然でそれを採用しています。
Grafanaは様々な認証方法を対応していますがSREではAzure ADのSSO認証を採用しています。ただしGrafana Entepriseを利用していないため、そのままだとLoki、Mimir、Tempoへのアクセスを細かく制御できずGrafanaに認証できる方全員が全てのメトリックス、ログ、トレース情報を見れてしまいます。認可レイヤを追加するためにアーキテクチャ図にあったLoki Oauth GatewayとMimir OAuth Gatewayを開発しました。
たとえばメトリックスを閲覧する際、GrafanaからMimirへの検索リクエストがOAuth Gatewayを経由させるようにしています。OAuth Gatewayは認証の際に取得されたOAuthトークンをパースしユーザ情報を抽出できます。その情報を元にリクエストを許可するかしないかを決めています。
OAuth Gatewayは単純なOpenResty(Nginx)プロキシとして開発しました。LuaでOauthトークンをパースしユーザが所属しているAzure ADグループを抽出します。アクセスしようとしているテナント情報がGrafanaからのリクエストの X-Scope-OrgIDヘッダに入っているため、Azure グループごとにアクセス可能なテナントさえ決めれば認可仕組みを簡単に実装できます。参考までコードのサンプルを以下に残しています。
nginx.conf
lua_package_path '/etc/nginx/?.lua;;';
server {
listen 8080;
location = / {
return 200 'OK';
}
...
location /prometheus {
# check permissions
rewrite_by_lua_file /etc/nginx/auth.lua;
# send request to mimir
set $mimir_backend my-mimir.mimir.svc.cluster.local;
proxy_pass http://$mimir_backend:8080$request_uri;
}
}
auth.lua
local jwt = require "resty.jwt"
-- settings: list of allowed tenants per Azure AD group
local authorizations_table = {
group1 = { "tenant1" },
group2 = { "tenant2" },
group3 = { "tenant1", "tenant2" },
}
-- retrieve JWT token containing user information
local id_token = ngx.req.get_headers()['x-id-token']
if not id_token then
ngx.status = 403
ngx.say("403 Forbidden: cannot find JWT token")
return ngx.exit(403)
end
-- parse JWT token, retrieve user groups
local jwt_obj = jwt:load_jwt(id_token)
local jwt_payload = jwt_obj['payload']
local groups = jwt_payload['groups']
-- determine user authorizations
local authorizations = {}
if groups then
for k, v in pairs(groups) do
local group_authorizations = authorizations_table[v]
if group_authorizations then
for k2, v2 in pairs(group_authorizations) do
authorizations[v2] = true
end
end
end
end
-- ensure user is authorized to access X-Scope-OrgID header tenant
local scope_orgid = ngx.req.get_headers()['x-scope-orgid']
local scope_orgid_authorized = false
for k, v in pairs(authorizations) do
if k == scope_orgid then
scope_orgid_authorized = true
break
end
end
if not(scope_orgid_authorized) then
ngx.status = 403
ngx.say("403 Forbidden: not allowed to access this tenant")
return ngx.exit(403)
end
MimirとLokiにアクセスする時にOAuth Gatewayを経由させることでユーザが所属しているグループによってログやメトリックスの細かいアクセス制御を実現できます。トレース情報を管理するTempoに関してはそこまで細かく権限管理する必要がなかったため現在はOAuth Gatewayを利用していないですが必要に応じて簡単に導入できます。
アクセス制御だけではなく、OAuth Gatewayを経由することで誰がいつどのデータにアクセスしたか、網羅的な監査ログも残せて便利です!
セキュリティを向上させるためにマルチレイヤのセキュリティ対策が重要で、認証と認可に加えてデータにアクセスする際にIP制限をかけるようにしました。ただしIP制限をかけることで障害の際に状況が把握しづらくなり対応が遅れてしまうリスクもあるため、たとえばログデータに対してはIP制限をかけるが一部のメトリックスに対してはIP制限をかけないなどある程度制御できるような仕組みが必要でした。
先ほど紹介したOAuth Gatewayに新たな実装を追加することでアクセスするデータに応じてのIP制限を実現できました。注意点として、GrafanaがMimirとLokiに転送できるリクエスト情報が限られていてクライアントIPを転送できなかったためクライアントIPをCookieに入れる必要がありました。そのため、クライアントIPをCookieに入れるGrafana Gatewayを導入しました。最終的な構成が以下のようになります。
Grafana GatewayもOpenRestyベースのサービスで、処理が単純のため割愛します。IP制限のためにOAuth Gatewayに以下のような実装を追加しました。
-- authorized ips
local authorized_ips = { "xxx.xxx.xxx.xxx", "yyy.yyy.yyy.yyy" }
-- retrieve client ip
local client_ip = ngx.var.cookie_Client_Ip
-- check allow list
if not(authorized_ips[client_ip]) then
ngx.status = 403
ngx.say("403 Forbidden: ip not allowed")
return ngx.exit(403)
end
この例だと全てのリクエストを許可するかしないかという処理になりますが、ユーザが所属しているグループとアクセスしようとしているテナントデータに応じて制御が可能なため、会社のセキュリティポリシーに柔軟に対応できて、かなり便利です!
「プロダクトの選定」のところで既述したように、長期間のログデータの検索と集計を別で考える必要があります。そのユースケースを対応できるように、ログデータをLokiだけではなく、大量なデータ検索と集計に優れているBigQueryに投入することにしました。ただしログを完全にリアルタイムでそのままにBigQueryに投入してしまうと、AWSのデータ転送料とNATゲートウェイ料金だったり、BigQueryのインジェスト料金がかかったりするためGCPのStorage Transfer Serviceをベースとした仕組みを用意しました。
流れを簡単に説明すると、
少し複雑に見えるかもしれないですがこの仕組みのメリットがたくさんあります
GrafanaだけではなくBigQuery連携の仕組みを設けることで、様々なユースケースに対応でき会社の方針に柔軟に対応可能になっています。普段はオブザーバビリティ基盤Grafanaでメトリックスとトレースと一緒にログを閲覧したり可視化したりしていますが、大量のログデータの集計または他のデータセットとジョインする必要が出た時にBigQueryを利用しています。
オブザーバビリティ関連のプロダクトがたくさんあり選定することが難しかったですが、Grafanaを本番環境で使い始めてから半年経っていて非常に好印象です!問い合わせ対応のためのちょっとした可視化が非常に楽にできるようになり、メトリックスとログとトレースを一箇所で見れることも便利です。自前運用が少し恐れていましたが運用してみてあまり大変ではないことがわかりました!
参考までオブザーバビリティ基盤の最終的なアーキテクチャ図を以下にまとめています。
Loki、Mimir、Tempoは今まであまりなかった非常に面白い仕組みを利用しているため、別記事で詳細に説明したいと思います。是非読んでみてください!
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @goinc_techtalk のフォローもよろしくお願いします!