Elasticsearch Index Lifecycle Management
Published: 2026-03-22
Without ILM, Elasticsearch indices grow indefinitely. On a single-node cluster with 50GB PVC, that's a disk-full within weeks. ILM automates the lifecycle: rollover when an index gets too big, merge and shrink on older data, delete after a retention period.
The four phases
| Phase | Trigger | Actions |
|---|---|---|
| hot | current write index | rollover at size or age |
| warm | after rollover | forcemerge, shrink (multi-node only) |
| cold | older data, low query rate | freeze (searchable, read-only) |
| delete | retention exceeded | delete |
For a single-node setup: skip shrink (requires multiple primary shards), skip freeze (adds overhead), use hot → delete only.
Creating the ILM policy
bashcurl -X PUT "http://elasticsearch:9200/_ilm/policy/vector-logs" \
-H "Content-Type: application/json" \
-d '{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "20gb",
"max_age": "7d"
},
"set_priority": {
"priority": 100
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}'
Rolls over the write index when it reaches 20GB or 7 days old. Indices older than 30 days from rollover are deleted.
Index template
The template applies the ILM policy to all new indices automatically:
bashcurl -X PUT "http://elasticsearch:9200/_index_template/vector-logs" \
-H "Content-Type: application/json" \
-d '{
"index_patterns": ["vector-*"],
"template": {
"settings": {
"index.lifecycle.name": "vector-logs",
"index.lifecycle.rollover_alias": "vector-logs",
"number_of_replicas": 0,
"number_of_shards": 1
},
"mappings": {
"properties": {
"@timestamp": {"type": "date"},
"message": {"type": "text"},
"level": {"type": "keyword"},
"kubernetes.namespace": {"type": "keyword"},
"kubernetes.pod.name": {"type": "keyword"}
}
}
}
}'
number_of_replicas: 0 — essential for single-node cluster. Elasticsearch keeps shards yellow if replicas can't be placed. number_of_shards: 1 — one shard per index for small-to-medium data volumes.
Bootstrap the write alias
ILM requires a write alias pointing to the initial index:
bashcurl -X PUT "http://elasticsearch:9200/vector-logs-000001" \
-H "Content-Type: application/json" \
-d '{
"aliases": {
"vector-logs": {
"is_write_index": true
}
}
}'
Vector's Elasticsearch sink writes to the alias vector-logs, not to a specific index. ILM creates vector-logs-000002, vector-logs-000003, etc. on rollover.
Bootstrap script as a Kubernetes Job
bash#!/bin/bash
set -e
ES_URL="http://elasticsearch-master.observability.svc.cluster.local:9200"
# Wait for ES
until curl -sf "$ES_URL/_cluster/health?wait_for_status=yellow"; do
echo "waiting for elasticsearch..."
sleep 5
done
# Apply ILM policy
curl -sf -X PUT "$ES_URL/_ilm/policy/vector-logs" \
-H "Content-Type: application/json" \
--data-binary @/bootstrap/ilm-policy.json
# Apply index template
curl -sf -X PUT "$ES_URL/_index_template/vector-logs" \
-H "Content-Type: application/json" \
--data-binary @/bootstrap/index-template.json
# Create initial index (idempotent: ignore 400)
curl -sf -X PUT "$ES_URL/vector-logs-000001" \
-H "Content-Type: application/json" \
--data-binary @/bootstrap/initial-index.json \
|| true
echo "Bootstrap complete"
The || true on the last step makes the Job idempotent — re-running it after a cluster rebuild doesn't fail if the index already exists.
Monitoring ILM
bash# Check which phase each index is in
curl http://elasticsearch:9200/vector-*/_ilm/explain?filter_path=indices.*.phase,indices.*.age
# Check for ILM errors
curl http://elasticsearch:9200/vector-*/_ilm/explain | jq '.indices | to_entries[] | select(.value.step_info.type != null)'
# Manually trigger rollover
curl -X POST "http://elasticsearch:9200/vector-logs/_rollover"
# Force ILM to re-evaluate
curl -X POST "http://elasticsearch:9200/vector-logs-000001/_ilm/retry"
Shards sizing guide
| Data volume | Shards | Shard size |
|---|---|---|
| < 5 GB/day | 1 | < 10 GB |
| 5–20 GB/day | 1–3 | 10–30 GB |
| > 50 GB/day | 3–6 | 20–50 GB |
Over-sharding is a common mistake. Each shard has overhead (file handles, memory). For small datasets, 1 shard per index is optimal.
Troubleshooting
Alias doesn't exist error on rollover: The bootstrap script wasn't run or the initial index doesn't have the alias. Run bootstrap again.
Index yellow: Usually number_of_replicas > 0 on a single-node cluster. Set replicas to 0.
ILM not progressing: ILM checks every 10 minutes by default. Use /_ilm/explain to see the current step and any errors. Use /_ilm/retry to unstick a failed step.