Skip to content

docs(opensearch): fix the ES-to-OpenSearch migration guide - #826

Open
SuJinpei wants to merge 2 commits into
mainfrom
docs/opensearch-migration-fixes
Open

docs(opensearch): fix the ES-to-OpenSearch migration guide#826
SuJinpei wants to merge 2 commits into
mainfrom
docs/opensearch-migration-fixes

Conversation

@SuJinpei

@SuJinpei SuJinpei commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

Fixes How_to_Migrate_from_Elasticsearch_to_OpenSearch.md. The guide was reviewed, then both migration paths were run end to end on a real cluster — ES 7.10.2 (3 nodes) → snapshot to S3 → OpenSearch 2.19.6 → reindex → upgrade to OpenSearch 3.3.1, plus ECK-deployed Elasticsearch 8.17.5 → reindex from remote. Everything below is either a defect that breaks a reader following the guide, or an explanation the run showed to be wrong.

Errors that break a reader following the guide verbatim

Problem Effect
s3.client.default.endpoint: "<http://minio.example.com:9000>" in 3 samples Markdown autolink brackets leaked into the YAML value — the S3 client is configured with an invalid host
repository-s3 URLs hardcoded to specific versions The guide itself says the plugin version must match the node exactly; opensearch-plugin install refuses a mismatch and the node will not start
Init container image from an internal registry Not pullable by customers
Keystore steps written as a single-pod action The keystore is per-node; snapshots fail on the pods that were skipped
"indices": "*" while the note below advises excluding system indices The command contradicts its own guidance
Mappings extracted with sed Silently drops index settings — an index using a custom analyzer cannot be recreated at all
OpenSearch upgraded to 3.3.1, Dashboards to 3.3.0 Mismatched pair
Phase 1's cluster resource cannot start Two separate omissions, each fatal — see below
version.created given as 7102099 for ES 7.10.2 The real value is 7100299; the reader never sees the documented number

Why Phase 1 could not start

  • No spec.bootstrap.pluginsList. Every general.additionalConfig entry is rendered onto the bootstrap pod too, and s3.client.* are only valid settings once repository-s3 is installed, so the bootstrap pod dies with unknown setting [s3.client.default.region]. The other nodes are pinned to it via cluster.initial_master_nodes, so the cluster never forms. The prerequisites section already showed the bootstrap: block — Phase 1 did not.
  • No admin password. OpenSearch 2.12+ exits with No custom admin password found. security.config.adminCredentialsSecret does not supply it — that is only the credential the Operator uses to reach the cluster — and the bootstrap pod has no env field, so it must go through additionalConfig.

Explanations corrected

  • Reindex from Remote was presented as ES 8.x-only. It also works from ES 7.10 and can target OpenSearch 3.x directly, removing the entire two-hop (verified: 5/5 documents from ES 7.10.2 straight into 3.3.1). Added a Choosing a Method section describing the real decision — copying index files vs re-indexing documents — including that reindex carries no settings, mappings or aliases.
  • The ES 8.x Lucene claim is unverified. The guide said ES 8.x snapshots are unreadable because of "a newer Lucene version with incompatible metadata protocols". Measured, ES 8.17.5 is on Lucene 9.12.0 and OpenSearch 2.19.6 on 9.12.3 — the same Lucene minor. An actual ES 8 snapshot restore was not tested, so rather than substitute another explanation the text now states the behaviour without asserting a cause.
  • additionalConfig becomes environment variables on every pod, not entries in an opensearch.yml ConfigMap. Corrected everywhere it was described.

Added

  • A Troubleshooting section for a node stuck in CrashLoopBackOff after an additionalConfig change: values there are not validated by the Operator, a bad key is only rejected at node boot, and the rollout halts at the first failed node while the rest keep serving the previous config.
  • A warning that reindex.remote.whitelist was removed in OpenSearch 3.x in favour of reindex.remote.allowlist — a real failure observed on a 3.3.1 cluster:
    SettingsException[unknown setting [reindex.remote.whitelist] please check that any
    required plugins are installed, or check the breaking changes documentation for
    removed settings]
    
  • What a restore does not carry over (index templates, ingest pipelines, ILM policies, users and roles, Kibana saved objects), replica counts on a smaller target cluster, and taking a snapshot before the major upgrade.
  • Relative links to the sibling OpenSearch guides, following this directory's convention.

Structure

The ES 7.10 path is renumbered Phase 0 / Phase 1 / Phase 2. Previously the source-side snapshot steps sat in an unnumbered Procedure section between Prerequisites and Phase 1, and step numbering restarted three times.

Notes for reviewers

  • English only. docs/zh is regenerated from the English source by the pipeline.
  • The example versions (7.10.2, 2.19.3, 3.3.1) are unchanged, but are now explicitly labelled as examples to substitute, with the command to read the running version. Aligning literal version numbers across the whole OpenSearch KB set is a separate change.
  • The admin-password route deserves a second opinion. Passing it through additionalConfig stores it in plain text in the cluster resource — the warning says so and points at securityConfigSecret for anything long-lived — but if there is a better supported route for OpenSearch 2.12+ on Operator 2.8.x, that belongs here instead.

Corrections to How_to_Migrate_from_Elasticsearch_to_OpenSearch.md, found by
reviewing it and then running both migration paths end to end against a real
ES 7.10.2 / OpenSearch 2.19.6 / OpenSearch 3.3.1 / Elasticsearch 8.17.5
setup.

Errors that break a reader following the guide verbatim:

- The S3 endpoint was written as "<http://minio.example.com:9000>" in three
  samples. The angle brackets are markdown autolink syntax that leaked into
  the YAML value, so the S3 client is pointed at an invalid host.
- The repository-s3 download URLs hardcoded versions, while the guide itself
  states the plugin version must match the node version exactly. Replaced
  with URL patterns plus the command that reads the running version.
- The Elasticsearch init container used an image from an internal registry
  that customers cannot pull.
- The keystore steps read as a single-pod action. The keystore is per-node
  and reload_secure_settings only reloads what is already on each node.
- The snapshot request used "indices": "*" while the note below it advised
  excluding system indices.
- The reindex step extracted mappings with sed, silently dropping the index
  settings, so any index with a custom analyzer could not be recreated.
- OpenSearch was upgraded to 3.3.1 but OpenSearch Dashboards to 3.3.0.
- The Phase 1 cluster resource could not start: it omitted
  spec.bootstrap.pluginsList (additionalConfig is rendered onto the bootstrap
  pod too, so s3.client.* is an unknown setting there without the plugin) and
  supplied no admin password (OpenSearch 2.12+ refuses to start without one,
  and the Operator has no dedicated field for it).
- The verification step cited 7102099 as the version.created of an ES 7.10.2
  index; the real value is 7100299. The check no longer names a literal,
  since the number varies with the patch release.

Corrections to the explanations:

- Reindex from Remote was presented as an ES 8.x-only method. It also works
  from ES 7.10 and can target OpenSearch 3.x directly, removing the need for
  an intermediate 2.x cluster. Added a "Choosing a Method" section covering
  the actual trade-off, including that reindex carries no settings, mappings
  or aliases.
- The ES 8.x section blamed the snapshot incompatibility on a newer Lucene
  version. Measured, ES 8.17.5 and OpenSearch 2.19.6 are on the same Lucene
  minor, so that is not the cause; the text now states the behaviour without
  asserting a reason.
- additionalConfig entries become environment variables on every pod, not
  entries in an opensearch.yml ConfigMap. Corrected everywhere it was
  described, including the troubleshooting step that told readers to grep
  ConfigMaps.

Additions:

- A Troubleshooting section for a node stuck in CrashLoopBackOff after an
  additionalConfig change, including that a bad key is only rejected at node
  boot and that the rollout halts at the first failed node.
- A warning that reindex.remote.whitelist was removed in OpenSearch 3.x in
  favour of reindex.remote.allowlist, which is a real failure seen on 3.3.1.
- Notes on what a restore does not carry over (index templates, ingest
  pipelines, ILM policies, users and roles, Kibana saved objects), replica
  counts on a smaller target, and taking a snapshot before a major upgrade.
- Relative links to the sibling OpenSearch guides.

The ES 7.10 path is renumbered Phase 0 / 1 / 2; the source-side snapshot
steps previously sat in an unnumbered section between the phases.

docs/zh is left untouched; it is regenerated from the English source by the
pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SuJinpei
SuJinpei force-pushed the docs/opensearch-migration-fixes branch from 4f55f43 to 818144e Compare August 7, 2026 09:58
@SuJinpei SuJinpei changed the title docs(opensearch): fix breaking errors in the ES-to-OpenSearch migration guide docs(opensearch): fix the ES-to-OpenSearch migration guide Aug 7, 2026
Follow-up to the ES-to-OpenSearch migration guide fixes:

- Init container image: replace the public `elasticsearch-oss:7.10.2` example
  with a placeholder plus the command to read the image off the running
  StatefulSet. The `-oss` distribution contradicts the `elastic` user used
  throughout the guide, and public registries are unreachable in air-gapped
  environments.
- Admin password: recommend the securityConfigSecret path documented in
  KB260100022 and scope `OPENSEARCH_INITIAL_ADMIN_PASSWORD` via
  `additionalConfig` to short-lived migration clusters, with the plain-text
  and Operator-version caveats spelled out.
- Scope the "additionalConfig becomes environment variables" claim to
  Operator 2.8.x and add the ConfigMap fallback check to Troubleshooting.
- reindex.remote.allowlist: add a step to verify the setting actually reached
  the nodes, since the Operator can drop it silently
  (opensearch-k8s-operator#883), plus the fallbacks when it does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@SuJinpei SuJinpei left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 0f90dfb. Thanks for picking this up — four of the five edits land real gaps and I'd take them as-is. Three things I'd like resolved before merge, flagged inline:

  1. The opensearch-k8s-operator#883 citation describes a failure mode that issue does not report. In #883 the env var was set correctly; the reporter passed a JSON array and OpenSearch parsed it into two mangled strings. The actionable warning that issue supports is "don't write a list-valued setting as an array" — which belongs next to the existing comma comment, not framed as the Operator dropping the entry.
  2. _nodes/settings?filter_path=**.reindex* is the one command in this commit I could not confirm returns anything. If it comes back empty on a correctly-configured cluster, the doc manufactures a false alarm on exactly the step it is trying to de-risk.
  3. "a different distribution produces a plugin the node refuses to load" — the plugin is a version-pinned zip and the node checks elasticsearch.version, which is identical between the -oss and default 7.10.2 images. The two real reasons (air-gap pullability, exact version match) are already stated elsewhere and both hold. This is the same class of defect as the Lucene rationale the first commit removed: an explanation that reads correctly but was never measured.

Plus two consistency points: the recommended security-config path is only in prose, never in the YAML people actually copy, and "short-lived migration cluster only" cannot be followed on the ES 7.10 path, where that cluster is upgraded in place and becomes production.

Good as-is, no changes needed: the admin-credentialsadmin-credentials-secret rename now matches KB260100022 §1.5 exactly and is the only occurrence in the file; the "read the image off the running StatefulSet" command and the air-gap rationale are the right fix for the -oss example; scoping the env-var rendering to Operator 2.8.x is the correct instinct; the relative link target and the #troubleshooting anchor both resolve; build is green.

Not from this commit, still open: the operand examples are pinned at 2.19.3 / 3.3.1 while the packaged component ships 2.19.6 / 3.7.0, so the exact-match plugin URLs break on an air-gapped cluster installed from the package. The "replace every occurrence with the versions your own clusters run" warning covers it in principle.


:::note
The above configuration only sets S3 configs for master nodes. If you have dedicated data nodes, add the same S3 config to `dataNodes` as well.
- The init container must use the **same Elasticsearch image your nodes already run** — the plugin is installed into a volume that the node container then mounts, and a different distribution (for example an `-oss` image against a cluster that runs the default distribution) produces a plugin the node refuses to load. Read the image off the running workload and paste that exact value:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism here is not supported by anything we have measured.

The plugin is a version-pinned zip fetched from artifacts.elastic.co (or the internal mirror), and the node validates elasticsearch.version from plugin-descriptor.properties. elasticsearch-oss:7.10.2 and elasticsearch:7.10.2 are the same version, so the installed plugin directory is identical — the init container is only supplying the elasticsearch-plugin binary. When I ran this end to end I used the default distribution throughout and never saw a load refusal, and I can't find a distribution-specific check that would produce one.

The two reasons that do hold are already in hand:

  • air-gap pullability — stated in the paragraph right below, and it's the strong one;
  • version must match exactly — already a :::warning further up.

Suggest dropping the "produces a plugin the node refuses to load" clause and keeping the rest:

The init container must use the same Elasticsearch image your nodes already run — the plugin is installed into a volume that the node container then mounts, and the plugin refuses to load on a node running any other version. Read the image off the running workload and paste that exact value:

The -oss example was worth removing for a separate reason worth keeping in the commit message but not the doc: that distribution has no security plugin, so it contradicts the elastic user this guide authenticates as everywhere.

:::warning Every change to `additionalConfig` or `pluginsList` restarts the whole cluster

- Both approaches trigger a **rolling restart of every node**, one at a time, to load the new configuration or plugin.
- With **Operator 2.8.x**, every entry under `additionalConfig` is rendered as an environment variable on **every** pod, including the transient bootstrap pod, and OpenSearch reads it as a setting. (Other Operator versions may render the same entries into a mounted `opensearch.yml` ConfigMap instead — check both when you verify a setting.) The Operator **does not validate these values**. An unknown or misspelled setting is only rejected when the node boots, and the node then fails to start — see [Troubleshooting](#troubleshooting).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoping this to Operator 2.8.x is right and I'd keep it.

The parenthetical is the part I'd cut. I checked, and I can't find a released Operator version that renders additionalConfig into a mounted opensearch.yml. The nearest thing is a maintainer proposal in opensearch-k8s-operator#883 ("IMO the best option forward is to rework how additionalConfig is provided … If we switch this to a configmap that gets mounted into the pods"). Presenting a proposal as current behaviour of some other version sends readers looking for a ConfigMap that has never existed — which is what the first commit deleted from Troubleshooting after verifying it on a live 2.8.0 cluster.

Suggest either naming the version that does this, or, if the hedge is worth keeping, marking it as upstream direction:

(Upstream is considering moving this to a mounted opensearch.yml ConfigMap — see opensearch-k8s-operator#883 — so re-check this on a newer Operator.)

Comment on lines +355 to +359
config:
# credentials the Operator itself uses to reach the cluster; the password must match
# the admin password the cluster is initialized with (see the warning below)
adminCredentialsSecret:
name: admin-credentials-secret

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prose below recommends the securityConfigSecret path, but this block — the thing readers actually copy — only implements the other one. There's no securityConfigSecret anywhere in the sample, so following the recommendation means reconstructing the whole block from KB260100022 by hand.

Suggest making both paths visible in the one artifact:

  security:
    config:
      # credentials the Operator itself uses to reach the cluster; the password must match
      # the admin password the cluster is initialized with (see the warning below)
      adminCredentialsSecret:
        name: admin-credentials-secret
      # Recommended: supply the full security configuration and drop the
      # OPENSEARCH_INITIAL_ADMIN_PASSWORD entry above - see KB260100022.
      # securityConfigSecret:
      #   name: securityconfig-secret

The secret names line up with KB260100022 §1.5 as written, so this is copy-paste compatible with that guide.

Comment on lines +378 to +385
- **Short-lived migration cluster only — `OPENSEARCH_INITIAL_ADMIN_PASSWORD` through `additionalConfig`.** With Operator 2.8.x the bootstrap pod has no `env` field of its own, so the only way to reach it is `general.additionalConfig`, which that Operator version turns into environment variables on every pod. Two caveats: the value is stored **in plain text** in the cluster resource, and the trick depends on the env-var rendering — on an Operator version that writes `additionalConfig` into `opensearch.yml` instead, this key becomes an unknown setting and every node fails to start exactly as described in [Troubleshooting](#troubleshooting). Verify it landed as an environment variable before relying on it:

```bash
kubectl get sts -n <namespace> <cluster-name>-<nodepool> \
-o jsonpath='{.spec.template.spec.containers[0].env}' | tr ',' '\n' | grep OPENSEARCH_INITIAL_ADMIN_PASSWORD
```

Delete the migration cluster, or rotate the password through the security configuration, once the migration is finished.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Short-lived migration cluster only" and "Delete the migration cluster … once the migration is finished" don't hold on this path.

The OpenSearch 2.x cluster deployed here is upgraded in place to 3.x in Phase 2 Step 2 and becomes the production cluster — deleting it is not one of the reader's options. So a reader who follows this section as written ends up with a plain-text admin password in a long-lived production resource, which is the outcome the caveat is trying to prevent. The "or rotate the password through the security configuration" half is the only applicable branch, and it's buried second.

Suggest replacing the closing line with something the ES 7.10 reader can act on:

On the ES 7.10 path this cluster is upgraded in place in Phase 2 and becomes your production cluster, so rotate the password through the security configuration (KB260100022) before you finish. Only delete it if you took the reindex-from-remote route into a separate target.

The verification command in this bullet is a good addition — keep it.

additionalConfig:
# Allow connections to ES 8.x host (OpenSearch 3.x uses 'allowlist')
# Allow connections to the ES 8.x host. Host and port only - no http:// or https:// prefix.
# Separate multiple hosts with commas.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is correct and is where the #883 reference actually belongs.

That issue is a value-format bug, not a propagation bug: the reporter wrote reindex.remote.allowlist: '["host-a", "host-b"]' and OpenSearch parsed it into two mangled entries ("[\"host-a\"", "\"host-b\"]"). Writing a list-valued setting as a YAML/JSON array is the natural thing to try, and it is what actually bites people — so it deserves an explicit "don't":

      # Allow connections to the ES 8.x host. Host and port only - no http:// or https:// prefix.
      # Separate multiple hosts with commas. Do not write this as a YAML or JSON list:
      # additionalConfig values are passed through as plain strings, and an array is
      # parsed into mangled entries (opensearch-k8s-operator#883).
      reindex.remote.allowlist: "es8-cluster-host:9200"

The single-host form shown here is the one I confirmed working, as is a comma-separated pair.

Comment on lines +604 to +610
**Confirm the setting actually took effect** once the rollout finishes. A misspelled setting crashes the node loudly, but a setting the Operator fails to propagate is silent — the nodes stay healthy and the reindex request later fails with `[es8-cluster-host:9200] not allowlisted in reindex.remote.allowlist`:

```bash
curl -k -u "admin:<password>" "https://localhost:9200/_nodes/settings?filter_path=**.reindex*"
```

Every node must report the allowlist. If the response is empty, the entry did not reach the nodes ([opensearch-k8s-operator#883](https://github.com/opensearch-project/opensearch-k8s-operator/issues/883) tracks this); set it on `nodePools[].additionalConfig` instead, or bake it into an `opensearch.yml` in a custom image, and check again before continuing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two problems in this paragraph — the underlying idea (verify before you rely on it) is good and worth keeping.

1. #883 is misattributed. That issue does not report a setting failing to reach the nodes. The reporter's own follow-up is explicit: "the env var is set properly (and must exist as a string)" — the value arrived, and it was the JSON-array formatting that broke it. The maintainer response confirms the design issue is that additionalConfig values are untyped strings passed through env vars, not that entries get dropped. So "the entry did not reach the nodes (#883 tracks this)" describes a failure mode nobody has reported, and the suggested fallback — nodePools[].additionalConfig — goes through the same env-var rendering and would not have helped that reporter. I'd re-aim the paragraph at "verify it parsed the way you expect" and move the #883 citation up to the comma comment on the sample.

2. The command is unverified. _nodes/settings?filter_path=**.reindex* is not demonstrated anywhere I can find. The form shown working in #883 — twice, by two different people — is:

curl -k -u "admin:<password>" "https://localhost:9200/_cluster/settings?include_defaults=true&filter_path=**.reindex.remote.allowlist"

_nodes/settings should surface -E-injected settings and has the real advantage of being per-node, which matches the "every node must report" instruction — so if you can run it once against a cluster where the setting is applied and confirm it returns the value, keep it. If not, use the _cluster/settings form. An empty response from a command that never reports on a correctly-configured cluster turns this into a false alarm on the exact step it's meant to de-risk.

Comment on lines +696 to +698
# Other Operator versions render the same entries into a mounted opensearch.yml instead.
# If the setting is not in the StatefulSet env, look for it in the cluster's ConfigMaps:
kubectl get cm -n <namespace> -o yaml | grep '<setting-name>'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This puts back the kubectl get cm | grep step the first commit removed. That removal wasn't a style call — I checked on a live Operator 2.8.0 cluster and there is no opensearch.yml ConfigMap at all; the only ConfigMap in the namespace containing an s3.client.* key was one I had hand-written myself. So on the version this product ships, step 3's fallback always finds nothing, and a reader debugging a CrashLooping node spends the step learning that.

Same ask as the other version-hedge: if there is a released Operator that does this, name it; otherwise drop these three lines, or mark the ConfigMap as upstream direction rather than an alternative to check today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants