k8s 上に OpenObserve を構築してクラスタのログを可視化する
概要
OpenObserve (以下 o2) は Observability の 3 大要素 (Logs, Metrics, Traces) を一元管理・可視化するためのプラットフォームです。
fluentd, prometheus, otel collector といった各種エージェントから送信されるデータを取り込んで一括に保存し、プラットフォーム上で可視化や分析が行えるようになっています。Rust で書かれているためパフォーマンスが良く、比較的新しいプロジェクトながら github star は ~ 9.5 k となっています。
o2 は elasticsearch のデータ取り組み・保存・クエリの機能と kibana の webUI の機能を兼ね備えており、公式でも Elasticsearch との互換性やパフォーマンスの比較がされています (参考: How does OpenObserve compare to Elasticsearch)。そのため Observability の 3 大要素の可視化・分析の部分に関しては elasticsearch + kibana をそのまま o2 に置き換えることができます。
以前に書いた Filebeat による k8s クラスタ Logging では下図のようにログ管理基盤として elasticsearch + kibana を使用しました。
以前の構成
o2 ではこの部分を以下のように置き換えることができます。
今回の構成
というわけで、今回は k8s クラスタ上に o2 を構築してクラスタ logging とアラート機能を試してみます。
セットアップ
o2 ではクラウド上で o2 の企業が管理する基盤にアクセスするマネージド型と自分の環境に o2 を構築する self-hosted 型があります。マネージド型は基本的に有償なので、今回は k8s クラスタ上に self-hosted 型の o2 を構築します。
HA 構成の準備
k8s クラスタ上での構築はシングル構成 と helm chart による HA 構成 が選べます。せっかくなのでここでは HA 構成にします。
HA 構成でデータ管理のために以下のような外部コンポーネントが必要になります。
種類 | 用途 | 使えるもの |
---|---|---|
オブジェクトストレージ | 各種エージェントから取り組んだデータ (logs, metrics, traces) | S3, minio など |
RDB | o2 のメタデータ | mysql, postgres など |
Coordinate | HA クラスタの管理 | ETCD, NATS など |
今回はそれぞれのコンポーネントを使用します。
種類 | 用途 | インストール方法 |
---|---|---|
オブジェクトストレージ | minio | helm により別途構築 |
RDB | CloudNativePG | helm により別途構築 |
Coordinate | ETCD | o2 と同時に構築 |
CloudNativePG
CloudNativePG は postgres と互換性を持つ k8s 用の RDB です。
まず helm を使って operator をインストールします。
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
cnpg/cloudnative-pg
次に o2 で使う user, password, database を作成します。user, password はいずれも o2
として secret に保存します。
apiVersion: v1
data:
username: bzI=
password: bzI=
kind: Secret
metadata:
name: o2-postgres-secret
namespace: o2
type: kubernetes.io/basic-auth
kubectl create namespace o2
kubectl apply -f secret.yml
database は CloudNativePG の cluster CRD 作成時に bootstrap で作成します。
piVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: o2-pg
spec:
instances: 3
bootstrap:
initdb:
database: o2
owner: o2
secret:
name: o2-postgres-secret
storage:
size: 2Gi
これを apply すると instances に設定した数の pod と接続用の svc が作成されます。
$ kubectl get pod,svc -l cnpg.io/cluster=o2-pg
NAME READY STATUS RESTARTS AGE
pod/o2-pg-1 1/1 Running 0 5h56m
pod/o2-pg-2 1/1 Running 0 5h56m
pod/o2-pg-3 1/1 Running 0 5h55m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/o2-pg-r ClusterIP 10.110.72.78 <none> 5432/TCP 5h56m
service/o2-pg-ro ClusterIP 10.106.93.194 <none> 5432/TCP 5h56m
service/o2-pg-rw ClusterIP 10.110.137.148 <none> 5432/TCP 5h56m
CloudNativePG は postgres と互換性があるので psql などで接続できます。
$ psql "postgres://o2:o2@10.110.137.148:5432/o2"
psql (15.5 (Ubuntu 15.5-0ubuntu0.23.04.1), server 16.2 (Debian 16.2-1.pgdg110+2))
WARNING: psql major version 15, server major version 16.
Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.
o2=>
minio
minio はまず ドキュメントの手順 に沿って operator をインストールします。
helm repo add minio-operator https://operator.min.io
helm install \
--namespace minio-operator \
--create-namespace \
operator minio-operator/operator
次に tenant (S3 のリージョンみたいなもの) を作成します。ドキュメントでは operator console を使って作成する方法が推奨されていますが、community が管理する tenant 用 helm chart があるのでこちらを使ってデプロイします。
chart 設定ファイルを取得。
helm show values minio-operator/tenant > values.yml
ここでは tenant 名を ap-northeast-1 とし、リソース節約のため tenant 用の pod, pvc を 2 つずつにします。
tenant:
name: ap-northeast-1
pools:
###
# The number of MinIO Tenant Pods / Servers in this pool.
# For standalone mode, supply 1. For distributed mode, supply 4 or more.
# Note that the operator does not support upgrading from standalone to distributed mode.
- - servers: 4
+ - servers: 2
###
# Custom name for the pool
name: pool-0
###
# The number of volumes attached per MinIO Tenant Pod / Server.
- volumesPerServer: 4
+ volumesPerServer: 2
###
デプロイ
helm install -n o2 minio minio-operator/tenant -f values.yml
これで ap-northeast-1 tenant として 2 つの minio pod が起動する。
$ kubectl get pod,svc,pvc -l v1.min.io/tenant=ap-northeast-1
NAME READY STATUS RESTARTS AGE
pod/ap-northeast-1-pool-0-0 2/2 Running 0 6h4m
pod/ap-northeast-1-pool-0-1 2/2 Running 0 6h4m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ap-northeast-1-hl ClusterIP None <none> 9000/TCP 6h4m
service/minio ClusterIP 10.100.150.186 <none> 443/TCP 6h4m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data0-ap-northeast-1-pool-0-0 Bound pvc-9e1404d0-9bf0-4631-9475-8168baa5b300 5Gi RWO openebs-hostpath <unset> 6h4m
persistentvolumeclaim/data0-ap-northeast-1-pool-0-1 Bound pvc-4221a568-2dd4-4817-b1c8-dbd388639bd9 5Gi RWO openebs-hostpath <unset> 6h4m
persistentvolumeclaim/data1-ap-northeast-1-pool-0-0 Bound pvc-817b006c-1772-4d22-b275-73b4ca7fa64c 5Gi RWO openebs-hostpath <unset> 6h4m
persistentvolumeclaim/data1-ap-northeast-1-pool-0-1 Bound pvc-3f59ed35-d04c-4afc-9329-60334f9efba6 5Gi RWO openebs-hostpath <unset> 6h4m
minio 用の CLI mc を使って o2 用に使用するオブジェクトストレージの bucket を作成します。
mc のインストール。
curl https://dl.min.io/client/mc/release/linux-amd64/mc -o mc
chmod +x mc
sudo mv mc /usr/local/bin/mc
mc --help
bucket を作成。
# alias を設定
$ mc alias set minio https://10.100.150.186 minio minio123 --insecure
# `o2-bucket` 名で bucket 作成
$ mc mb --insecure minio/o2-bucket
# 確認
$ mc ls --insecure minio
[2024-04-28 08:02:42 UTC] 0B o2-bucket/
OpenObserve インストール
o2 は helm chart でインストールできますが、設定をカスタマイズするために values.yml
に書き出します。
helm repo add openobserve https://charts.openobserve.ai
helm show values openobserve/openobserve > values.yml
取り組んだデータの保存先に先ほど作成した minio を指定するため、values.yml 内で以下の値を設定します。
- S3_ACCESS_KEY, S3_SECRET_KEY は minio に接続するための accesskey, secretkey を指定。デフォルトでは minio の values に書いてある
minio
,minio123
。 - ZO_S3_PROVIDER: minio
- ZO_S3_SERVER_URL: minio の headless svc に接続するためのアドレスを指定
- ZO_S3_REGION_NAME: tenant 名を指定
- ZO_S3_BUCKET_NAME: bucket 名を指定
auth:
ZO_S3_ACCESS_KEY: "minio"
ZO_S3_SECRET_KEY: "minio123"
config:
ZO_S3_PROVIDER: "minio"
ZO_S3_SERVER_URL: "http://ap-northeast-1-hl.minio.svc.cluster.local:9000"
ZO_S3_REGION_NAME: "ap-northeast-1"
ZO_S3_BUCKET_NAME: "o2-bucket"
minio:
enabled: false
次に o2 メタデータ保存先に先ほど作成した CloudNativePG を指定。
- ZO_META_STORE: postgres
- ZO_META_POSTGRES_DSN: postgres 接続先 DB を
postgres://[user]:[password]@[host]:[port]/[db]
形式で指定。
config:
ZO_META_STORE: "postgres"
ZO_META_POSTGRES_DSN: "postgres://o2:o2@o2-pg-rw.o2.svc.cluster.local:5432/o2"
postgres:
enabled: false
以上で準備が完了したのでデプロイ
helm install -n o2 o2 openobserve/openobserve -f values.yml
これでクラスタ管理用の ectd と o2 内部コンポーネントの pod が起動します。
$ kubectl get pod -l app.kubernetes.io/instance=o2
NAME READY STATUS RESTARTS AGE
o2-etcd-0 1/1 Running 1 (6h10m ago) 6h10m
o2-etcd-1 1/1 Running 1 (6h10m ago) 6h10m
o2-etcd-2 1/1 Running 1 (6h10m ago) 6h10m
o2-openobserve-alertmanager-6946c5ff7c-2vxkz 1/1 Running 0 6h10m
o2-openobserve-compactor-55bf86947c-tzx5m 1/1 Running 1 (6h9m ago) 6h10m
o2-openobserve-ingester-0 1/1 Running 0 6h10m
o2-openobserve-querier-69f4c56c6c-vxmj5 1/1 Running 1 (6h9m ago) 6h10m
o2-openobserve-router-55c6d54b5c-wfplq 1/1 Running 1 (6h9m ago) 6h10m
o2 では o2-openobserve-router
svc の port 5080 で webUI にアクセスできます。values.yml 内に記載されている以下の root ユーザーでログインできます。
auth:
ZO_ROOT_USER_EMAIL: "root@example.com"
ZO_ROOT_USER_PASSWORD: "Complexpass#123"
起動直後ではデータが何も取り込まれていないため、これから filebeat + logstash を使ってクラスタ上の pod のログを収集して o2 に保存します。
Filebeat
o2 に投入するログデータは o2 に依存せず、fluentd や filebeat など各種ログエージェント側の設定に従います。そのため Filebeat による k8s クラスタ Logging で使用した Filebeat の設定がそのまま使えます。
基本的には記事内の設定と同じですが、個々では簡単のため pod のログのみ収集対象にするように filebeat.yml を設定します。
daemonset:
extraEnvs: []
filebeatConfig:
filebeat.yml: |
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
- add_fields:
target: ""
fields:
cluster: k8s-cluster
output.logstash:
host: '${NODE_NAME}'
hosts: ["logstash-logstash.es.svc.cluster.local:5044"]
Logstash
filebeat が収集するログの parse と構造化に使用する logstash も前回の設定がそのまま使用できます。前回は docker で構築してましたが、せっかくなのでここでは logstash も helm で k8s クラスタ上に構築します。
$ helm show values elastic/logstash > values.yml
output の設定
o2 にログを投入する際に対応可能なログエージェントは Ingestion に書いてありますが、logstash の設定例は記載されていません。ただ、JSON や BULK などの REST API を使うことで json 形式の log が取り込めるようになっており、issue にあるように logstash では http output plugin を使うことで REST API 経由でデータを送信できるので、こちらを参考に logstash の output を設定します。
- urlには o2 の route svc, port 5080 を指定する。
- エンドポイントの書式は
/api/[organization]/[stream]/_multi
に従う。 - organization は
default
がデフォルトで作成されているようなのでこれを指定。 - stream はない場合は自動で作成されるので好きな名前で良い。
- エンドポイントの書式は
- o2 の認証情報は headers に base64 encode したものを指定する。
- デフォルトのユーザーで認証する場合は
echo -n "root@example.com:Complexpass#123" | base64
で encode された値を指定。
- デフォルトのユーザーで認証する場合は
- mapping では投入する値を key-value で指定していく。key 名は投入するに応じて任意に指定し、value には対応する logstash 側のデータを logstash の変数形式で指定。
-
kubernetes
などネストされた json データをまとめて mapping するやり方がわからなかったので、ここではデータ分析に有用そうな値を指定することにする。
-
output {
http {
url => ["http://o2-openobserve-router.o2.svc.cluster.local:5080/api/default/k8s/_multi"]
format => "json"
http_method => "post"
content_type => "application/json"
headers => ["Authorization", "Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM="]
mapping => {
"@timestamp" => "%{[@timestamp]}"
"message" => "%{[message]}"
"host_name" => "%{[host][name]}"
"cluster_name" => "%{[cluster]}"
"container.name" => "%{[kubernetes][container][name]}"
"node.name" => "%{[kubernetes][node][name]}"
"node.hostname" => "%{[kubernetes][node][hostname]}"
"pod.name" => "%{[kubernetes][pod][name]}"
"pod.ip" => "%{[kubernetes][pod][ip]}"
"namespace" => "%{[kubernetes][namespace]}"
}
}
}
xpack monitoring の無効化
今回の検証とは関係ないですが、xpack.monitoring を有効化していると elasticsearch に license を取得しにいこうとして logstash ログに多数のエラーが出力されます。これは煩わしいので xpack.monitoring.enabled: false
で無効化しておきます。
logstashConfig:
logstash.yml: |
http.host: "0.0.0.0"
#xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]
xpack.monitoring.enabled: false
デプロイ
以上をまとめると logstash 用の values.yml の変更点は以下。
values.yml
logstashConfig:
logstash.yml: |
http.host: "0.0.0.0"
#xpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]
xpack.monitoring.enabled: false
logstashPipeline:
logstash.conf: |
input {
beats {
port => 5044
}
}
filter {
if [kubernetes][container][name] == "filebeat" {
json {
source => "message"
}
mutate {
rename => {
"log.level" => "loglevel"
"@timestamp" => "timestamp"
"message" => "logmessage"
}
}
}
}
filter {
date {
match => ["timestamp", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"]
timezone => "Asia/Tokyo"
target => "@timestamp"
}
}
output {
http {
url => ["http://o2-openobserve-router.o2.svc.cluster.local:5080/api/default/k8s/_multi"]
format => "json"
http_method => "post"
content_type => "application/json"
headers => ["Authorization", "Basic cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM="]
mapping => {
"@timestamp" => "%{[@timestamp]}"
"message" => "%{[message]}"
"host_name" => "%{[host][name]}"
"cluster_name" => "%{[cluster]}"
"container.name" => "%{[kubernetes][container][name]}"
"node.name" => "%{[kubernetes][node][name]}"
"node.hostname" => "%{[kubernetes][node][hostname]}"
"pod.name" => "%{[kubernetes][pod][name]}"
"pod.ip" => "%{[kubernetes][pod][ip]}"
"namespace" => "%{[kubernetes][namespace]}"
}
}
}
service:
ports:
- name: beats
port: 5044
protocol: TCP
targetPort: 5044
デプロイ
helm install -n es logstash elastic/logstash -f values.yml
少し待つと logstash pod が起動し、filebeat → logstash → o2 ingestor でログが送信されるようになります。うまくいかない場合は logstash pod のログを見て適宜修正。
ログを OpenObserve WebUI で確認する
o2 に投入されたログは webUI 等で確認できます。
左のメニューバーから log を選択すると、はじめにログを検索する stream の選択する必要があるので、ログ投入時に指定した k8s
を選びます。右上の Run query
をクリックすると、指定した範囲 (デフォルトでは現時刻から 15 分前) で投入されたログのヒストグラムと実際のログの中身が閲覧できます。
左側にある field では投入されたログの field 名に基づいてクエリを実行したり、column の表示・非表示の切り替えが選択できます。ここでは logstash の http plugin の mapping で指定した key 名が表示されます。cluster_name、node、namespace、pod name, container name などを選択すると、ログがどこから発生したものであるのか一目で判別できるようになります(現時点では列幅を調整する機能はなさそうなので若干見づらいですが)。
field 名をクリックすると表示範囲内でのログの field の内訳が確認できます。例えば下図の例では node_hostname
field で k8s-w1
ノード上から収集されたログが 66.7 k, k8s-w2
ノード上から収集されたログが 66.8 k というような感じです(単位がわからないがおそらくログレコード数)。
特定の条件を満たすログを抽出する場合は左上の Query Editor に Query を入力します。クエリ文はかなり補完してくれたり、左側の field 内の項目を選択すると自動で query 条件を追加してくれたりします。
クエリを実行することで指定した条件を満たすログのみが表示されます。
以上の点から、UI の見た目やレイアウトも含めて使い勝手は kibana にかなり似ており、kibana を既に使っている場合にはシームレスに移行できます。また、kibana と比較すると必要な機能のみがコンパクトにまとまっている印象を受けます。比較のため kibana と o2 の UI を以下に乗せておきます。
kibana の画面
o2 の画面
ログアラートの設定
o2 では取り込んだログの内容を元にアラートを設定することができます。o2 のアラートでは一般的なアラートと同様に取り組んだデータを必要に応じて集計(aggregate)し、条件を満たしたら事前に設定した送信先に alert 情報を送信する方式になっています(例えば 5 分間の間に特定の deployment で error
を含むログが 10 回以上出力されたら slack に通知するなど)。
ここでは自作アプリケーションで error ログを出力して実際にアラートが発生した際に slack で通知を行う動作を確認してみます。
アラートの作成
まず alert > templates > Add template から新規にテンプレートを作成します。テンプレートはアラートが発生した際に指定先に送信されるメッセージの中身となっています。ドキュメント にあるようにいくつかの組み込み変数が使用可能。
次に Destination を作成します。Destination は alert が発生した際の template の送信先となります。ここでは slack webhook でメッセージを post するため、webhook の url を指定します。
最後に alert の発生条件を指定します。ここでは自作アプリケーションのコンテナ名を myapp
とし、1 分間の間に 10 回以上 error
を含むメッセージが出力されたらアラートが上がるように設定します。
自作アプリケーションは適当に python で作成し、/error
にアクセスしたら error
を含むログを出力するようにします。
from flask import Flask, request
app = Flask(__name__)
@app.route("/", methods=["GET","POST"])
def route():
print(request.get_data())
return "ok"
@app.route("/error", methods=["GET","POST"])
def err():
return "error", 500
if __name__ == "__main__":
app.run(host="0.0.0.0")
これをコンテナ化し、myapp pod としてクラスタ上で稼働させます。
$ kubectl get pod,svc
NAME READY STATUS RESTARTS AGE
pod/myapp-c67fd8d6c-2h9n2 1/1 Running 0 38h
pod/myapp-c67fd8d6c-nkq8w 1/1 Running 0 38h
pod/myapp-c67fd8d6c-xns7w 1/1 Running 0 38h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/myapp ClusterIP 10.110.55.135 <none> 5000/TCP 38h
実際にリクエストを送信して o2 側で閾値以上のログを検出すると template の内容に基づいて slack にtemplate で指定したメッセージが投稿されます。
template で text field のみ指定した場合のメッセージは上記のとおり淡白であるため、可読性を考えるなら block field などで format を作り込む、prometheus alertmanager を経由して post するなどいろいろ工夫の余地があります。
Architecture
最後に o2 の architecture について見ておきます。architecture に関しては ドキュメントの High Availability (HA) mode の図がわかりやすいのでこちらを参照。
o2 の architecture 図。https://openobserve.ai/docs/architecture/#high-availability-ha-mode より引用
構築時に見たように o2 内部コンポーネントは 5 つ存在し、機能ごとに k8s pod が別れて起動します。それぞれの機能は上記のドキュメントに詳細が書かれていますが、ざっくりまとめると以下のようになっています。
- Router: 外部からのアクセスを querier や ingester にルーティングするプロキシの役割
- Ingester: 投入されたログやメトリクスを適切な形式に変換してオブジェクトストレージに保存する
- Querier: 保存されたデータをクエリして取得する
- Compactor: ストレージ内のデータをまとめたりデータ保持ポリシーを管理するなど、データ保存効率を向上させる役割
- AlertManager: アラート関係の機能
一方で、以下の機能は o2 内に含めず外部に切り出すことによりスケーリング性を高める構成になっています。
- Ingestion tool: logging agent など
- Cluster Coordinate: 内部コンポーネントのクラスタ管理
- RDB: データベース
- Data Store: 投入されたデータを保存するオブジェクトストレージ
- Authentication: ユーザ管理と認証
o2 では stateless node architecture を採用しており、ingester や querier などの内部コンポーネントは state を持ちません。Elasticsearch の HA 構成ではノード間でクラスタを構成するために内部的に node role などの機能を持ちますが、o2 の HA 構成では etcd など外部の Cluster Coordinate を利用しているのが対象的でクラウドネイティブであるとも言えます。これにより k8s クラスタ上での取り回しやパフォーマンスの良さ、スケール性を確保しているのが特徴となっており、ドキュメントでも elasticsearch との比較としてこの点を強調しています。
おわりに
現在の Elasticsearch + kibana はログ、メトリクス、トレースなどの Observability の可視化だけでなく全文検索やアプリケーション分析、時系列分析、 anomaly detection など非常に多機能に渡っていますが、それゆえ本格的な運用を考えると ES 自体の学習コストやリソースのコストが軽視できません。OpenObserve は ES から Observability の部分のみを切り出したというような立ち位置になっています。公式でも elasticsearch とのパフォーマンス比較や使い勝手、互換性が強調されているように、クラスタ上で HA 構成の Observability 基盤を構築するなら選択肢に入ってくるかと思います。今回は試してませんがログだけでなくメトリクスやトレースの可視化・分析も可能なので、これらを一元管理するためのプラットフォームとして使うといった用途が考えられます。
一方でまだ歴史の浅いプロジェクトであり、コアな機能は備わっているものの細かい部分はまだまだ開発中といった印象があります(個人的には k8s の CRD でアラートなどのリソースを作成できるよう対応してほしい)。
Discussion