Migrating from Curator to ILM Policies

Retire a running elasticsearch-curator rotation and hand the same live indices to an ILM policy so that neither tool ever deletes the same data twice and no index is left unmanaged in between.

The risk in this migration is not the policy authoring — that part is mechanical. It is the cutover ordering. Curator runs from cron with no knowledge of ILM, and ILM has no knowledge of the cron; for a brief window both could hold a claim on the same syslog-* indices. Do it in the wrong order and either nothing rotates (both disabled) or both rotate (both live), and overlapping delete steps race. This procedure sequences the four operations — create the policy, attach a template, adopt existing indices, disable the cron — so there is exactly one controller acting on every index at every instant. It is the detailed runbook behind the higher-level comparison in ILM rollover vs Curator-based rotation, and it plugs into the broader Index Lifecycle Management (ILM) control plane.

Prerequisites

  • Elasticsearch 8.x with data-tier roles assigned, and elasticsearch-py v8.0+ for the automation step.
  • The existing Curator action_file.yml and its cron/timer entry, plus shell access to the host that runs it.
  • manage_ilm and manage_index_templates cluster privileges, and manage on the syslog-* pattern.
  • A staging deployment that mirrors the production index naming, so the cutover is rehearsed before it touches live retention.

Implementation

The safe cutover is a strict sequence. Each step narrows who controls the indices until only ILM remains, and the Curator cron is not disabled until ILM demonstrably owns every index.

Safe Curator-to-ILM cutover orderingA left-to-right four-step timeline. Step 1: create the ILM policy — inert, Curator still owns rotation. Step 2: attach via composable template — only new indices become managed. Step 3: adopt existing indices with PUT index/_settings — older indices come under ILM. Step 4: disable the Curator cron — ILM is now the sole controller. A control bar beneath the steps shows Curator in control across steps 1 to 3 and ILM taking full control at step 4, with a marked handoff so the two never both hold the delete phase for the same index.1Create policyinert — manages nothingPUT _ilm/policy/syslog-ilm2Attach templatenew indices managedindex.lifecycle.name3Adopt existingolder indices managedPUT index/_settings4Disable cronILM sole controllercrontab -e / systemctlwho controls rotationCurator still owns delete — cron liveILM onlyhandoff: disable the cron only after adoption completes — never both holding delete

1. Author the ILM policy equivalent to the Curator actions

Translate each Curator action into a phase. Rollover conditions move into the hot phase unchanged; a forcemerge filtered by age becomes a warm-phase action with a min_age; the delete_indices age filter becomes the delete phase. Reproduce the retention intent, not the literal day count, because ILM measures phase age from rollover rather than creation.

PUT _ilm/policy/syslog-ilm
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": { "max_age": "1d", "max_primary_shard_size": "50gb" },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "2d",
        "actions": { "forcemerge": { "max_num_segments": 1 } }
      },
      "delete": {
        "min_age": "90d",
        "actions": { "delete": {} }
      }
    }
  }
}

Creating the policy is inert: it manages nothing until an index references it, so this step is completely safe while the Curator cron is still live.

2. Attach the policy via a composable template

Bind the policy so every new backing index is managed from creation. The template also names the write alias so ILM knows which alias to advance on rollover. Building this template pair in full is covered in bootstrapping index templates and data streams.

PUT _index_template/syslog-template
{
  "index_patterns": ["syslog-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "syslog-ilm",
      "index.lifecycle.rollover_alias": "syslog-write"
    }
  }
}

A template only affects indices created after it is applied. Every syslog-* index that already exists is untouched by this step — which is exactly why step 3 exists.

3. Attach the policy to existing indices

Bring the already-created indices under management explicitly. For the current write index, set both the policy and the rollover alias; for older backing indices that only need to progress through warm and delete, the policy name alone is enough.

PUT syslog-000006/_settings
{
  "index.lifecycle.name": "syslog-ilm"
}

At this point every index is managed by ILM, but the Curator cron is still running — both systems can act. That overlap is tolerable only because you have not yet reached a delete boundary under ILM; you must close it in step 4 before ILM’s delete phase can fire.

4. Disable the Curator cron

With every index confirmed managed, remove Curator from the loop so ILM is the sole controller. Disable the schedule, do not merely stop a running invocation:

# Remove the syslog rotation entry, or disable the timer unit.
crontab -e                          # delete the curator line
systemctl disable --now curator-syslog.timer

Only now is the migration complete. ILM owns rotation end to end, and no cron will fire a second delete against an index ILM is relocating or has already removed.

5. Automate steps 1 and 3 with the Python v8 client

For a repeatable, auditable cutover, drive policy creation and existing-index adoption through the v8 client in one idempotent pass. This applies the policy, then attaches it to every existing index that is not yet managed:

import logging
from elasticsearch import Elasticsearch, ApiError

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)


def migrate_to_ilm(es: Elasticsearch, policy_name: str, pattern: str, policy: dict) -> None:
    # 1. Apply the policy — idempotent, and inert until an index references it.
    es.ilm.put_lifecycle(name=policy_name, policy=policy)
    logger.info("Policy '%s' applied.", policy_name)

    # 2. Adopt existing indices that are not yet managed by any policy.
    explained = es.ilm.explain_lifecycle(index=pattern)
    for index, meta in explained["indices"].items():
        if meta.get("managed"):
            logger.info("Skipping %s — already managed by %s.", index, meta.get("policy"))
            continue
        # Attach the policy in place; the template only covers indices created later.
        es.indices.put_settings(
            index=index,
            settings={"index.lifecycle.name": policy_name},
        )
        logger.info("Attached '%s' to %s.", policy_name, index)


if __name__ == "__main__":
    es = Elasticsearch(
        "https://es-cluster-01:9200",
        api_key="YOUR_BASE64_ENCODED_API_KEY",
        verify_certs=True,
    )

    syslog_policy = {
        "phases": {
            "hot": {"min_age": "0ms", "actions": {
                "rollover": {"max_age": "1d", "max_primary_shard_size": "50gb"}}},
            "warm": {"min_age": "2d", "actions": {"forcemerge": {"max_num_segments": 1}}},
            "delete": {"min_age": "90d", "actions": {"delete": {}}},
        }
    }

    try:
        migrate_to_ilm(es, "syslog-ilm", "syslog-*", syslog_policy)
    except ApiError as exc:
        logger.error("Migration step failed (%s): %s", exc.status_code, exc.info)
        raise

Run this script before disabling the cron. It is safe to re-run: put_lifecycle replaces the policy in place, and indices already managed are skipped rather than re-attached.

Verification

Confirm ILM owns every index before you trust the cron is safe to remove. First, check that no syslog-* index is left unmanaged:

GET syslog-*/_ilm/explain?filter_path=indices.*.managed,indices.*.policy
{
  "indices": {
    "syslog-000006": { "managed": true, "policy": "syslog-ilm" },
    "syslog-000007": { "managed": true, "policy": "syslog-ilm" }
  }
}

Every entry must show "managed": true with "policy": "syslog-ilm". A single "managed": false means an index would be orphaned the moment the cron stops. Second, confirm the Curator schedule is actually gone from the host — a stopped invocation is not a disabled schedule:

crontab -l | grep -i curator          # expect no output
systemctl list-timers | grep -i curator   # expect no output

Both commands returning nothing confirms ILM is the sole controller and the cutover is complete.

Gotchas and edge cases

  • Stop Curator before ILM can delete the same indices — order is everything. The dangerous window is when both hold a delete claim. Disable the cron only after adoption is verified, but before any index reaches its ILM delete min_age. If you disable the cron too early, rotation stops entirely until ILM adopts the indices; too late, and overlapping deletes race and can drop an index mid-relocation.
  • Existing indices need an explicit attach — the template is not retroactive. A composable template only manages indices created after it is applied. Every backing index that predates the template stays unmanaged until you PUT <index>/_settings with the policy name, which is what step 3 and the Python loop handle.
  • min_age is measured from rollover, not creation. Curator age filters typically count from creation_date; ILM counts phase age from the rollover event. An index that filled slowly before rolling will hit its ILM delete boundary later than the old Curator rule would have. Do not assume “older than 90 days” carries over numerically — verify the effective retention in _ilm/explain.
  • Rehearse in staging first. A mistimed cutover on a live pipeline can either stop rotation (disks fill) or double-delete (retention breach). Run the full sequence against a staging deployment with the same index naming, watch one rollover and one phase transition happen under ILM, then repeat in production.

FAQ

What if I disable the Curator cron before ILM adopts the old indices?

Then those pre-existing indices are managed by nothing: Curator has stopped and ILM never adopted them, so they will not force-merge, migrate, or delete. They sit and accumulate until a disk watermark blocks writes. Always verify "managed": true on every syslog-* index before removing the cron, and run the adoption step for any that report false.

Do I need to re-index or restart anything to attach the policy?

No. Attaching a policy to an existing index is a live settings update via PUT <index>/_settings — no re-index, no restart, no downtime. ILM picks the index up at its next poll and begins evaluating it from its current age. The data and its shards are untouched; only the index metadata gains the index.lifecycle.name setting.

Can I keep Curator running for cleanup tasks ILM does not handle?

Yes, provided it acts only on indices ILM does not manage. Narrow the Curator delete filter to a pattern that cannot overlap syslog-* (for example, unmanaged legacy names), so the two never contend for the same index. Using Curator for genuine one-off cleanup of unmanaged indices is fine; the migration only removes it from the recurring rotation of managed data.

← Back to ILM Rollover vs Curator · ILM Architecture & Fundamentals