Schedule icon
Request icon
Script icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Assert icon
Sequential icon

Repair a rollover write alias and prove where writes land

Detect an OpenSearch write alias with no write index, repair the flags behind an approval then prove a canary lands in the intended index.

Categories
DataInfrastructure

Safety

An alias is how applications address a rollover index family. When no member carries is_write_index, OpenSearch answers every write through the alias with no write index is defined and ingestion stops. When the flag sits on an older member, writes succeed in the wrong index and nobody notices until a retention job deletes them. Both states are fixed with one POST _aliases call, which is exactly why doing it by hand is risky: an alias action body replaces the whole alias definition on that index, so a hand written batch silently drops the filter, the routing or the hidden flag that was there. It can also move writes to an index that is not the one you meant. So this flow refuses by default and each guard below is load bearing. Every one of them was proven by its own execution.

Guard What it stops
One exact alias_name, expected_index and complete member_indices, no wildcard, no comma, no leading dot the endpoints the flow builds widening to indexes you did not name
expected_uuids_json compared with settings.index.uuid adopting an index that was deleted then recreated under the same name
The membership the cluster returns has to equal member_indices writing to an index that joined the alias unnoticed
Alias options outside the known set are refused dropping an option the flow cannot carry forward
Members have to share the mappings of expected_index treating a canary accepted by one member as evidence for another
max_documents ceiling, exact total hits, no shard failures approving a plan built on a truncated document snapshot
A one member alias with no flag at all counts as healthy moving an implicit write target that already works
dry_run true, auto_remediate false, Pause read as onResume.approved ?? false three separate gates before one write
Recheck of UUIDs, every alias option and every document after the approval acting on a plan that went stale while a person was deciding
One atomic _aliases batch whose actions are rebuilt from the recheck an alias left halfway between two write indexes
Only is_write_index is changed, membership is never added or removed changing which indexes the alias covers
A canary written through the alias with require_alias, read back from expected_index a 200 acknowledged being reported as a working write path
Cleanup deletes only this execution's canary id under if_seq_no and if_primary_term deleting a document another run wrote
A final document comparison against the approved snapshot a canary left behind or an existing document disturbed
concurrency: limit 1 with QUEUE two executions repairing the same alias at the same time

How it works

  1. read_members (io.kestra.plugin.opensearch.Request) sends one GET to the explicit comma separated member list. That single response carries the settings, the mappings and every alias with every alias option, which is the whole detector input. read_documents reads every document with its version and routing, with track_total_hits.
  2. inspect (io.kestra.plugin.scripts.python.Script) validates the declared scope, compares the index UUIDs, refuses unknown alias options, mismatched mappings and a truncated or oversized document set, then decides the verdict. Degraded means no member carries is_write_index: true or the member that does is not expected_index. A one member alias with no flag at all routes writes implicitly and is healthy. The planned actions copy each member's filter, index_routing, search_routing and is_hidden forward and change nothing but the write flag. The task emits the full snapshot, which is what the approval is bound to.
  3. log_findings (io.kestra.plugin.core.log.Log) prints the verdict with the per member flag and the options being carried forward. handle_degraded (io.kestra.plugin.core.flow.If) stops here when the alias is healthy, through log_healthy.
  4. notify_findings (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the finding before the pause. route_remediation (If) requires auto_remediate true and dry_run false together, otherwise log_report_only ends the run and the exact action set stays in the inspect log.
  5. approve_repair (io.kestra.plugin.core.flow.Pause) waits up to an hour. apply_if_approved reads onResume.approved ?? false, so a resume that answers nothing is a refusal and log_declined records it.
  6. recheck_members and recheck_documents read the cluster again. recheck_plan compares it with the approved snapshot and exits non-zero on any difference, which fails the run before a single write. The actions that go to OpenSearch are rebuilt from this read, not from the approved copy.
  7. repair_alias sends one POST _aliases batch with timeout and master_timeout. repair_acknowledged (io.kestra.plugin.core.execution.Assert) fails the run when the cluster did not acknowledge it.
  8. prove_with_canary (io.kestra.plugin.core.flow.Sequential) writes one document through the alias with require_alias=true and an id derived from execution.id (canary_write), reads it back straight from expected_index with the alias write routing (canary_read), reads the alias state again (verify_members) then verify_repair exits non-zero unless the alias matches the approved plan, unrelated aliases on the same members are untouched, the canary was created in and read from expected_index with the source it was written with and its sequence number did not move.
  9. The finally block runs on every path. cleanup_canary (If) deletes the canary only when this execution created it, guarded by if_seq_no and if_primary_term, then verify_cleanup fails the run unless the document set is byte for byte the pre-repair snapshot.
  10. errors.alert_failure posts any failure to Slack with the alias name, the execution id and the canary id to look for.

The outputs worth reading afterwards: {{ outputs.inspect.vars.report }} is the verdict, {{ outputs.inspect.vars.snapshot }} is the full state the approval is bound to, {{ outputs.recheck_plan.vars.actions }} is the exact alias batch that was sent and {{ outputs.verify_repair.vars.canaryIndex }} is the index a real write through the alias reached.

What you get

  • Ingestion restored on a rollover family whose alias lost its write index, without a hand written alias body.
  • Writes moved off the wrong member, with the filter, routing and hidden options of every member preserved.
  • Proof by a real write, not by metadata. A blocked, closed or mapping conflicted target makes the run fail even though the flag reads back correctly.
  • Existing documents, their versions and routing, plus unrelated aliases on the same indexes, all left as they were.

Who it's for

Teams running OpenSearch rollover index families, log and metric pipelines in particular, where an interrupted rollover, a half applied alias edit or an ISM action can leave the write alias with no write index or the wrong one.

Why orchestrate this with Kestra

The manual fix is a curl -XPOST _aliases body pasted together during an incident, with no record of what the alias looked like before, no check that the person is still looking at current state and no proof that writes work afterwards. As a flow the detection, the Slack finding, the approval, the recheck, the atomic write, the canary verdict and the cleanup are one audited execution. The Pause is what makes a change this sharp safe to automate: a person decides with the per member flags in front of them. The plan is then rebuilt from a fresh read after they answer. OpenSearch applies an alias batch atomically but it has no compare and set, so re-reading inside the same execution is the only way to avoid overwriting a change made in the meantime.

Tested end to end

Kestra 2.0.5, OpenSearch plugin 1.6.1, OpenSearch 2.19.6 (build hash 97d3c13bf22a) in Docker. Flow validate on stock Kestra 2.0.5 was clean: no constraint, no warning, no deprecation. The fixture is two indexes of three documents each behind one alias that carries a filter, index routing, search routing and is_hidden, plus a second unrelated alias on the older member and a separate one member alias. Every state below was read with the OpenSearch REST API directly, never from a flow output. The operational payoff was measured by an independent PUT <alias>/_create/...?require_alias=true through the alias before and after the repair.

Case Execution Result
Healthy rAaF4JvU9WI9uGyacZI46 SUCCESS. The expected member already held the flag, no write task ran
One member alias, no flag (lookalike) 60aqPTacuGHhTkr4srqqjh SUCCESS. Spared as healthy, the approval was never offered
One member alias, is_write_index: false (boundary) 7ZWoEWCPmD3S5j6Hf5mOOC SUCCESS. Reported degraded, nothing written
Wrong write index 4DVQJ7iWFTXHG6t8wioARE SUCCESS. Named logs-000001 as the holder against an expected logs-000002, no write
Degraded dry run 2OPzSrJIATijAnVwagL39C SUCCESS. Printed the exact action set, the alias still had no write index afterwards
Declined approval 5qqQhjMELO7zeBnqsy95u SUCCESS. Resumed with approved: false, alias untouched
Remediation 1BWkrJRO1HaxXGWrPGldQU SUCCESS. An independent write through the alias went from HTTP 400 no write index is defined to HTTP 201 landing in logs-000002. The canary was created in logs-000002 at sequence number 7, read back identical, deleted again. All 6 documents, their versions and routing unchanged, the unrelated alias unchanged, both filters carried forward
Repeat 5V2Do5aWiNT4xOBNXAtiYF SUCCESS. Took the healthy exit, sent no second alias batch and wrote no second canary
Ineffective write 24lThDJGr8EOU2GLM5HSzP FAILED as intended. With index.blocks.write on the target, the alias batch still returned acknowledged: true and the flag read back correct, then the canary was refused with 403 cluster_block_exception and the run failed
Change during the pause 7P3NlMQpG2DiXpZkADJKd7 FAILED as intended. An administrator changed the alias filter from term to terms while the approval waited. recheck_plan named the drift and refused before any write, so the new filter survived
UUID mismatch 6A7FC1hMextnICwl7by3Pi FAILED as intended. A declared UUID that no longer matched stopped the run at inspect, before any read of documents mattered

All eleven ran on the final revision. A second fixture on a fresh container repeated the three that matter: healthy 4LpPtNPsxntbvjwyCEreXK, remediation 3tj3t3Xy1t7FA6t1L620N2 with the same 400 to 201 and 6 conserved documents, then the pause race 4bGn8JQs4v7Nt2skr7bDuI refused again. Slack was a local HTTP sink that recorded both the finding and the failure alerts.

Prerequisites

  • OpenSearch 2.19 reachable from the Kestra worker. Tested on 2.19.6 only.
  • Kestra 2.0.5 with the OpenSearch and Slack plugins plus the Docker task runner for the Python steps.
  • Minimum permissions on the named members only: indices:admin/get and indices:data/read/search to detect, indices:admin/aliases to repair, indices:data/write/index and indices:data/write/delete for the one canary. No cluster manage right is needed.
  • A Slack incoming webhook.

Secrets

Secret What it is
OPENSEARCH_HOST cluster URL the worker can reach, for example http://opensearch:9200
OPENSEARCH_USERNAME account with the rights above
OPENSEARCH_PASSWORD its password
SLACK_WEBHOOK_URL incoming webhook for the finding and the failure alert

Inputs

Input Type Default What it does
alias_name STRING none the one alias the flow may write to
expected_index STRING none the member that should receive writes
member_indices ARRAY none complete expected membership, 1 to 10 names
expected_uuids_json STRING none member name to index UUID map you intend to act on
canary_source_json STRING {"kestra_alias_probe": true} the document written through the alias to prove the destination
max_documents INT 100 ceiling for the before and after document comparison
dry_run BOOL true report the actions without sending them
auto_remediate BOOL false allow a repair to be considered at all

Quick start

  1. Add the four secrets to your Kestra instance.
  2. Read your own scope: GET _cat/aliases/<alias>?v for the membership and GET <members>?filter_path=**.uuid for the UUIDs.
  3. Execute with the defaults. dry_run true and auto_remediate false report the per member flags plus the exact alias batch a repair would send. Nothing is written.
  4. Check that expected_index is really where the next documents belong. If someone moved the flag on purpose, stop here.
  5. Stop other alias administrators for the next minute, then execute with dry_run: false and auto_remediate: true. The execution pauses.
  6. Approve it. The flow rechecks, sends one atomic batch, writes a canary through the alias, verifies where it landed, removes it then proves your documents are untouched.
  7. Enable the daily_check schedule once you trust the report. It forces report-only mode and ships with a placeholder scope to replace.

How to extend

  • Point canary_source_json at a document shaped like your own data, so the mappings are exercised by the probe.
  • Run one execution per alias family from a parent flow, so each keeps its own approval, recheck and canary.
  • Send the finding to PagerDuty instead of Slack when a stalled write alias means dropped ingestion.
  • Keep a copy of {{ outputs.inspect.vars.snapshot }} in your incident record: it is the alias definition before the change, which is what a rollback needs.
  • Pair it with an ISM policy review when the same alias loses its write index repeatedly, since the rollover action is usually the cause.

Limits: the flow changes is_write_index only. It never adds or removes alias membership, never creates an index and never rolls over, so an alias whose newest index does not exist yet is out of scope, as is an alias with no members. It refuses rather than acts when membership, a UUID, the mappings or an alias option is not what you declared. The document conservation proof needs a quiet family under max_documents: with live writers the counts move on their own and the run fails on its own evidence. The recheck closes the window from approval to write but it is not a lock, so another administrator can still change the alias a moment later. Tested against OpenSearch 2.19.6 with the security plugin disabled and basic auth in the connection block. Elasticsearch was not tested.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.