Перейти до змісту

Event System: audit trail для кластера

Event System: audit trail для кластера

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Коли щось відбувається в кластері, потрібно знати що, коли і чому. Events — це журнал подій, який записує все: створення подів, планування, відмови нод.

Структура події

type Event struct {
    Type      string    `json:"type"`      // Normal, Warning
    Reason    string    `json:"reason"`    // Created, Scheduled, NodeNotReady
    Message   string    `json:"message"`   // людське пояснення
    Object    string    `json:"object"`    // pod/web-0, node/node-1
    Timestamp time.Time `json:"timestamp"`
}

Два типи: Normal (все добре) і Warning (проблема). Object вказує, до якого ресурсу відноситься подія.

Хто записує події

Різні компоненти записують свої події:

// API Server - створення поду
api.store.RecordEvent(Event{
    Type: "Normal", Reason: "Created",
    Message: "Pod web-0 created",
    Object: "pod/web-0",
})

// Scheduler - планування
s.store.RecordEvent(Event{
    Type: "Normal", Reason: "Scheduled",
    Message: "Pod web-0 scheduled to node-1",
    Object: "pod/web-0",
})

// NodeController - відмова ноди
nc.store.RecordEvent(Event{
    Type: "Warning", Reason: "NodeNotReady",
    Message: "Node node-1 heartbeat timeout",
    Object: "node/node-1",
})

// ReplicationController - масштабування
rc.store.RecordEvent(Event{
    Type: "Normal", Reason: "Created",
    Message: "Created pod web-2 for deployment web",
    Object: "deployment/web",
})

Зберігання

func (s *Store) RecordEvent(evt Event) error {
    key := fmt.Sprintf("%d-%s",
        evt.Timestamp.UnixNano(), evt.Object)
    return s.put(bucketEvents, []byte(key), evt)
}

Ключ — timestamp у наносекундах плюс об'єкт. BoltDB зберігає ключі відсортованими, тому останні події мають найбільші ключі.

Читання (від найновіших)

func (s *Store) ListEvents(limit int) ([]Event, error) {
    var events []Event
    s.db.View(func(tx *bolt.Tx) error {
        c := tx.Bucket(bucketEvents).Cursor()
        count := 0
        for k, v := c.Last(); k != nil && count < limit;
            k, v = c.Prev() {
            var evt Event
            json.Unmarshal(v, &evt)
            events = append(events, evt)
            count++
        }
        return nil
    })
    return events, nil
}

Cursor.Last() + Prev() — ітерація від кінця. Отримуємо найновіші події першими.

Перегляд через sheepctl

$ sheepctl events
TYPE     REASON        OBJECT           MESSAGE
Normal   Created       pod/web-0        Pod web-0 created
Normal   Scheduled     pod/web-0        Pod web-0 scheduled to node-1
Normal   Created       pod/web-1        Created pod web-1 for deployment web
Warning  NodeNotReady  node/node-2      Node node-2 heartbeat timeout

Це як kubectl get events, тільки простіше.

💡 Цікаві факти

  • Events у Kubernetes — це повноцінний об'єкт API з власним TTL: за замовчуванням вони живуть лише близько години, а потім etcd їх видаляє. Тому kubectl get events після інциденту часто вже порожній — events не призначені бути довготривалим audit trail.
  • Щоб однакові події не засипали etcd, Kubernetes їх агрегує: повторювані events склеюються в один із лічильником count і полем lastTimestamp. Наша перевірка if condition != NodeNotReady з минулих частин — це примітивна, ручна версія тієї самої ідеї дедуплікації.
  • Реальний справжній audit trail у Kubernetes — це окремий механізм, Audit Logging на рівні API Server, а не Events. Це поширене непорозуміння: Events — для людей і дебагу "що зараз коїться", audit log — для безпеки і відповідності.
  • Трюк із сортуванням за ключем — не наш винахід. BoltDB (як і LevelDB, RocksDB, etcd) тримає ключі у відсортованому B+tree, тому префікс-timestamp дає безкоштовний хронологічний порядок. На цьому ж принципі будуються time-series бази.
  • У Kubernetes кожен Event має поле involvedObject — посилання на pod, node чи deployment. Саме тому kubectl describe pod web-0 автоматично показує timeline подій внизу: UX побудований навколо об'єкта, а не процесу, що залогував.
  • Kubernetes мігрує Events з core/v1 на events.k8s.io/v1. Нова API-група додає структуровані поля й кращу агрегацію — навіть "прості" observability-об'єкти еволюціонують під production-навантаженням.
  • kube-apiserver запускає фоновий garbage collector з --event-ttl (дефолт 1 година). У нашому Shepherd store аналога немає — events накопичуються, доки сам не додас ротацію.
  • Kubernetes Events — це не Event Sourcing у сенсі DDD. Це сповіщення про зміни стану, а не source of truth. Spec Deployment у etcd — це правда; Event лише каже "щось із ним сталося".

Що я зрозумів, поки розбирався з темою

Найнесподіваніше для мене — що events це не логи. Я звик кидати все у stdout, але лог прив'язаний до процесу, який його написав: впав компонент — зник контекст. Event же прив'язаний до об'єкта (pod/web-0), а не до процесу. Тому можна спитати "що сталося саме з цим подом" і зібрати картину від різних компонентів — API, Scheduler, Agent — в одному місці. Це інший спосіб мислення: не "хто що залогував", а "що сталося з ресурсом".

І ще: timestamp у наносекундах як частина ключа спочатку виглядав як хак. Але саме він дав і унікальність, і сортування одним рядком. Іноді найпростіше рішення і є правильним.

На що звернути увагу

  • Ключ — це timestamp + object. Якщо два events на той самий об'єкт впадуть в одну наносекунду (малоймовірно, але можливо), один затре інший. Реальні системи додають у ключ ще й випадковий суфікс або UID.
  • Немає TTL: у нас events ростуть у BoltDB вічно. На довгому проді файл роздується, а Last()+Prev() доведеться гортати все більше сміття.
  • Запис event — best-effort і не в одній транзакції зі зміною стану. Можна оновити статус ноди, але впасти до RecordEvent — і подія загубиться. Event не є гарантією, що зміна сталася.

Що можна покращити

  • Додати TTL/ротацію: видаляти events старші за N годин або тримати фіксовану кількість останніх (ring buffer).
  • Зробити агрегацію повторюваних events із лічильником count і lastTimestamp замість запису кожного дубля окремо.
  • Додати фільтрацію на рівні API (?object=pod/web-0, ?type=Warning) замість grep на клієнті.
  • Стрімити events через watch (SSE або long-poll), щоб sheepctl events -w показував їх у реальному часі, а не лише знімок.

Спробуй сам

sheepctl events
# Фільтруй по типу:
sheepctl events | grep Warning
# Через API:
curl -s localhost:9876/api/v1/events | jq '.[0:5]'

Серія Orchestrator завершена. Починаємо Image Registry — свій Docker Registry з нуля.

Ресурси

  • Event API reference — Kubernetes Event v1
  • events.k8s.io API — структуровані Event-об'єкти з агрегацією
  • Audit logging — справжній security audit trail, окремо від Events
  • kubectl: перегляд events — як оператори зазвичай їх читають
  • Event sourcing — класична стаття Мартіна Фаулера (схожа ідея, інша мета)
  • BoltDB cursor — як Last() + Prev() обходить ключі у зворотному порядку

Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow

Попередня: Pod Lifecycle | Наступна: OCI Distribution Spec