ELK Stack — Kubernetes Logging Exercises
Practice Exercises
Five hands-on exercises that cover everything from this chapter. Each builds on real-world Kubernetes logging scenarios with increasing difficulty.
Prerequisites: A running Kubernetes cluster (Minikube, kind, or managed),
kubectlconfigured, and a basic understanding of Elasticsearch and Kibana.
Exercise 1 — Deploy Filebeat DaemonSet
Difficulty: ⭐⭐ Beginner
Scenario
Your team has a Kubernetes cluster with several microservices running across 3 nodes. Currently, developers SSH into nodes to read logs. Your task is to deploy Filebeat as a DaemonSet so that all container logs are shipped to Elasticsearch automatically.
Requirements
- Create a namespace called
elastic-system - Create a Filebeat ConfigMap with the
containerinput type reading from/var/log/containers/*.log - Create a ServiceAccount, ClusterRole, and ClusterRoleBinding for Filebeat
- Deploy Filebeat as a DaemonSet with proper volume mounts
- Configure the
add_kubernetes_metadataprocessor - Verify Filebeat is running on all nodes
- Confirm logs are appearing in Elasticsearch
Show Solution
Step 1 — Create the namespace:
kubectl create namespace elastic-system
Step 2 — Apply the complete manifest:
# filebeat-complete.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: elastic-system
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
output.elasticsearch:
hosts: ["http://elasticsearch.elastic-system:9200"]
index: "k8s-logs-%{+yyyy.MM.dd}"
setup.template.name: "k8s-logs"
setup.template.pattern: "k8s-logs-*"
setup.ilm.enabled: false
logging.level: info
logging.to_stderr: true
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: filebeat
namespace: elastic-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: filebeat
rules:
- apiGroups: [""]
resources: ["namespaces", "pods", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: filebeat
subjects:
- kind: ServiceAccount
name: filebeat
namespace: elastic-system
roleRef:
kind: ClusterRole
name: filebeat
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: filebeat
namespace: elastic-system
spec:
selector:
matchLabels:
app: filebeat
template:
metadata:
labels:
app: filebeat
spec:
serviceAccountName: filebeat
terminationGracePeriodSeconds: 30
containers:
- name: filebeat
image: docker.elastic.co/beats/filebeat:8.13.0
args: ["-c", "/etc/filebeat/filebeat.yml", "-e"]
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
securityContext:
runAsUser: 0
resources:
limits:
memory: 200Mi
cpu: 200m
requests:
memory: 100Mi
cpu: 100m
volumeMounts:
- name: config
mountPath: /etc/filebeat
readOnly: true
- name: varlogcontainers
mountPath: /var/log/containers
readOnly: true
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
- name: data
mountPath: /usr/share/filebeat/data
volumes:
- name: config
configMap:
name: filebeat-config
- name: varlogcontainers
hostPath:
path: /var/log/containers
- name: varlogpods
hostPath:
path: /var/log/pods
- name: data
hostPath:
path: /var/lib/filebeat-data
type: DirectoryOrCreate
kubectl apply -f filebeat-complete.yaml
Step 3 — Verify:
# Check DaemonSet
kubectl get daemonset filebeat -n elastic-system
# Check pods are running on all nodes
kubectl get pods -n elastic-system -l app=filebeat -o wide
# Check Filebeat logs for errors
kubectl logs -l app=filebeat -n elastic-system --tail=20
# Verify indices in Elasticsearch
curl -s http://localhost:9200/_cat/indices/k8s-logs-* | head -5
Exercise 2 — Autodiscover for Multi-App Logging
Difficulty: ⭐⭐⭐ Intermediate
Scenario
Your cluster runs three applications: an Nginx reverse proxy, a Java Spring Boot API, and a Python worker. Each produces logs in a different format. You need to configure Filebeat autodiscover so that each application’s logs are parsed correctly — Nginx access logs are dissected, Java stack traces are grouped into multi-line events, and Python logs are parsed as JSON.
Requirements
- Deploy three sample applications (provided below)
- Configure Filebeat autodiscover with the
kubernetesprovider - Use
templateswith conditions to match each app by its Kubernetes labels - Parse Nginx access logs using the
dissectprocessor - Handle Java multi-line stack traces
- Parse Python JSON logs
- Verify in Kibana that each application’s logs have structured fields
Sample Applications
# sample-apps.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-proxy
namespace: demo
labels:
app: nginx-proxy
spec:
replicas: 1
selector:
matchLabels:
app: nginx-proxy
template:
metadata:
labels:
app: nginx-proxy
log-format: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-api
namespace: demo
labels:
app: java-api
spec:
replicas: 1
selector:
matchLabels:
app: java-api
template:
metadata:
labels:
app: java-api
log-format: java
spec:
containers:
- name: api
image: openjdk:17-slim
command: ["sh", "-c", "while true; do echo 'INFO [main] com.example.App - Request processed'; sleep 5; done"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-worker
namespace: demo
labels:
app: python-worker
spec:
replicas: 1
selector:
matchLabels:
app: python-worker
template:
metadata:
labels:
app: python-worker
log-format: json
spec:
containers:
- name: worker
image: python:3.12-slim
command: ["python3", "-c", "import json,time\nwhile True:\n print(json.dumps({'level':'INFO','service':'worker','message':'Job completed','duration_ms':142})); time.sleep(5)"]
Show Solution
Step 1 — Deploy sample apps:
kubectl create namespace demo
kubectl apply -f sample-apps.yaml
Step 2 — Update Filebeat ConfigMap with autodiscover:
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: elastic-system
data:
filebeat.yml: |-
filebeat.autodiscover:
providers:
- type: kubernetes
node: ${NODE_NAME}
templates:
# Nginx access logs — dissect into structured fields
- condition:
equals:
kubernetes.labels.log-format: "nginx"
config:
- type: container
paths:
- /var/log/containers/*-${data.container.id}.log
processors:
- dissect:
tokenizer: '%{client_ip} - %{user} [%{timestamp}] "%{method} %{path} %{protocol}" %{status} %{bytes} "%{referrer}" "%{user_agent}"'
field: "message"
target_prefix: "nginx"
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
# Java logs — multi-line for stack traces
- condition:
equals:
kubernetes.labels.log-format: "java"
config:
- type: container
paths:
- /var/log/containers/*-${data.container.id}.log
multiline.type: pattern
multiline.pattern: '^\s+(at|\.{3}|Caused by)'
multiline.negate: false
multiline.match: after
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
# Python JSON logs — parse JSON body
- condition:
equals:
kubernetes.labels.log-format: "json"
config:
- type: container
paths:
- /var/log/containers/*-${data.container.id}.log
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
output.elasticsearch:
hosts: ["http://elasticsearch.elastic-system:9200"]
index: "k8s-logs-%{+yyyy.MM.dd}"
setup.template.name: "k8s-logs"
setup.template.pattern: "k8s-logs-*"
setup.ilm.enabled: false
Step 3 — Apply and restart:
kubectl apply -f filebeat-configmap.yaml
# Restart Filebeat pods to pick up new config
kubectl rollout restart daemonset/filebeat -n elastic-system
Step 4 — Verify:
# Check Filebeat logs for autodiscover events
kubectl logs -l app=filebeat -n elastic-system | grep -i "autodiscover"
# Query Elasticsearch for nginx structured fields
curl -s http://localhost:9200/k8s-logs-*/_search -H "Content-Type: application/json" -d '{
"query": { "exists": { "field": "nginx.status" } },
"size": 1
}' | python3 -m json.tool
# Query for Python JSON fields
curl -s http://localhost:9200/k8s-logs-*/_search -H "Content-Type: application/json" -d '{
"query": { "match": { "service": "worker" } },
"size": 1
}' | python3 -m json.tool
Exercise 3 — Deploy ECK Operator + Elasticsearch + Kibana
Difficulty: ⭐⭐⭐ Intermediate
Scenario
Instead of managing Elasticsearch and Kibana with raw StatefulSets and Deployments, you want to use the ECK operator for production-grade management. Deploy a 3-node Elasticsearch cluster and a single Kibana instance using ECK CRDs.
Requirements
- Install ECK CRDs and the operator
- Deploy a 3-node Elasticsearch cluster with 50Gi storage per node
- Deploy Kibana connected to the Elasticsearch cluster
- Retrieve the
elasticuser password - Port-forward Kibana and log in
- Verify cluster health is green
Show Solution
Step 1 — Install ECK:
# Install CRDs
kubectl create -f https://download.elastic.co/downloads/eck/2.12.1/crds.yaml
# Install operator
kubectl apply -f https://download.elastic.co/downloads/eck/2.12.1/operator.yaml
# Verify
kubectl get pods -n elastic-system -l control-plane=elastic-operator
Step 2 — Deploy Elasticsearch:
# elasticsearch.yaml
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elasticsearch
namespace: elastic-system
spec:
version: 8.13.0
nodeSets:
- name: default
count: 3
config:
node.store.allow_mmap: false
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 2Gi
cpu: 1
limits:
memory: 2Gi
cpu: 2
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
kubectl apply -f elasticsearch.yaml
# Wait for green health
kubectl get elasticsearch -n elastic-system -w
Step 3 — Deploy Kibana:
# kibana.yaml
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
name: kibana
namespace: elastic-system
spec:
version: 8.13.0
count: 1
elasticsearchRef:
name: elasticsearch
podTemplate:
spec:
containers:
- name: kibana
resources:
requests:
memory: 1Gi
cpu: 500m
limits:
memory: 1Gi
cpu: 1
kubectl apply -f kibana.yaml
kubectl get kibana -n elastic-system -w
Step 4 — Get password and access:
# Get the elastic password
PASSWORD=$(kubectl get secret elasticsearch-es-elastic-user \
-n elastic-system \
-o go-template='{{.data.elastic | base64decode}}')
echo "Elastic password: $PASSWORD"
# Port-forward Kibana
kubectl port-forward svc/kibana-kb-http -n elastic-system 5601:5601
# Open https://localhost:5601
# Login: elastic / <password from above>
Step 5 — Verify cluster health:
curl -sk https://localhost:9200/_cluster/health \
-u "elastic:$PASSWORD" | python3 -m json.tool
Expected output:
{
"cluster_name": "elasticsearch",
"status": "green",
"number_of_nodes": 3,
"number_of_data_nodes": 3,
"active_primary_shards": 5,
"active_shards": 10
}
Exercise 4 — Fluent Bit as Filebeat Alternative
Difficulty: ⭐⭐⭐⭐ Advanced
Scenario
Your ops team wants to evaluate Fluent Bit as a lighter alternative to Filebeat. Deploy Fluent Bit as a DaemonSet, configure it to enrich logs with Kubernetes metadata, and ship them to Elasticsearch. Compare resource usage with Filebeat.
Requirements
- Create a
loggingnamespace - Deploy Fluent Bit as a DaemonSet with RBAC
- Configure the
kubernetesfilter for metadata enrichment - Configure the
esoutput to send to Elasticsearch - Parse CRI log format with a custom parser
- Generate test logs and verify they appear in Elasticsearch
- Compare memory and CPU usage between Fluent Bit and Filebeat pods
Show Solution
Step 1 — Create namespace and deploy:
kubectl create namespace logging
Step 2 — Apply the complete Fluent Bit manifest:
# fluent-bit-complete.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
namespace: logging
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser cri
DB /var/log/flb_kube.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Kube_Tag_Prefix kube.var.log.containers.
Merge_Log On
K8S-Logging.Parser On
[OUTPUT]
Name es
Match *
Host elasticsearch-es-http.elastic-system
Port 9200
Logstash_Format On
Logstash_Prefix fluent-bit-k8s
Retry_Limit False
Replace_Dots On
tls Off
Suppress_Type_Name On
parsers.conf: |
[PARSER]
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluent-bit
namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluent-bit
rules:
- apiGroups: [""]
resources: ["namespaces", "pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fluent-bit
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluent-bit
subjects:
- kind: ServiceAccount
name: fluent-bit
namespace: logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: logging
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
serviceAccountName: fluent-bit
terminationGracePeriodSeconds: 10
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.0
ports:
- containerPort: 2020
name: http-metrics
volumeMounts:
- name: config
mountPath: /fluent-bit/etc/
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlogcontainers
mountPath: /var/log/containers
readOnly: true
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
resources:
limits:
memory: 100Mi
cpu: 200m
requests:
memory: 50Mi
cpu: 50m
volumes:
- name: config
configMap:
name: fluent-bit-config
- name: varlog
hostPath:
path: /var/log
- name: varlogcontainers
hostPath:
path: /var/log/containers
- name: varlogpods
hostPath:
path: /var/log/pods
kubectl apply -f fluent-bit-complete.yaml
Step 3 — Generate test logs:
# Deploy a log generator
kubectl run log-generator --image=busybox --restart=Never \
--command -- sh -c 'while true; do echo "{\"level\":\"INFO\",\"msg\":\"test log from fluent-bit exercise\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"; sleep 2; done'
Step 4 — Verify in Elasticsearch:
# Check Fluent Bit logs for errors
kubectl logs -l app=fluent-bit -n logging --tail=20
# Check for new indices
curl -s http://localhost:9200/_cat/indices/fluent-bit-k8s-* | head -5
# Search for test logs
curl -s http://localhost:9200/fluent-bit-k8s-*/_search -H "Content-Type: application/json" -d '{
"query": { "match": { "message": "test log from fluent-bit" } },
"size": 3
}' | python3 -m json.tool
Step 5 — Compare resource usage:
# Check Fluent Bit memory and CPU
kubectl top pods -n logging -l app=fluent-bit
# Check Filebeat memory and CPU (if deployed)
kubectl top pods -n elastic-system -l app=filebeat
# Expected comparison:
# Fluent Bit: ~3-10 MB memory, ~5m CPU
# Filebeat: ~30-60 MB memory, ~20m CPU
Exercise 5 — Implement Log Retention with ILM for K8s Logs
Difficulty: ⭐⭐⭐⭐⭐ Advanced
Scenario
Your production cluster generates ~50 GB of logs per day. Management requires 90-day retention for production logs and 7-day retention for development logs. You need to implement ILM policies, use the hot-warm-cold architecture, and set up snapshots before deletion.
Requirements
- Create an ILM policy for production logs: hot (2 days) → warm (14 days) → cold (30 days) → delete (90 days)
- Create an ILM policy for development logs: hot (1 day) → delete (7 days)
- Create index templates for
k8s-production-*andk8s-development-* - Bootstrap the initial indices with write aliases
- Configure Filebeat to route logs by namespace
- Create a snapshot repository and SLM policy
- Verify ILM policies are applied and indices transition correctly
Show Solution
Step 1 — Create ILM policies:
# Production ILM policy
curl -X PUT "http://localhost:9200/_ilm/policy/k8s-production-policy" \
-H "Content-Type: application/json" -d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "30gb",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "2d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"allocate": { "require": { "data": "warm" } },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "14d",
"actions": {
"allocate": { "require": { "data": "cold" } },
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}'
# Development ILM policy
curl -X PUT "http://localhost:9200/_ilm/policy/k8s-development-policy" \
-H "Content-Type: application/json" -d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "20gb",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"delete": {
"min_age": "7d",
"actions": { "delete": {} }
}
}
}
}'
Step 2 — Create index templates:
# Production template
curl -X PUT "http://localhost:9200/_index_template/k8s-production-template" \
-H "Content-Type: application/json" -d '{
"index_patterns": ["k8s-production-*"],
"template": {
"settings": {
"index.lifecycle.name": "k8s-production-policy",
"index.lifecycle.rollover_alias": "k8s-production",
"number_of_shards": 2,
"number_of_replicas": 1
}
}
}'
# Development template
curl -X PUT "http://localhost:9200/_index_template/k8s-development-template" \
-H "Content-Type: application/json" -d '{
"index_patterns": ["k8s-development-*"],
"template": {
"settings": {
"index.lifecycle.name": "k8s-development-policy",
"index.lifecycle.rollover_alias": "k8s-development",
"number_of_shards": 1,
"number_of_replicas": 0
}
}
}'
Step 3 — Bootstrap initial indices:
# Production
curl -X PUT "http://localhost:9200/k8s-production-000001" \
-H "Content-Type: application/json" -d '{
"aliases": {
"k8s-production": { "is_write_index": true }
}
}'
# Development
curl -X PUT "http://localhost:9200/k8s-development-000001" \
-H "Content-Type: application/json" -d '{
"aliases": {
"k8s-development": { "is_write_index": true }
}
}'
Step 4 — Configure Filebeat to route by namespace:
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: elastic-system
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
output.elasticsearch:
hosts: ["http://elasticsearch.elastic-system:9200"]
indices:
- index: "k8s-production"
when.contains:
kubernetes.namespace: "production"
- index: "k8s-development"
when.contains:
kubernetes.namespace: "development"
- index: "k8s-logs-%{+yyyy.MM.dd}"
setup.ilm.enabled: false
setup.template.enabled: false
kubectl apply -f filebeat-configmap.yaml
kubectl rollout restart daemonset/filebeat -n elastic-system
Step 5 — Set up snapshot repository and SLM:
# Register snapshot repository (example: filesystem for local testing)
curl -X PUT "http://localhost:9200/_snapshot/k8s-backup" \
-H "Content-Type: application/json" -d '{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/backups"
}
}'
# Create SLM policy
curl -X PUT "http://localhost:9200/_slm/policy/nightly-k8s-snapshots" \
-H "Content-Type: application/json" -d '{
"schedule": "0 30 2 * * ?",
"name": "<k8s-snap-{now/d}>",
"repository": "k8s-backup",
"config": {
"indices": ["k8s-production-*", "k8s-development-*"],
"ignore_unavailable": true,
"include_global_state": false
},
"retention": {
"expire_after": "90d",
"min_count": 5,
"max_count": 50
}
}'
Step 6 — Verify:
# Check ILM policy status
curl -s "http://localhost:9200/k8s-production-000001/_ilm/explain" | python3 -m json.tool
# Check ILM policies exist
curl -s "http://localhost:9200/_ilm/policy/k8s-production-policy" | python3 -m json.tool
# Check index aliases
curl -s "http://localhost:9200/_aliases" | python3 -m json.tool
# Manually trigger SLM for testing
curl -X POST "http://localhost:9200/_slm/policy/nightly-k8s-snapshots/_execute"
# List snapshots
curl -s "http://localhost:9200/_snapshot/k8s-backup/_all" | python3 -m json.tool
Summary
| Exercise | Skill Practiced | Difficulty |
|---|---|---|
| 1 | Filebeat DaemonSet basics | ⭐⭐ |
| 2 | Autodiscover + multi-format parsing | ⭐⭐⭐ |
| 3 | ECK operator deployment | ⭐⭐⭐ |
| 4 | Fluent Bit as alternative shipper | ⭐⭐⭐⭐ |
| 5 | ILM + retention + snapshots | ⭐⭐⭐⭐⭐ |