DEV Community

Cover image for Installing Elasticsearch, Logstash, and Kibana (ELK Stack) on Kubernetes
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya Originally published at docs.vultr.com

Installing Elasticsearch, Logstash, and Kibana (ELK Stack) on Kubernetes

The Elastic (ELK) Stack — Elasticsearch, Logstash, Kibana, and Beats — collects, processes, stores, and visualizes logs in real time. This guide deploys it on Kubernetes via the Elastic Cloud on Kubernetes (ECK) operator: Elasticsearch for storage/search, Logstash for pipeline processing, Filebeat as a per-node log shipper, and Kibana for visualization, all behind Traefik with Let's Encrypt TLS.

Prerequisites: a Kubernetes cluster (3+ nodes, 4GB RAM each minimum), kubectl/helm configured, a domain for Kibana (e.g. kibana.example.com).


Install Elasticsearch

ECK handles Elasticsearch cluster config, scaling, and upgrades automatically.

1. Install the operator — check the ECK releases page for the current version:

$ kubectl create -f https://download.elastic.co/downloads/eck/3.4.0/crds.yaml
$ kubectl apply -f https://download.elastic.co/downloads/eck/3.4.0/operator.yaml
$ kubectl get -n elastic-system pods
Enter fullscreen mode Exit fullscreen mode

Confirm elastic-operator-0 is Running.

2. Define the Elasticsearch cluster:

$ nano elasticsearch.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: quickstart
spec:
  version: 9.4.0
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
    volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 40Gi
        storageClassName: standard
Enter fullscreen mode Exit fullscreen mode

Replace standard with an actual StorageClass from your cluster — run kubectl get storageclass to see what's available. node.store.allow_mmap: false avoids memory-map permission issues in containers.

$ kubectl apply -f elasticsearch.yaml
Enter fullscreen mode Exit fullscreen mode

Wait ~2 minutes, then verify:

$ kubectl get pods -l elasticsearch.k8s.elastic.co/cluster-name=quickstart
$ kubectl get service quickstart-es-http
$ kubectl get pvc
$ kubectl get elasticsearch
Enter fullscreen mode Exit fullscreen mode

Single-node setups show yellow health (replicas can't be placed) — that's expected. red means check pods/service/PVC.

3. Grab the generated elastic user password — you'll need it for Elasticsearch and Kibana:

$ PASSWORD=$(kubectl get secret quickstart-es-elastic-user -o go-template='{{.data.elastic | base64decode}}')
$ echo $PASSWORD
Enter fullscreen mode Exit fullscreen mode

Install Logstash

Logstash ingests from Filebeat, filters/enriches, and forwards to Elasticsearch.

$ nano logstash.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: logstash.k8s.elastic.co/v1alpha1
kind: Logstash
metadata:
  name: quickstart
spec:
  version: 9.4.0
  count: 1
  elasticsearchRefs:
  - name: quickstart
    clusterName: quickstart
  pipelines:
  - pipeline.id: main
    config.string: |
      input {
        beats {
          port => 5044
        }
      }
      filter {
        if [kubernetes][namespace] {
          mutate {
            add_field => { "environment" => "%{[kubernetes][namespace]}" }
          }
        }
      }
      output {
        elasticsearch {
          hosts => [ "${QUICKSTART_ES_HOSTS}" ]
          user => "${QUICKSTART_ES_USER}"
          password => "${QUICKSTART_ES_PASSWORD}"
          ssl_certificate_authorities => "${QUICKSTART_ES_SSL_CERTIFICATE_AUTHORITY}"
          index => "logstash-%{+YYYY.MM.dd}"
        }
      }
  services:
  - name: beats
    service:
      spec:
        type: ClusterIP
        ports:
        - port: 5044
          name: beats
          protocol: TCP
          targetPort: 5044
  volumeClaimTemplates:
  - metadata:
      name: logstash-data
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 40Gi
      storageClassName: standard
Enter fullscreen mode Exit fullscreen mode

elasticsearchRefs auto-injects connection credentials as env vars. The pipeline listens for Filebeat on port 5044, tags each log with its Kubernetes namespace, and writes to a daily logstash-YYYY.MM.dd index.

$ kubectl apply -f logstash.yaml
$ kubectl get pods -l logstash.k8s.elastic.co/name=quickstart
$ kubectl get logstash
Enter fullscreen mode Exit fullscreen mode

Expect green health, 1/1 available.


Install Filebeat

Runs as a DaemonSet, tailing /var/log/containers/ on every node and enriching with Kubernetes metadata.

$ nano filebeat.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: beat.k8s.elastic.co/v1beta1
kind: Beat
metadata:
  name: quickstart
spec:
  type: filebeat
  version: 9.4.0
  config:
    filebeat.inputs:
    - type: filestream
      id: kubernetes-container-logs
      paths:
      - /var/log/containers/*.log
      parsers:
      - container: {}
      prospector.scanner.symlinks: true
      processors:
      - add_kubernetes_metadata:
          host: ${NODE_NAME}
          matchers:
          - logs_path:
              logs_path: /var/log/containers/
      - drop_event.when:
          or:
          - equals:
              kubernetes.namespace: "kube-system"
          - equals:
              kubernetes.namespace: "kube-public"
          - equals:
              kubernetes.namespace: "elastic-system"
          - equals:
              kubernetes.namespace: "kube-node-lease"
    output.logstash:
      hosts: ["quickstart-ls-beats:5044"]
  daemonSet:
    podTemplate:
      spec:
        serviceAccountName: filebeat
        automountServiceAccountToken: true
        terminationGracePeriodSeconds: 30
        dnsPolicy: ClusterFirstWithHostNet
        hostNetwork: true
        containers:
        - name: filebeat
          env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          securityContext:
            runAsUser: 0
          volumeMounts:
          - name: varlogcontainers
            mountPath: /var/log/containers
          - name: varlogpods
            mountPath: /var/log/pods
          - name: varlibdockercontainers
            mountPath: /var/lib/docker/containers
        volumes:
        - name: varlogcontainers
          hostPath:
            path: /var/log/containers
        - name: varlogpods
          hostPath:
            path: /var/log/pods
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
Enter fullscreen mode Exit fullscreen mode

drop_event.when filters out system namespace noise. hostNetwork: true + runAsUser: 0 are needed for node-level log access.

RBAC:

$ nano filebeat-rbac.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: ServiceAccount
metadata:
  name: filebeat
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: filebeat
rules:
- apiGroups: [""]
  resources: [namespaces, pods, nodes]
  verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: filebeat
subjects:
- kind: ServiceAccount
  name: filebeat
  namespace: default
roleRef:
  kind: ClusterRole
  name: filebeat
  apiGroup: rbac.authorization.k8s.io
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f filebeat-rbac.yaml
$ kubectl apply -f filebeat.yaml
$ kubectl get pods -A -l common.k8s.elastic.co/type=beat
$ kubectl get beat
Enter fullscreen mode Exit fullscreen mode

One Filebeat pod per node, green health.


Install Kibana

$ nano kibana.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: quickstart
  namespace: default
spec:
  version: 9.4.0
  count: 1
  elasticsearchRef:
    name: quickstart
  http:
    tls:
      selfSignedCertificate:
        disabled: true
Enter fullscreen mode Exit fullscreen mode

TLS is disabled here because Traefik handles termination downstream.

$ kubectl apply -f kibana.yaml
$ kubectl get pods --selector='kibana.k8s.elastic.co/name=quickstart'
$ kubectl get service quickstart-kb-http
$ kubectl get kibana
Enter fullscreen mode Exit fullscreen mode

Secure with Traefik + cert-manager

1. Install Traefik as Ingress:

$ helm repo add traefik https://traefik.github.io/charts
$ helm repo update
$ helm install traefik traefik/traefik \
  --namespace traefik \
  --create-namespace \
  --set "ports.web.exposedPort=80" \
  --set "ports.websecure.exposedPort=443" \
  --set "ports.web.http.redirections.entryPoint.to=websecure" \
  --set "ports.web.http.redirections.entryPoint.scheme=https" \
  --set persistence.enabled=false
$ kubectl get all -n traefik
Enter fullscreen mode Exit fullscreen mode

Wait ~3 minutes for the LoadBalancer, then get its IP and point your DNS provider's A record at it:

$ kubectl get service traefik --namespace traefik
Enter fullscreen mode Exit fullscreen mode

2. Install cert-manager — check the releases page for the current version:

$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml
$ kubectl get pods -n cert-manager
Enter fullscreen mode Exit fullscreen mode

3. Create a ClusterIssuer:

$ nano issuer.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    email: admin@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f issuer.yaml
Enter fullscreen mode Exit fullscreen mode

4. Create the Ingress. Replace kibana.example.com:

$ nano kibana-ingress.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kibana
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - kibana.example.com
      secretName: kibana-tls
  rules:
    - host: kibana.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: quickstart-kb-http
                port:
                  number: 5601
Enter fullscreen mode Exit fullscreen mode
$ kubectl apply -f kibana-ingress.yaml
$ kubectl get ingress -n default
$ kubectl get certificate -n default
Enter fullscreen mode Exit fullscreen mode

kibana-tls should show READY: True — give cert-manager a few minutes if not.

5. Confirm TLS works:

$ curl -I https://kibana.example.com
Enter fullscreen mode Exit fullscreen mode

Access Kibana

$ kubectl get secret quickstart-es-elastic-user -o go-template='{{.data.elastic | base64decode}}' && echo
Enter fullscreen mode Exit fullscreen mode

Visit https://kibana.example.com, log in as elastic with that password, Explore on my own.

Explore logs:

  1. Main menu → Analytics → Discover.
  2. Select (or create) the logstash-* data view — index pattern logstash-*, time field @timestamp.
  3. Filter with queries like kubernetes.namespace: default, kubernetes.pod.name: demo-app*, or message: error.
  4. Adjust the time range picker, click any log entry to expand full metadata.

Troubleshooting

Elasticsearch pod stuck Pendingkubectl describe pod quickstart-es-default-0. Usually insufficient node resources, or the PVC's StorageClass doesn't exist (kubectl get storageclass to check).

Elasticsearch yellow/redkubectl get elasticsearch. Yellow is normal for single-node (no room for replicas); scale nodeSets.count to 3+ for green in production. Red means check kubectl logs quickstart-es-default-0 for OOM or storage failures.

Filebeat not collecting — verify RBAC:

$ kubectl get serviceaccount filebeat
$ kubectl get clusterrole filebeat
$ kubectl get clusterrolebinding filebeat
Enter fullscreen mode Exit fullscreen mode

Reapply filebeat-rbac.yaml if anything's missing; check kubectl logs -l common.k8s.elastic.co/type=beat.

Certificate not issuedkubectl describe certificate kibana-tls -n default, kubectl describe certificaterequest -n default, kubectl logs -n cert-manager -l app=cert-manager. Usually DNS not propagated yet (dig kibana.example.com) or port 80 not reachable for the HTTP-01 challenge.

"Kibana server is not ready yet" — confirm kubectl get elasticsearch shows green; if it does but Kibana still fails, check kubectl logs -l kibana.k8s.elastic.co/name=quickstart.

LoadBalancer stuck <pending> — needs cloud-provider LoadBalancer integration; check kubectl describe service traefik -n traefik if it doesn't resolve within 5 minutes.


Next Steps

Logs flow from every node through Filebeat → Logstash → Elasticsearch, visualized in Kibana over TLS. From here:

  • Scale nodeSets.count to 3+ for a green, replicated Elasticsearch cluster
  • Add more Logstash filters (grok, geoip) for richer log parsing
  • Set up Kibana alerting rules on error-rate thresholds

For the full guide, visit the original article on Vultr Docs.

Top comments (0)