Elasticsearch in Kubernetes: HelmRelease, ingest pipelines, and bootstrap
Published: 2026-03-14
Elasticsearch runs on every cluster. It stores APISIX access logs and application logs from Vector. Getting it right requires more than just a HelmRelease — there's a bootstrap script that initializes ingest pipelines, index templates, and sets replica counts correctly for a single-node setup.
The HelmRelease
Elasticsearch is deployed from the charts bundled in the git repo itself — not from an external Helm registry:
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch
spec:
interval: 1h
chart:
spec:
chart: ./charts/elastic/elasticsearch
version: "7.17.3"
sourceRef:
kind: GitRepository
name: flux-system
namespace: flux-system
install:
remediation:
retries: -1
upgrade:
remediation:
retries: -1
retries: -1 means Flux will keep retrying indefinitely on failure. This is intentional for stateful services — a transient PVC provisioning delay shouldn't permanently mark the release as failed.
Per-environment patches
The base HelmRelease contains no hostname or storage config. Patches add environment-specific values:
yaml# projects/dev/kustomization/hub/patches/elasticsearch.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch
namespace: dev
spec:
values:
master:
persistence:
storageClass: local-path
resources:
requests:
memory: "4Gi"
cpu: "500m"
limits:
memory: "6Gi"
esConfig:
elasticsearch.yml: |
xpack.security.enabled: true
xpack.monitoring.enabled: false
ingress:
hosts:
- host: elasticsearch.dev.example.com
paths:
- path: /
YC clusters use yc-network-ssd instead of local-path.
Bootstrap script
After first install, Elasticsearch is a blank slate. The bootstrap script creates the necessary configuration:
bashES_USER=elastic ES_PASS='<password>' ./scripts/bootstrap-elasticsearch.sh
It runs against all clusters and does:
1. Ingest pipeline for APISIX logs:
json{
"description": "Parse APISIX access log JSON",
"processors": [
{ "date": { "field": "timestamp", "formats": ["UNIX_MS"], "target_field": "@timestamp" } },
{ "remove": { "field": "timestamp" } },
{ "geoip": { "field": "remote_addr", "ignore_missing": true } }
]
}
2. Index template for APISIX:
json{
"index_patterns": ["apisix*"],
"settings": {
"number_of_replicas": 0,
"default_pipeline": "apisix"
}
}
3. Index templates for application logs:
json{
"index_patterns": ["vector*", "app*"],
"settings": { "number_of_replicas": 0 }
}
number_of_replicas: 0 is essential for single-node Elasticsearch. With replicas > 0, indices stay yellow forever.
4. Patch existing indices:
bashcurl -X PUT "http://${ES_HOST}:9200/vector*/_settings" \
-H 'Content-Type: application/json' \
-d '{"index": {"number_of_replicas": 0}}'
This handles the case where Vector created indices before the template was applied.
Checking cluster health
bashkubectl port-forward -n logging svc/elasticsearch 9200:9200
# Cluster health
curl -u elastic:$ES_PASS http://localhost:9200/_cluster/health?pretty
# Index list with status
curl -u elastic:$ES_PASS http://localhost:9200/_cat/indices?v
# Check all indices have 0 replicas (single node should be all green)
curl -u elastic:$ES_PASS http://localhost:9200/_cat/indices?v&h=index,health,rep
All indices should be green with 0 replicas on a single-node cluster.
Index lifecycle
APISIX logs write to apisix (single index with the ingest pipeline). Application logs write to vector-YYYY-MM-DD (daily rolling indices).
No ILM is configured — old indices are deleted when disk fills up. If you want automation:
bash# Delete indices older than 30 days
curl -X DELETE "http://localhost:9200/vector-$(date -d '30 days ago' +%Y.%m.%d)"
Or configure a CronJob to run this periodically.
Kibana
Kibana runs only on the infra cluster and connects to the infra Elasticsearch instance. It provides the APM UI, Discover, and dashboards for access log analysis.
For spoke clusters, raw Elasticsearch queries via port-forward or the Elasticsearch Exporter + Grafana dashboard are sufficient.
Elasticsearch Exporter
The Prometheus Elasticsearch exporter scrapes index stats, cluster health, and JVM metrics:
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: elasticsearch-exporter
namespace: observability
spec:
chart:
spec:
chart: prometheus-elasticsearch-exporter
sourceRef:
kind: HelmRepository
name: prometheus-community
values:
es:
uri: http://elasticsearch.logging.svc.cluster.local:9200
username: elastic
password: "" # use secretRef in practice
serviceMonitor:
enabled: true
labels:
release: kube-prom-stack
Key metrics: elasticsearch_cluster_health_status, elasticsearch_indices_docs, elasticsearch_jvm_memory_used_bytes.
Troubleshooting
Yellow cluster health: Check replica count — number_of_replicas must be 0 on single-node.
OOM kills: Elasticsearch is memory-hungry. The JVM heap is set to 50% of container memory limit. A 4Gi limit gives 2Gi heap — adequate for moderate log volumes.
Slow queries / high CPU: Check the Slow Query Log:
json{"index": {"search.slowlog.threshold.query.warn": "2s"}}
Index not using pipeline: Check the default_pipeline is set in the index template, not just the index settings.