🌈

k8s 上に OpenObserve を構築してクラスタのログを可視化する

2024/04/29に公開

概要

OpenObserve (以下 o2) は Observability の 3 大要素 (Logs, Metrics, Traces) を一元管理・可視化するためのプラットフォームです。

https://openobserve.ai/

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 です。

https://github.com/cloudnative-pg/charts

まず 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 に保存します。

secret.yml
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 で作成します。

cluster.yml
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 があるのでこちらを使ってデプロイします。

https://min.io/docs/minio/kubernetes/upstream/operations/install-deploy-manage/deploy-minio-tenant-helm.html

chart 設定ファイルを取得。

helm show values minio-operator/tenant > values.yml

ここでは tenant 名を ap-northeast-1 とし、リソース節約のため tenant 用の pod, pvc を 2 つずつにします。

values.yml
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 名を指定
values.yml
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] 形式で指定。
values.yml
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 を設定します。

values.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 で無効化しておきます。

values.yml
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
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 に通知するなど)。

https://openobserve.ai/docs/user-guide/alerts/alerts/

ここでは自作アプリケーションで 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