Rolling Back a Broken ILM Policy Version
A bad edit to app-events-ilm — say a delete phase whose min_age was shortened from 30d to 3d — is live and already deleting data early; revert it safely without waiting for a fix to be authored from scratch.
Rolling back an Elasticsearch ILM policy is not a call to some “revert” API, because none exists. A deployment keeps only the current definition, a version counter, and a modified_date; it stores no history of previous bodies. Git is the history. A rollback is therefore a forward apply of an earlier known-good revision — the same put_lifecycle mechanism as any other change, pointed at an older commit. This is the recovery counterpart to promoting ILM policy changes across environments and builds on the field mechanics laid out in versioning ILM policies across environments, all within ILM policy design and lifecycle synchronization. The crucial caveat: re-applying the good definition fixes what future indices and future phase transitions will do, but it cannot rewind an index that already advanced or an index that was already deleted.
Prerequisites
- Elasticsearch 8.x with the affected policy (
app-events-ilm) live, and the previous known-good policy JSON retrievable from source control by commit. elasticsearch-pyv8.0+, using the v8 surface (ilm.put_lifecycle,ilm.explain_lifecycle,ilm.move_to_step,ilm.retry).manage_ilmat cluster scope, plusmanageonapp-events-*to drive per-index recovery, per securing ILM policies with RBAC.
Implementation
Rollback is two moves. First, re-PUT the previous good body so the definition reverts (its version bumps forward — Elasticsearch only ever increments). Second, deal with any index the bad version already harmed or wedged: an index sitting in an ERROR step after you fix the definition needs ilm.retry, and an index that advanced into the wrong phase needs ilm.move_to_step to be nudged back onto a valid step. Recovering a genuinely stuck step in depth is the subject of recovering a stuck ILM step.
import json
import logging
from elasticsearch import Elasticsearch
from elasticsearch import ApiError, BadRequestError, NotFoundError
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
POLICY_ID = "app-events-ilm"
def load_good_revision(path: str, git_sha: str) -> dict:
"""Load the previous known-good policy JSON from git and stamp its source sha."""
with open(path, encoding="utf-8") as fh:
artifact = json.load(fh)
# Record which revision we rolled back TO — the version integer will still bump forward.
artifact["policy"].setdefault("_meta", {})["git_sha"] = git_sha
return artifact
def rollback_policy(client: Elasticsearch, artifact: dict) -> dict:
"""Re-PUT the known-good definition. version increments; the actions revert."""
before = client.ilm.get_lifecycle(name=POLICY_ID)[POLICY_ID]["version"]
try:
client.ilm.put_lifecycle(name=POLICY_ID, policy=artifact["policy"])
except BadRequestError as exc:
logger.error("Rollback body rejected: %s", exc.info)
raise
live = client.ilm.get_lifecycle(name=POLICY_ID)[POLICY_ID]
logger.info("Rolled back %s: version %s -> %s, restored git_sha=%s",
POLICY_ID, before, live["version"], live["policy"].get("_meta", {}).get("git_sha"))
return live
def recover_indices(client: Elasticsearch, index_pattern: str = "app-events-*") -> None:
"""After reverting the definition, unstick any index the bad version left in ERROR."""
explain = client.ilm.explain_lifecycle(index=index_pattern)
for idx, state in explain.get("indices", {}).items():
if state.get("step") != "ERROR":
continue
logger.warning("%s in ERROR: %s", idx, state.get("step_info"))
try:
# Per-index retry re-runs the failed step under the now-reverted definition.
client.ilm.retry(index=idx)
logger.info("Retried %s", idx)
except ApiError as exc:
logger.error("Retry failed for %s (%s): %s", idx, exc.status_code, exc.info)
def move_off_bad_step(client: Elasticsearch, index: str) -> None:
"""For an index that advanced into the wrong action, move it back to a valid step."""
state = client.ilm.explain_lifecycle(index=index)["indices"][index]
current = {"phase": state["phase"], "action": state["action"], "name": state["step"]}
# Steer the index back to the hot-phase rollover check under the reverted policy.
target = {"phase": "hot", "action": "rollover", "name": "check-rollover-ready"}
client.ilm.move_to_step(index=index, current_step=current, next_step=target)
logger.info("Moved %s from %s to %s", index, current["name"], target["name"])
if __name__ == "__main__":
es = Elasticsearch("https://prod-es:9200", api_key="PROD_KEY", verify_certs=True)
try:
good = load_good_revision("repo/ilm/app-events-ilm.json", git_sha="9f8e7d6c5b4a")
rollback_policy(es, good) # 1. revert the definition
recover_indices(es) # 2. unstick any index left in ERROR
except NotFoundError:
logger.error("Policy %s not found — nothing to roll back.", POLICY_ID)
raiseNote what the rollback does and does not touch. Re-PUTting the good body reverts the actions every index will run from its next step evaluation onward. It does not undo a delete that already fired: an app-events-* index the shortened delete phase already removed is gone, and no policy change brings it back — restore it from a snapshot instead. move_to_step and retry operate per index and only help indices that still exist and are still managed.
Verification
Confirm the definition reverted by reading the phase that was broken back to its correct value, and confirm the restored _meta.git_sha:
GET _ilm/policy/app-events-ilm?filter_path=**.version,**._meta,**.policy.phases.delete{
"app-events-ilm": {
"version": 14,
"policy": {
"_meta": { "git_sha": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e", "version": "2.3.0" },
"phases": {
"delete": { "min_age": "30d", "actions": { "delete": {} } }
}
}
}
}The delete.min_age is back to 30d and _meta reflects the good revision — even though version has climbed to 14 rather than returning to an earlier number, because it only ever increments. Then check the affected indices are no longer wedged:
GET app-events-*/_ilm/explain?filter_path=**.step,**.phase,**.step_infoEvery remaining app-events-* index should report a non-ERROR step (typically hot/rollover/check-rollover-ready), confirming the reverted definition took and the recovery unstuck anything the bad version had halted.
Gotchas and edge cases
- Re-applying does not rewind indices already advanced or deleted. The rollback changes future behavior only. An index the bad
deletephase already removed cannot be recovered by any policy edit — restore it from a snapshot. An index that already force-merged or relocated under the bad version stays in that state; the good definition governs it from its next transition. - Version only increments — it never returns to the old number. After a rollback,
versionis higher than before, not lower, andmodified_datereflects the rollback moment. Verify the rollback by inspecting the reverted phases and_meta.git_sha, never by expectingversionto match a previous value. - Check
in_use_bybefore assuming the rollback protects anything. Ifin_use_byis empty, the policy is attached to nothing and reverting it changes no index’s behavior — the real problem may be a missing template binding. Confirmin_use_bylists theapp-events-*indices you intend to protect before and after the rollback. - Test the rollback in staging first. Apply the good revision to staging and run
explain_lifecyclethere before touching production, so a mistaken “good” revision (or one that references a repository production lacks) fails on staging rather than compounding the incident.
FAQ
Does Elasticsearch keep old policy versions I can roll back to?
No. A deployment stores only the current definition plus a version counter and modified_date; there is no server-side archive of previous bodies. Rollback means re-applying an earlier revision from git with put_lifecycle, which increments version forward while reverting the actual actions. Your source control is the only history that exists.
Will rolling back recover data the bad policy already deleted?
No. Re-applying the good definition only changes what indices do from their next step evaluation onward — it cannot resurrect an index a shortened delete phase already removed. Recover deleted data from a snapshot. For an index that merely advanced into the wrong phase but still exists, use ilm.move_to_step to steer it back to a valid step under the reverted policy.
An index is stuck in ERROR after I reverted the policy — how do I clear it?
Once the definition is reverted and the underlying cause resolved, call ilm.retry(index=...) to re-run the failed step under the good definition. If the index advanced into an invalid action, use ilm.move_to_step to move it to a valid step first. The deeper recovery playbook for a genuinely wedged step is covered in recovering a stuck ILM step.
Related
- Promoting ILM policy changes across environments — the forward promotion this rollback undoes.
- Versioning ILM policies across environments — how version, modified_date and in_use_by behave, and why git is the history.
- Recovering a stuck ILM step — unsticking an index wedged in an ERROR step with move and retry.
← Back to Versioning ILM Policies Across Environments · ILM Policy Design & Lifecycle Synchronization