
## はじめに

自宅サーバーで運用している Kubernetes クラスタでは、Pod の異常終了や Scheduling の失敗といった Kubernetes イベントを Slack に通知するために [kubernetes-event-exporter](https://github.com/mustafaakin/kubernetes-event-exporter) を利用していました。ただし、2 年ほど前から開発が止まっていることと、将来的に特定のイベントを metrics として時系列データ化したいなど kubernetes-event-exporter だけでは実現できないユースケースがあったため、 OpenTelemetry Collector をベースとしたイベント通知の基盤に置き換えることにしました。

Kubernetes イベントの収集には OpenTelemetry Collector Contrib の `k8seventsreceiver` を利用し、Slack への通知部分は既存のコンポーネントでは要件を満たせなかったため、`notificationexporter` という Exporter を自作しています。

本記事では、kubernetes-event-exporter からこの OpenTelemetry Collector ベースの通知基盤に移行した内容について紹介します。

## 前提

本記事では以下のバージョンを前提としています。

- OpenTelemetry Collector（Contrib コンポーネント）: v0.160.0
- OpenTelemetry Collector Core: v1.66.0
- Go: 1.26

自作した `notificationexporter` は、以下のリポジトリで公開しています。

[ucpr/opentelemetry-collector-components](https://github.com/ucpr/opentelemetry-collector-components)

## kubernetes-event-exporter による通知

これまでは、kubernetes-event-exporter で以下のような設定を行い、Slack へ Kubernetes のイベント通知を行っていました。

```yaml:values.yaml
config:
  route:
    routes:
      - match:
          - receiver: "slack"
            type: "Warning"
  receivers:
    - name: "slack"
      slack:
        token: "$SLACK_TOKEN"
        channel: "#k8s-events"
        message: "{{ .Message }}"
        fields:
          cluster: "{{ .ClusterName }}"
          namespace: "{{ .InvolvedObject.Namespace }}"
          kind: "{{ .InvolvedObject.Kind }}"
          name: "{{ .InvolvedObject.Name }}"
          reason: "{{ .Reason }}"
          count: "{{ .Count }}"
```

`type: "Warning"` の Kubernetes イベントのみを対象に、Slack Bot Token を使って `#k8s-events` チャンネルに通知するというシンプルな構成です。

![k8s_events_to_slack_with_otel-00](https://ucprdev-image-proxy.ucpr.workers.dev/images/articles/k8s_events_to_slack_with_otel/k8s_events_to_slack_with_otel-00.png)

## OpenTelemetry Collector ベースの通知基盤

新しい通知基盤は、以下のようなパイプラインで構成しています。

```
k8s_events (Receiver)
  → memory_limiter (Processor)
  → filter (Processor)
  → transform (Processor)
  → routing (Connector)
  → notification (Exporter)
```

### k8s_events Receiver で Kubernetes イベントを収集する

Kubernetes イベントの収集には、OpenTelemetry Collector Contrib が提供している `k8seventsreceiver` を利用しています。

```yaml
receivers:
  k8s_events:
    auth_type: serviceAccount
    dedup_interval: 10m
```

`auth_type: serviceAccount` を指定することで、Collector に紐付けた ServiceAccount の権限で Kubernetes API から Event リソースを watch します。RBAC は以下のように、`events` リソースへの `get`/`list`/`watch` のみを許可する ClusterRole を用意しています。

```yaml:rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: otel-collector-k8s-events
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: otel-collector-k8s-events
rules:
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: otel-collector-k8s-events
subjects:
  - kind: ServiceAccount
    name: otel-collector-k8s-events
    namespace: opentelemetry
roleRef:
  kind: ClusterRole
  name: otel-collector-k8s-events
  apiGroup: rbac.authorization.k8s.io
```

### Filter / Transform Processor で通知対象を絞り込む

収集した Kubernetes イベントは `filter` Processor で `Warning` のみに絞り込み、`transform` Processor で通知用の属性を組み立てています。

```yaml
processors:
  filter/warning_only:
    error_mode: ignore
    log_conditions:
      - 'log.severity_text != "Warning"'
  transform/enrich:
    error_mode: ignore
    log_statements:
      - 'set(log.attributes["notify.summary"], Concat(["[", log.attributes["k8s.namespace.name"], "] ", resource.attributes["k8s.object.kind"], "/", resource.attributes["k8s.object.name"], ": ", log.attributes["k8s.event.reason"]], ""))'
```

OTTL（OpenTelemetry Transformation Language）でイベントの属性から通知サマリを組み立てておくことで、後段の Exporter 側のテンプレートをシンプルに保つことができます。

kubernetes-event-exporter では、例えば OOM の Event は Node に対して発行されるため Namespace や Pod の情報を持っておらず、プロセス名しか出力されないため、それだけではどのワークロードで発生した問題か切り分けにくいという課題がありました。しかし、Transform Processor 等を利用すれば容易に付加情報を付与できるため、こうした課題にも対応しやすくなります。

`filter` Processor の条件も OTTL で記述するため、`log.severity_text` 以外の属性（Namespace や Reason、対象リソースの種類など）を組み合わせた柔軟なフィルタクエリを追加することも可能です。現状は Warning のみを対象にしていますが、今後特定の Namespace や Reason を通知対象から除外・追加するといった要件にも、このフィルタ条件を拡張するだけで対応できます。

### Routing Connector で通知先を振り分ける

`routing` Connector を挟んでいるのが、この構成の特徴の一つです。現状は以下のように全件を単一の Pipeline に流す設定のみですが、将来的に Namespace や Reason ごとに条件を追加し、複数の通知先に振り分けられるように設計しています。

```yaml
connectors:
  routing/notify:
    default_pipelines: [logs/notify_default]
    table:
      - context: log
        condition: "true"
        pipelines: [logs/notify_default]
```

### notificationexporter で Slack に通知する

Slack への通知部分には、既存の Exporter では要件を満たせなかったため `notificationexporter` を自作しました。ログレコードをユーザー定義の Go template で描画し、任意の HTTP エンドポイントに POST するだけのシンプルな Exporter です。

```go:config.go
type Config struct {
	Method           string           `mapstructure:"method"`
	Type             string           `mapstructure:"type"` // webhook / slack_app / discord_webhook / discord_bot
	SlackApp         SlackAppConfig   `mapstructure:"slack_app"`
	DiscordBot       DiscordBotConfig `mapstructure:"discord_bot"`
	BodyTemplate     string           `mapstructure:"body_template"`
	BodyTemplateFile string           `mapstructure:"body_template_file"`
	Condition        string           `mapstructure:"condition"` // OTTL の log_record 条件
}
```

`BodyTemplate` は文字列を直接埋め込む場合のフィールドですが、今回のようにファイルとして管理したい場合は `BodyTemplateFile`（`body_template_file`）を利用します。

`type` に `webhook` を指定すると Slack Incoming Webhook 宛にそのまま送信でき、`slack_app` を指定すると Slack の Web API（`chat.postMessage`）を Bot Token 経由で呼び出すこともできます。今回は Incoming Webhook を利用しており、設定は以下のようになっています。

```yaml
exporters:
  notification/default:
    type: webhook
    endpoint: "${env:SLACK_WEBHOOK_DEFAULT}"
    body_template_file: /etc/otelcol/templates/slack-default.tmpl
    timeout: 5s
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 60s
      max_elapsed_time: 5m
    sending_queue:
      enabled: true
      num_consumers: 1
      queue_size: 256
```

`body_template_file` で指定したテンプレートでは Slack の Block Kit を使ったメッセージを組み立てており、Severity に応じて色を変える、Namespace や対象リソース名をフィールドとして表示するなど、kubernetes-event-exporter のときよりも見やすい通知に変更しています。テンプレートに Kubernetes イベントの任意の文字列（`Body` や `Reason` など）をそのまま埋め込むと JSON が壊れる可能性があるため、`toJson` という template 関数でエスケープするようにしています。

```
{{ printf "[%s] %s" .SeverityText .Body | toJson }}
```

## OCB でカスタムディストリビューションをビルドする

`k8seventsreceiver` と `notificationexporter` を組み合わせた Collector のバイナリは、[OpenTelemetry Collector Builder（OCB）](https://github.com/open-telemetry/opentelemetry-collector/tree/main/cmd/builder) を使ってビルドしています。

```yaml:builder-config.yaml
dist:
  name: otelcol-k8s-events
  version: 0.1.0
receivers:
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8seventsreceiver v0.160.0
processors:
  - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.160.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.160.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor v0.160.0
connectors:
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/routingconnector v0.160.0
exporters:
  - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.160.0
  - gomod: github.com/ucpr/opentelemetry-collector-components/exporters/notificationexporter v0.0.0-...
```

コンポーネントのバージョンは、クラスタ内で既に稼働している別用途の OpenTelemetry Collector（`otel/opentelemetry-collector-contrib:0.160.0`）に合わせて `v0.160.0` に固定しています。ビルドした Collector は `FROM scratch` の Docker イメージとして non-root ユーザーで実行するようにし、GitHub Actions で `ghcr.io/ucpr/otelcol-k8s-events` に push しています。

## 移行してみて

kubernetes-event-exporter を削除する前に、新旧の通知基盤を並行稼働させて動作を確認してから切り替えを行いました。

![k8s_events_to_slack_with_otel-01](https://ucprdev-image-proxy.ucpr.workers.dev/images/articles/k8s_events_to_slack_with_otel/k8s_events_to_slack_with_otel-01.png)

旧環境では Slack Bot Token による通知のみに対応していましたが、`notificationexporter` は Slack Incoming Webhook・Slack App・Discord Webhook・Discord Bot に対応した汎用的な Exporter として作ったため、今後 Kubernetes イベント以外の通知にも同じコンポーネントを流用できそうです。

## おわりに

本記事では、kubernetes-event-exporter から OpenTelemetry Collector と自作の notificationexporter を使った Kubernetes イベントの Slack 通知基盤への移行について紹介しました。OpenTelemetry Collector の Receiver / Processor / Connector を組み合わせることで、kubernetes-event-exporter が提供していた機能を再現しつつ、Slack 以外の通知先にも対応できる拡張性のある構成にできたと感じています。

本記事において、異なっている説明や表現がありましたらご連絡ください。

## 参考

- [mustafaakin/kubernetes-event-exporter](https://github.com/mustafaakin/kubernetes-event-exporter)
- [Kubernetes Eventを保持したりSlack通知させたりする](https://christina04.hatenablog.com/entry/kubernetes-event-exporter)
- [open-telemetry/opentelemetry-collector-contrib - k8seventsreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/k8seventsreceiver)
- [open-telemetry/opentelemetry-collector-contrib - routingconnector](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/routingconnector)
- [open-telemetry/opentelemetry-operator](https://github.com/open-telemetry/opentelemetry-operator)
- [open-telemetry/opentelemetry-collector - builder](https://github.com/open-telemetry/opentelemetry-collector/tree/main/cmd/builder)
- [ucpr/opentelemetry-collector-components](https://github.com/ucpr/opentelemetry-collector-components)
