epok

Send Kubernetes logs to Epok

Vector as a DaemonSet on every node. One pod per worker reads /var/log/containers/*.logvia the host mount, parses the CRI log format, and ships to Epok. Once logs arrive, Epok's 70+ Kubernetes detection rules fire automatically: CrashLoopBackOff, OOMKilled, ImagePullBackOff, FailedScheduling, probe failures, eviction patterns.

Time to first log: 5 min · Works on: EKS, GKE, AKS, k3s, kind, self-hosted · API key: app.getepok.dev → Settings → API Keys

1. Store the API key as a secret

bash
kubectl create namespace epok
kubectl -n epok create secret generic epok-credentials \
  --from-literal=api-key=epk_REPLACE_ME

2. Vector config (ConfigMap)

yaml
# epok-vector-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: epok-vector-config
  namespace: epok
data:
  vector.yaml: |
    sources:
      kube_logs:
        type: kubernetes_logs
        # Vector auto-discovers pods, attaches namespace/pod/container
        # labels, and parses the CRI/JSON log format. No extra config
        # needed for the common case.
        glob_minimum_cooldown_ms: 200

    transforms:
      enrich:
        type: remap
        inputs: [kube_logs]
        source: |
          # Map the kubernetes_logs fields to Epok's expected shape.
          .service = .kubernetes.container_name
          .namespace = .kubernetes.pod_namespace
          .pod = .kubernetes.pod_name
          .node = .kubernetes.pod_node_name
          ._msg = .message
          del(.message)

    sinks:
      epok:
        type: elasticsearch
        inputs: [enrich]
        endpoint: https://ingest.getepok.dev
        bulk:
          index: logs
        auth:
          strategy: basic
          user: ${EPOK_API_KEY}
          password: x
        # Sensible defaults for k8s log volume.
        batch:
          max_events: 1000
          timeout_secs: 5
        buffer:
          type: memory
          max_events: 5000
          when_full: drop_newest

3. RBAC for Vector

Vector needs read access to pods + namespaces to enrich log entries with metadata.

yaml
# epok-vector-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: epok-vector
  namespace: epok
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: epok-vector
rules:
- apiGroups: [""]
  resources: ["pods", "namespaces", "nodes"]
  verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: epok-vector
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: epok-vector
subjects:
- kind: ServiceAccount
  name: epok-vector
  namespace: epok

4. DaemonSet

yaml
# epok-vector-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: epok-vector
  namespace: epok
spec:
  selector:
    matchLabels: { app: epok-vector }
  template:
    metadata:
      labels: { app: epok-vector }
    spec:
      serviceAccountName: epok-vector
      tolerations:
      - operator: Exists                  # Run on every node, incl. tainted ones
      containers:
      - name: vector
        # Pin to a specific version, not :latest, so a Vector release
        # never breaks your log pipeline. Bump deliberately after testing.
        image: timberio/vector:0.43.0-alpine
        env:
        - name: EPOK_API_KEY
          valueFrom:
            secretKeyRef:
              name: epok-credentials
              key: api-key
        resources:
          requests: { cpu: 50m,  memory: 128Mi }
          limits:   { cpu: 500m, memory: 512Mi }
        volumeMounts:
        - { name: config,   mountPath: /etc/vector }
        - { name: var-log,  mountPath: /var/log,           readOnly: true }
        - { name: var-lib,  mountPath: /var/lib/docker,    readOnly: true }
        - { name: data,     mountPath: /vector-data }
      volumes:
      - { name: config,   configMap: { name: epok-vector-config } }
      - { name: var-log,  hostPath:  { path: /var/log } }
      - { name: var-lib,  hostPath:  { path: /var/lib/docker } }
      - { name: data,     emptyDir: {} }

5. Apply everything

bash
kubectl apply -f epok-vector-config.yaml
kubectl apply -f epok-vector-rbac.yaml
kubectl apply -f epok-vector-daemonset.yaml

# Watch the pods come up
kubectl -n epok rollout status daemonset/epok-vector

# Tail one of the agents to confirm
kubectl -n epok logs -l app=epok-vector --tail=20 -f

Verify

  1. Open app.getepok.dev Live Tail. Container logs should stream in within 30–60 s, tagged with namespace, pod, container.
  2. Trigger a failure: kubectl delete pod <some-pod> and force a CrashLoopBackOff. Within ~2 minutes the K8s Intelligence detector fires an alert in New Errors.
  3. Open Services. Each unique container_name appears as a service card with hit counts.

Common gotchas

  • Containerd vs Docker. Modern clusters use containerd — logs live under /var/log/containers/ (already mounted above). The /var/lib/docker mount is a no-op on pure-containerd nodes and harmless to leave in.
  • PodSecurity / Pod Security Admission. DaemonSets that mount host paths need a privileged or baseline policy. If your cluster enforces restricted, create the epok namespace with the pod-security.kubernetes.io/enforce=privileged label.
  • EKS + Fargate.Fargate nodes can't run DaemonSets. Use Fluent Bit's built-in EKS Fargate logging instead — config snippet in the AWS guide.
  • Resource limits. The 500 m CPU / 512 Mi memory cap is right for ~5 k logs/sec/node. Bump if you see Vector OOMing under heavy log bursts.

Want pod-level dashboards out of the box? Once logs are flowing, Epok auto-builds Service Health views per container. No dashboards to configure.