Attaching an ILM Policy in a Composable Index Template

Put index.lifecycle.name inside a composable index template’s settings so that every index created under its pattern inherits the policy at birth, without any per-index manual step.

Attaching the pointer in the template is the difference between a managed pipeline and a pile of orphaned indices. A policy sitting in the deployment is inert; it governs an index only once that index carries index.lifecycle.name in its effective settings, and settings are frozen into an index the instant it is created. The template is the injection point, and this page covers exactly what to write into it — including the one extra setting (index.lifecycle.rollover_alias) that alias-based rollover needs and data streams must never have. It is the narrow, load-bearing detail inside the broader work of bootstrapping index templates and data streams, which in turn sits under ILM Policy Design & Lifecycle Synchronization.

Prerequisites

  • Elasticsearch 8.x and an existing ILM policy named logs-nginx-ilm (see building custom ILM policies via the API).
  • elasticsearch-py v8.0+ — the code uses indices.put_index_template with keyword arguments, not body=.
  • manage_index_templates and manage_ilm cluster privileges on the applying account.
  • A decision on rollover mechanism: data stream (no alias) or alias-based (needs a rollover_alias and a bootstrap write index).

Implementation

The lifecycle pointer is a single setting — index.lifecycle.name — placed in template.settings (or in a composed_of component template that supplies settings). The rollover mechanism decides whether a second setting rides alongside it. The diagram below shows the two shapes the same attachment takes.

Alias-based versus data-stream attachment of an ILM policy in one templateTwo side-by-side template panels. The left panel, alias-based, sets index.lifecycle.name to logs-nginx-ilm and index.lifecycle.rollover_alias to logs-nginx, and notes a bootstrap write index flagged is_write_index true is required. The right panel, data-stream, sets index.lifecycle.name to logs-nginx-ilm, adds data_stream empty object, sets no rollover_alias, and notes no bootstrap index is needed because the stream owns the write pointer.One pointer, two rollover mechanismsAlias-based rolloverindex.lifecycle.name: logs-nginx-ilmindex.lifecycle.rollover_alias: logs-nginxrequires a bootstrap write index:is_write_index: true on -000001for updatable / by-ID document workloadsData streamindex.lifecycle.name: logs-nginx-ilmdata_stream: {} · no rollover_aliasno bootstrap index needed:the stream owns the write pointerfor append-only timestamped telemetry

Data-stream variant

For append-only telemetry, add data_stream: {} and attach the policy through settings. No alias, no bootstrap index:

PUT _index_template/logs-nginx-template
{
  "index_patterns": ["logs-nginx*"],
  "data_stream": {},
  "priority": 200,
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-nginx-ilm"
    }
  }
}

The first document indexed into logs-nginx (or an explicit PUT _data_stream/logs-nginx) creates a backing index that already carries the policy. Because the stream is the write target, ILM rolls it automatically — there is nothing to alias.

Alias-based variant

When the workload updates documents by ID rather than only appending, use an alias. The template names the alias and, critically, you bootstrap the first write index yourself:

PUT _index_template/logs-nginx-template
{
  "index_patterns": ["logs-nginx*"],
  "priority": 200,
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-nginx-ilm",
      "index.lifecycle.rollover_alias": "logs-nginx"
    }
  }
}
PUT logs-nginx-000001
{
  "aliases": { "logs-nginx": { "is_write_index": true } }
}

The -000001 suffix is required: ILM parses the trailing zero-padded integer and increments it on each rollover, so the bootstrap index name must end in a number. Miss the bootstrap and the first write creates a plain index with no write alias, and rollover never fires.

Driving it from Python (v8)

The same attachment, applied idempotently from the v8 client — the template PUT is an upsert, so re-running converges the deployment without side effects:

from elasticsearch import Elasticsearch, ApiError

es = Elasticsearch("https://es-cluster-01:9200", api_key="YOUR_KEY", verify_certs=True)


def attach_policy_data_stream(policy: str = "logs-nginx-ilm") -> None:
    es.indices.put_index_template(
        name="logs-nginx-template",
        index_patterns=["logs-nginx*"],
        data_stream={},                       # data-stream variant: no rollover_alias
        priority=200,
        template={"settings": {"index.lifecycle.name": policy}},
    )


def attach_policy_alias(alias: str = "logs-nginx", policy: str = "logs-nginx-ilm") -> None:
    es.indices.put_index_template(
        name="logs-nginx-template",
        index_patterns=[f"{alias}*"],
        priority=200,
        template={
            "settings": {
                "index.lifecycle.name": policy,
                "index.lifecycle.rollover_alias": alias,   # alias-based variant only
            }
        },
    )
    # Bootstrap the first write index so rollover has a numbered starting point.
    try:
        es.indices.create(index=f"{alias}-000001", aliases={alias: {"is_write_index": True}})
    except ApiError as exc:
        if getattr(exc, "status_code", None) != 400:   # already-exists on re-run is fine
            raise

Verification

Create the index or stream, then read the effective setting off the resulting index — the template’s declared intent is not proof; the merged result is. For the data-stream variant:

GET .ds-logs-nginx-2026.07.17-000001/_settings?filter_path=**.lifecycle
{
  ".ds-logs-nginx-2026.07.17-000001": {
    "settings": { "index": { "lifecycle": { "name": "logs-nginx-ilm" } } }
  }
}

An empty response means no template matched (or a higher-priority one won without the pointer) and the index is unmanaged. Then confirm ILM adopted it:

GET .ds-logs-nginx-2026.07.17-000001/_ilm/explain
{
  "indices": {
    ".ds-logs-nginx-2026.07.17-000001": {
      "managed": true,
      "policy": "logs-nginx-ilm",
      "phase": "hot",
      "step": "check-rollover-ready"
    }
  }
}

"managed": true with a non-ERROR step is the healthy state. For the alias-based variant, run the same two checks against logs-nginx-000001, and additionally confirm the alias has exactly one write index with GET _cat/aliases/logs-nginx?v&h=alias,index,is_write_index.

Gotchas and edge cases

  • The template only governs indices created after it exists. Attaching index.lifecycle.name to a template does nothing to already-open indices; they keep whatever settings they were born with. To bring an existing index under management, set the pointer directly with PUT <index>/_settings, or wait for the next rollover to mint a backing index under the new template.
  • Priority must win the overlap. If a higher-priority template also matches the pattern, its settings apply and yours are ignored wholesale — there is no field-level blend. Elasticsearch’s built-in logs-*/metrics-* templates sit at 100200, so claim a higher priority for any pattern that could overlap them.
  • rollover_alias is for alias-based rollover only. Setting it on a data-stream template produces an index ILM cannot roll, because the stream already owns the write pointer the alias tries to claim. Data-stream templates carry data_stream: {} and no alias.
  • A data-stream template needs data_stream: {}, not just the pattern. Without the marker, PUT _data_stream/logs-nginx fails with no matching index template with a data_stream definition, and a first write creates an ordinary index instead of a stream.

FAQ

Where exactly does the lifecycle pointer go in the template?

In template.settings as index.lifecycle.name, or inside a composed_of component template that supplies settings. Both resolve into the created index’s effective settings; when both are present, the inline template block wins over the component fragment. The key must be the full dotted index.lifecycle.name — a bare lifecycle.name outside the index namespace is silently ignored.

Will attaching a policy to a template manage my existing indices?

No. A template applies only to indices created after it is registered, so existing open indices keep their original settings and stay unmanaged if they were created without the pointer. Attach the policy directly to those with PUT <index>/_settings {"index.lifecycle.name":"logs-nginx-ilm"}, or let the next rollover create a fresh backing index that inherits it.

Do I set rollover_alias for a data stream?

Never. A data stream is its own write pointer and rolls its backing indices automatically, so index.lifecycle.rollover_alias has nothing to point at and actively breaks rollover if present. Use rollover_alias only in the alias-based variant, where you also bootstrap a write index flagged is_write_index: true. On a data-stream template, data_stream: {} plus index.lifecycle.name is the complete attachment.

← Back to Bootstrapping Index Templates & Data Streams · ILM Policy Design & Lifecycle Synchronization