ELK in Kubernetes: Elasticsearch, APM Server, and Kibana
Published: 2026-03-17
We use the ELK stack for two things: log search and application performance monitoring (APM). Logs come from Vector. APM traces come from the Elastic APM agent in .NET applications. Kibana ties them together.
Deploy via FluxCD
Three HelmReleases, all from the Elastic Helm repository:
yaml# elasticsearch
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch
namespace: elk
spec:
chart:
spec:
chart: elasticsearch
version: "8.x"
sourceRef:
kind: HelmRepository
name: elastic
values:
replicas: 1
minimumMasterNodes: 1
esJavaOpts: "-Xmx2g -Xms2g"
resources:
requests:
cpu: 500m
memory: 3Gi
limits:
cpu: 2000m
memory: 4Gi
volumeClaimTemplate:
resources:
requests:
storage: 200Gi
storageClassName: local-path
persistence:
enabled: true
esJavaOpts sets the JVM heap. The rule: heap = half the container memory limit. With memory: 4Gi limit, -Xmx2g leaves 2 GB for OS page cache (used by Lucene).
yaml# kibana
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kibana
namespace: elk
spec:
chart:
spec:
chart: kibana
version: "8.x"
sourceRef:
kind: HelmRepository
name: elastic
values:
elasticsearchHosts: "http://elasticsearch-master.elk.svc.cluster.local:9200"
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
yaml# apm-server
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: apm-server
namespace: elk
spec:
chart:
spec:
chart: apm-server
version: "8.x"
sourceRef:
kind: HelmRepository
name: elastic
values:
apmConfig:
apm-server.yml: |
apm-server:
host: "0.0.0.0:8200"
output.elasticsearch:
hosts: ["http://elasticsearch-master.elk.svc.cluster.local:9200"]
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
APM agent in .NET
xml<PackageReference Include="Elastic.Apm.NetCoreAll" Version="1.*" />
One line in Program.cs:
csharpbuilder.Services.AddAllElasticApm();
Configure via environment variables:
yamlenv:
- name: ELASTIC_APM_SERVER_URL
value: "http://apm-server.elk.svc.cluster.local:8200"
- name: ELASTIC_APM_SERVICE_NAME
value: "my-service"
- name: ELASTIC_APM_ENVIRONMENT
value: "prod"
- name: ELASTIC_APM_LOG_LEVEL
value: "Warning"
- name: ELASTIC_APM_TRANSACTION_SAMPLE_RATE
value: "0.1" # sample 10% — reduce APM storage
The agent auto-instruments ASP.NET Core requests, Entity Framework queries, and HttpClient calls. No manual span creation required for most use cases.
Custom APM spans
csharpvar transaction = Agent.Tracer.CurrentTransaction;
using var span = transaction.StartSpan("ProcessPayment", "payment");
span.SetLabel("order_id", orderId.ToString());
try {
await _paymentService.ChargeAsync(orderId);
span.Outcome = Outcome.Success;
} catch (Exception ex) {
span.CaptureException(ex);
throw;
}
Index lifecycle management
Without ILM, Elasticsearch grows unbounded:
jsonPUT _ilm/policy/k8s-logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "20gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 }
}
},
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}
Apply via Kibana Dev Tools or a CI job. Data older than 30 days is deleted automatically. Warm phase force-merges segments to reduce disk usage.
Useful Kibana queries
# All errors in the last hour
kubernetes.namespace: "app" AND log.level: ERROR
# Slow HTTP requests (APM)
transaction.duration.us > 5000000
# Specific pod
kubernetes.pod.name: "my-service-abc123"
# Exception trace
error.exception.message: *NullReference*
KQL (Kibana Query Language) is more readable than Lucene and supports autocomplete in Discover.
APM → Kibana service map
With APM instrumented across multiple services, Kibana's Service Map shows:
- Which services call which
- Average latency per edge
- Error rate per service
This is the distributed tracing map that APM builds automatically from spans. No manual topology config.
Troubleshooting
APM data not showing in Kibana: Check APM Server logs and verify ELASTIC_APM_SERVER_URL is reachable from the app pod.
High memory / OOM: Reduce esJavaOpts heap if other workloads are competing for memory. Don't set heap > 50% of container limit.
Index not created: Vector writes vector-YYYY-MM-DD indices. If no logs appear, check Vector sink errors (see the Vector article).