ELK Stack — Project: ELK on Kubernetes with ECK
Project Overview
The Elastic Cloud on Kubernetes (ECK) operator is the official way to deploy and manage Elasticsearch, Kibana, and Beats on Kubernetes. In this project you will:
- Install the ECK operator
- Deploy a production-style Elasticsearch cluster
- Deploy Kibana connected to the cluster
- Deploy Filebeat as a DaemonSet to collect pod logs
- Deploy a sample application and route its logs to a dedicated index
- Build Kibana dashboards for Kubernetes observability
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │┌────────┐│ │┌────────┐│ │┌────────┐│ │
│ ││Filebeat││ ││Filebeat││ ││Filebeat││ (DaemonSet) │
│ │└────┬───┘│ │└────┬───┘│ │└────┬───┘│ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴──┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │ Pods │ │ │ │ Pods │ │ │ │ Pods │ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Elasticsearch Cluster │ │
│ │ (3 nodes managed by ECK) │ │
│ └─────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Kibana │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ECK Operator │ │
│ │ (manages all Elastic resources) │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Prerequisites
- A running Kubernetes cluster (Minikube, kind, EKS, GKE, or AKS)
kubectlconfigured to access the cluster- At least 8 GB of memory available across nodes
helminstalled (optional, for ECK operator)
# Verify cluster access
kubectl cluster-info
kubectl get nodes
Step 1 — Install the ECK Operator
# Install ECK Custom Resource Definitions
kubectl create -f https://download.elastic.co/downloads/eck/2.12.1/crds.yaml
# Install the ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.12.1/operator.yaml
# Verify the operator is running
kubectl -n elastic-system get pods
Wait until the operator pod shows Running:
kubectl -n elastic-system wait --for=condition=Ready pod -l control-plane=elastic-operator --timeout=120s
Step 2 — Create a Namespace
kubectl create namespace elk-project
Step 3 — Deploy Elasticsearch
Create elasticsearch.yaml:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elk-project
namespace: elk-project
spec:
version: 8.13.0
nodeSets:
- name: default
count: 3
config:
node.store.allow_mmap: false
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 1Gi
cpu: 500m
limits:
memory: 2Gi
cpu: "1"
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Apply it:
kubectl apply -f elasticsearch.yaml
# Watch the deployment progress
kubectl -n elk-project get elasticsearch -w
Wait for the cluster health to become green:
kubectl -n elk-project wait --for=jsonpath='{.status.health}'=green \
elasticsearch/elk-project --timeout=300s
Retrieve the Elasticsearch password
ES_PASSWORD=$(kubectl -n elk-project get secret elk-project-es-elastic-user \
-o jsonpath='{.data.elastic}' | base64 -d)
echo "Elasticsearch password: $ES_PASSWORD"
Step 4 — Deploy Kibana
Create kibana.yaml:
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
name: elk-project
namespace: elk-project
spec:
version: 8.13.0
count: 1
elasticsearchRef:
name: elk-project
podTemplate:
spec:
containers:
- name: kibana
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 500m
Apply it:
kubectl apply -f kibana.yaml
# Wait for Kibana to be ready
kubectl -n elk-project wait --for=jsonpath='{.status.health}'=green \
kibana/elk-project --timeout=300s
Access Kibana
# Port-forward to access Kibana locally
kubectl -n elk-project port-forward svc/elk-project-kb-http 5601:5601 &
# Open https://localhost:5601 (accept the self-signed cert)
# Login: elastic / <password from Step 3>
Step 5 — Deploy Filebeat DaemonSet
Create filebeat.yaml:
apiVersion: beat.k8s.elastic.co/v1beta1
kind: Beat
metadata:
name: elk-project
namespace: elk-project
spec:
type: filebeat
version: 8.13.0
elasticsearchRef:
name: elk-project
config:
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
# Route logs based on namespace
filebeat.autodiscover:
providers:
- type: kubernetes
node: ${NODE_NAME}
hints.enabled: true
hints.default_config:
type: container
paths:
- /var/log/containers/*${data.kubernetes.container.id}.log
output.elasticsearch:
hosts: ["https://elk-project-es-http:9200"]
ssl.certificate_authorities:
- /mnt/elastic/es-certs/ca.crt
username: elastic
password: "${ES_PASSWORD}"
# Index routing by namespace
setup.template.name: "k8s-logs"
setup.template.pattern: "k8s-logs-*"
daemonSet:
podTemplate:
spec:
serviceAccountName: filebeat
automountServiceAccountToken: true
dnsPolicy: ClusterFirstWithHostNet
hostNetwork: true
containers:
- name: filebeat
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: ES_PASSWORD
valueFrom:
secretKeyRef:
name: elk-project-es-elastic-user
key: elastic
volumeMounts:
- name: varlogcontainers
mountPath: /var/log/containers
- name: varlogpods
mountPath: /var/log/pods
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
- name: es-certs
mountPath: /mnt/elastic/es-certs
readOnly: true
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 256Mi
cpu: 200m
volumes:
- name: varlogcontainers
hostPath:
path: /var/log/containers
- name: varlogpods
hostPath:
path: /var/log/pods
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: es-certs
secret:
secretName: elk-project-es-http-certs-public
optional: false
Create the RBAC resources and apply:
# Create ServiceAccount and ClusterRole for Filebeat
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: filebeat
namespace: elk-project
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: filebeat
rules:
- apiGroups: [""]
resources: ["pods", "namespaces", "nodes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: filebeat
subjects:
- kind: ServiceAccount
name: filebeat
namespace: elk-project
roleRef:
kind: ClusterRole
name: filebeat
apiGroup: rbac.authorization.k8s.io
EOF
# Deploy Filebeat
kubectl apply -f filebeat.yaml
# Verify Filebeat pods are running on every node
kubectl -n elk-project get pods -l beat.k8s.elastic.co/name=elk-project
Step 6 — Deploy a Sample Application
Create sample-app.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: log-generator
namespace: elk-project
labels:
app: log-generator
spec:
replicas: 2
selector:
matchLabels:
app: log-generator
template:
metadata:
labels:
app: log-generator
spec:
containers:
- name: log-generator
image: busybox:1.36
command:
- /bin/sh
- -c
- |
while true; do
LEVEL=$(shuf -n1 -e INFO INFO INFO INFO WARN ERROR)
MSG=$(shuf -n1 -e "User login" "Page viewed" "Order created" "Payment processed" "DB query slow" "Cache miss" "Connection timeout")
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"level\":\"$LEVEL\",\"app\":\"log-generator\",\"message\":\"$MSG\",\"pod\":\"$HOSTNAME\"}"
sleep $((RANDOM % 5 + 1))
done
resources:
requests:
memory: 32Mi
cpu: 10m
limits:
memory: 64Mi
cpu: 50m
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: elk-project
labels:
app: web-app
spec:
replicas: 1
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 128Mi
cpu: 100m
Apply it:
kubectl apply -f sample-app.yaml
# Verify pods are running
kubectl -n elk-project get pods -l app=log-generator
kubectl -n elk-project get pods -l app=web-app
Step 7 — Verify Data in Elasticsearch
# Port-forward to Elasticsearch
kubectl -n elk-project port-forward svc/elk-project-es-http 9200:9200 &
# Check indices (use the password from Step 3)
curl -sk -u "elastic:$ES_PASSWORD" https://localhost:9200/_cat/indices?v | grep -E "k8s|filebeat"
Query logs from the sample app
curl -sk -u "elastic:$ES_PASSWORD" "https://localhost:9200/filebeat-*/_search?pretty" \
-H 'Content-Type: application/json' -d'
{
"size": 5,
"query": {
"match": {
"kubernetes.labels.app": "log-generator"
}
},
"sort": [{ "@timestamp": "desc" }]
}'
Step 8 — Kibana Dashboards
Access Kibana at https://localhost:5601 and create:
Data View
- Pattern:
filebeat-* - Time field:
@timestamp
Dashboard Panels
| Panel | Type | Description |
|---|---|---|
| Pod Log Volume | Stacked bar | Breakdown by kubernetes.pod.name |
| Namespace Activity | Pie chart | Terms on kubernetes.namespace |
| Log Level Distribution | Donut chart | Terms on parsed level field |
| Error Logs Table | Data table | Filter level: ERROR or log.level: error |
| Node Log Distribution | Horizontal bar | Terms on kubernetes.node.name |
| Container Restarts | Metric | Based on container restart events |
Step 9 — Namespace-Based Index Routing (Optional)
To route logs from different namespaces to different indices, update the Filebeat config:
# Add to the filebeat config section in filebeat.yaml
output.elasticsearch:
hosts: ["https://elk-project-es-http:9200"]
index: "k8s-%{[kubernetes.namespace]}-%{+yyyy.MM.dd}"
setup.template.name: "k8s"
setup.template.pattern: "k8s-*"
setup.ilm.enabled: false
This creates separate indices like k8s-elk-project-2026.04.01 and k8s-default-2026.04.01.
Scaling Considerations
Add dedicated node roles
nodeSets:
- name: master
count: 3
config:
node.roles: ["master"]
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
limits:
memory: 2Gi
- name: data
count: 3
config:
node.roles: ["data", "ingest"]
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
limits:
memory: 4Gi
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
Cleanup
kubectl delete namespace elk-project
kubectl delete clusterrole filebeat
kubectl delete clusterrolebinding filebeat
What You Learned
- How to install and use the ECK operator
- How to deploy Elasticsearch and Kibana on Kubernetes
- How to deploy Filebeat as a DaemonSet for cluster-wide log collection
- How to use Kubernetes metadata for log enrichment
- How to route logs by namespace