New to Kestra?
Use blueprints to kickstart your first workflows.
Audit OpenSearch ISM policy attachment via the Explain API, alert on gaps, and optionally attach the expected policy.
An index created without an Index State Management (ISM) policy has no rollover, no retention, and no deletion lifecycle - it just grows until disk pressure or a cluster health alert forces someone to deal with it by hand. This blueprint reads OpenSearch's own ISM Explain API to check whether a target index actually has a policy attached, alerts on a gap or on an index that could not be reached, and can attach the expected policy with OpenSearch's own ISM Add Policy API - which, per OpenSearch's own documentation, never overwrites a policy an index already has.
periodic_policy_check (io.kestra.plugin.core.trigger.Schedule) runs every 6 hours, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.check_ism_status (io.kestra.plugin.opensearch.Request, GET _plugins/_ism/explain/{{ index_name }}) returns status (HTTP code) and response (the Explain API's JSON body, keyed by index name).evaluate_index_reachability (io.kestra.plugin.core.flow.If) branches on status == 200.else (UNREACHABLE): alert_index_unreachable reports the non-200 status - the index may not exist, or ISM may be unavailable.then (index reachable): nested evaluate_policy_attachment (io.kestra.plugin.core.flow.If) branches on whether response["{{ index_name }}"].policy_id, read with a dynamically built jq filter, is non-null.then (HEALTHY): log_policy_attached records the attached policy_id.else (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, attach_ism_policy calls POST _plugins/_ism/add/{{ index_name }} with {"policy_id": expected_policy_id}. alert_policy_gap always fires to Slack regardless of which remediation path ran.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails outright - an unreachable cluster or bad credentials must not read as "no gap".auto_remediate, dry_run) between "gap detected" and "policy actually attached".app-logs-*) where policy attachment is expected but not otherwise enforced.Request driving both a management-plane read (_ism/explain) and a management-plane write (_ism/add), not just document indexing or search.The ISM Explain API run by hand answers one moment in time; it does not decide a cadence, classify the result into a three-state risk level, gate a remediation behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any policy is attached, and an execution history that shows exactly when an index went ungoverned and whether it was fixed.
opensearchproject/opensearch distribution as a core plugin, unlike the optional opensearch-sql plugin some other blueprints in this repo depend on - but this has not been confirmed against a live container in this environment, so treat it as unverified until you run it.expected_policy_id (via PUT _plugins/_ism/policies/{policy_id}, not part of this flow) before auto_remediate is ever set to "true" - the Add Policy API attaches an existing policy by ID, it does not create one.Local testing:
docker run -d --name opensearch-ism-gate -p 9202:9200 -p 9602:9600 -e "discovery.type=single-node" -e "DISABLE_SECURITY_PLUGIN=true" opensearchproject/opensearch:2.11.0
docker network connect YOUR_KESTRA_NETWORK opensearch-ism-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set OPENSEARCH_HOST to http://opensearch-ism-gate:9200 from inside that network, or http://localhost:9202 from the host). With DISABLE_SECURITY_PLUGIN=true, OPENSEARCH_USERNAME/OPENSEARCH_PASSWORD can be set to any placeholder value - the cluster does not check them.
OPENSEARCH_HOST: OpenSearch connection.hosts entry, scheme and port included.OPENSEARCH_USERNAME / OPENSEARCH_PASSWORD: basicAuth credentials used by every io.kestra.plugin.opensearch.Request task in this flow.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_policy_gap, alert_index_unreachable, and the errors block.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_OPENSEARCH_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.index_name (STRING, default app-logs): index audited and, if remediated, updated.expected_policy_id (STRING, default app-logs-policy): policy_id expected to be attached, and applied on remediation.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for the ISM Add Policy API to actually run.outputs.check_ism_status.status / .response: the ISM Explain API's raw HTTP status and JSON body, every run.outputs.attach_ism_policy.status / .response: the ISM Add Policy API's raw HTTP status and JSON body, present only when remediation actually ran.PUT app-logs), and separately create a policy under app-logs-policy with PUT _plugins/_ism/policies/app-logs-policy.OPENSEARCH_HOST, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD, and SLACK_WEBHOOK_URL secrets.alert_policy_gap fires (AT_RISK: the index exists, no policy is attached yet).auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the attachment without calling the API.auto_remediate: "true" and dry_run: "false" and confirm attach_ism_policy runs, then verify with GET _plugins/_ism/explain/app-logs that policy_id is now set.index_name at a non-existent index and re-run to confirm alert_index_unreachable fires (UNREACHABLE) instead of either HEALTHY or AT_RISK.periodic_policy_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.index_name values (or an index pattern resolved with a prior Request to _cat/indices) with io.kestra.plugin.core.flow.Loop to run the same gate fleet-wide in one flow.enabled field to catch a policy that is attached but disabled, a different risk than no policy at all.show_policy=true query parameter on the Explain API to compare the currently applied policy's content against the latest version from the Get Policy API, catching stale-policy drift in addition to missing-policy drift.Request task's own connection and is caught by the errors block, not by evaluate_index_reachability - that branch only fires when the cluster answers but with a non-200 status, most commonly a 404 for a nonexistent index.AbstractLoad, AbstractSearch, AbstractTask, Bulk, Get, Load, Ppl, Put, Request, Scroll, Search): both the Explain read and the Add Policy write go through the same generic io.kestra.plugin.opensearch.Request task used elsewhere in this repo for _update_by_query.failed_indices with failures: true instead of actually changing anything - attach_ism_policy's own response output carries this, nothing in this flow parses or acts on failed_indices differently, so check outputs.attach_ism_policy.response directly if you need to confirm the attach actually took.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for the Add Policy API to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match._plugins/_ism/explain/{index} and _plugins/_ism/add/{index} endpoints and their exact JSON shapes are taken verbatim from OpenSearch's own ISM API documentation, and the Request task's properties (method, endpoint, parameters, body, connection) and outputs (status, response) are taken verbatim from the plugin's source on main - but no Docker container or Kestra engine was run to execute this flow end to end.