Moving from Elasticsearch to OpenSearch looks simple on paper. After all, OpenSearch is a fork of Elasticsearch and should work? However, making the switch without downtime or data loss can feel like a leap of faith. You know they are the same, but then in a way also not the same.
I was in exactly this situation for a small project. We had an existing dev Elasticsearch cluster, some indexes with real production data, and wanted to demonstrate that we can migrate fully to a completely fresh OpenSearch cluster. There were few clear requirements: no custom code needed, and very low downtime with maximum compatibility so the rest of the application didn’t need changing.
The obvious solution for this, and what we ended up using, was a simple snapshot and restore to an S3 bucket. This is available natively to both platforms, and worked exactly as needed in one go. In this post, I’m going to take you through the steps involved for us, from creating the snapshot repo to restoring it in OpenSearch.
I> You understand that I have used some other values in naming.
Tooling
I used a small open-source CLI called esops
(Elasticsearch / OpenSearch operations). This is way easier for me when
doing things manually in Kibana or via the Elasticsearch API. I
personally find this easier, as this cli tool is similar as what I do
with kubectl when doing “ops” work with Kubernetes. But
your miles may vary.
Snapshot registering
First we need to configure a repository in Elasticsearch with the S3 Bucket, this will allow us to create a snapshot on Elasticsearch and store it on the S3 Bucket.
$ esops --context dev-es001 snapshot repository register \
--repository backups --type s3 \
--setting bucket=poc-elasticsearch-opensearch \
--setting base_path=clusters/dev-es001/snapshots \
--setting region=us-east-1 \
--setting client=default
ACTION DRY_RUN REPOSITORY TYPE ACK
register false backups s3 trueThis will register the repository where we store the snapshots in an
S3 Bucket named poc-elasticsearch-opensearch, and store the
snapshots on a directory clusters/dev-es001/snapshots. This
S3 Bucket is located on us-east-1. Not shown with this
blog, but I already had this S3 Bucket available and had the
Elasticsearch (and also OpenSearch later) configured with access to the
S3 Bucket. Output suggest that register actions was successful. We can
verify that the backups repository exist in the
Elasticsearch cluster.
$ esops --context dev-es001 snapshot verify --repository backups
ACTION REPOSITORY NODES
verify backups 2And just to be sure, we don’t have any snapshots yet! Just checking.
$ esops --context dev-es001 snapshot list --repository backups
REPOSITORY NAME STATE INDICES SHARDS DURATIONGood, no snapshots. As the idea is to restore a snapshot from the S3
Bucket, we need to register the repository on the OpenSearch cluster
too. But we have to configure this as read only. It is a development
environment, but that doesn’t mean we have to configure it fully
writable. Using the same configuration options, like the
base_path and region, we use the S3 Bucket in the same was
as on Elasticsearch.
$ esops --context poc-os01 snapshot repository register \
--repository backups --type s3 \
--setting bucket=poc-elasticsearch-opensearch \
--setting base_path=clusters/dev-es001/snapshots \
--setting region=us-east-1 \
--setting client=default \
--setting readonly=true
ACTION DRY_RUN REPOSITORY TYPE ACK
register false backups s3 trueAnd once the register is completed, just validating that it actually exist.
$ esops --context poc-os01 snapshot verify --repository backups
ACTION REPOSITORY NODES
verify backups 1The development environment isn’t used that much, as the indexes doesn’t have a lot of data in it. And some days before we rebuild the development environment, otherwise we would have some more data in our cluster.
$ esops --context dev-es001 index list
NAME HEALTH STATUS PRI REP DOCS STORE
backlog-reports-2026.05.10 green open 1 1 820 194.5kb
backlog-reports-2026.05.11 green open 1 1 790 157.0kb
backlog-reports-2026.05.12 green open 1 1 845 166.7kbWe have 3 indexes with some data in it, lets verify that we don’t have any indices in our OpenSearch cluster.
$ esops --context poc-os01 index list
NAME HEALTH STATUS PRI REP DOCS STOREGood! Nothing exists (well, not the ones we will be restoring as there are some hidden indexes on it.). That means we are ready with our preparations and we can create the snapshot on the Elasticsearch cluster.
Snapshot creation
We will create a snapshot daily-2026.05.12 on our backup
repository (we created earlier) and specify all
backlog-reports-* indices. With the --wait we
will wait till the snapshot process is done and afterwards we get our
terminal back.
$ esops --context dev-es001 snapshot create \
--repository backups \
--snapshot daily-2026.05.12 \
--indices 'backlog-reports-*' --wait
ACTION DRY_RUN REPOSITORY SNAPSHOT ACCEPTED WAIT STATE
create false backups daily-2026.05.12 true false STARTED
snapshot backups/daily-2026.05.12 state=STARTED shards=0/3 (init=3 started=0 failed=0)
snapshot backups/daily-2026.05.12 state=SUCCESS shards=3/3 (init=0 started=0 failed=0)
ACTION DRY_RUN REPOSITORY SNAPSHOT ACCEPTED WAIT STATE
create false backups daily-2026.05.12 true false SUCCESSThat went quick, a few seconds. That is nice that our development environment doesn’t have a lot of data in it. Lets verify that the snapshot is a success.
$ esops --context dev-es001 snapshot list --repository backups
REPOSITORY NAME STATE INDICES SHARDS DURATION
backups daily-2026.05.12 SUCCESS 3 3/3 3.407sYes, snapshot went successful and it took indeed several seconds, 3.4 which is fine. In production environment it will be a lot longer. Now we have created our snapshot, let us check if the OpenSearch cluster is able to see that snapshot too.
Snapshot check
We use the esops migrate check command with the 2
cluster names to see if the snapshot from source Elasticsearch is able
to migrate the target OpenSearch.
$ esops migrate check --source dev-es001 --target poc-os01
SOURCE context=dev-es001 dialect=elasticsearch version=9.3.3 url=https://dev-es001.infra.local
TARGET context=poc-os01 dialect=opensearch version=3.6.0 url=https://poc-os01.infra.local:9200
STATUS CHECK DETAIL
OK source.connectivity dev-es001 reachable (elasticsearch 9.3.3)
OK target.connectivity poc-os01 reachable (opensearch 3.6.0)
WARN dialect cross-dialect elasticsearch → opensearch — security model, ILM/ISM, and pipelines do not migrate
WARN version downgrade elasticsearch 9.3.3 → opensearch 3.6.0 — index format compatibility is one-way; reind…
OK target.remote_whitelist dev-es001-01.infra.local:9200 allowed by target reindex.remote.allowlist (dev-es001-01.infra.local:9200, dev-es001-02.infra.local:9200)
SUMMARY status=WARN blockers=0 warnings=2In our OpenSearch cluster we had to add an extra configuration option
reindex.remote.allowlist with the hostnames of the nodes
that are part of the Elasticsearch cluster You also see that we do a
“downgrade”, from Elasticsearch 9.3.3 to OpenSearch 3.6.0. It is
debatable if this is a “downgrade”, that is why I put quotation marks on
“downgrade”. But the migrate check doesn’t show any
blockers, so lets see if we can plan the migration (sort of a
dry-run).
$ esops migrate plan --source dev-es001 --target poc-os01 \
--indices 'backlog-reports-2026.05.*'
SOURCE context=dev-es001 dialect=elasticsearch version=9.3.3
TARGET context=poc-os01 dialect=opensearch version=3.6.0
THROUGHPUT 5000 docs/sec
STATUS INDEX DOCS PRIMARY_STORE ISSUES EST_DURATION
OK backlog-reports-2026.05.10 820 103.5kb 0 0s
OK backlog-reports-2026.05.11 790 78.6kb 0 0s
OK backlog-reports-2026.05.12 845 83.4kb 0 0s
TOTALS indices=3 docs=2455 primary_store=265.5kb estimated_duration=0s
SUMMARY status=OK blockers=0 warnings=0The plan all looks fine and status=ok is what we want to
see. Now we can start the actual reindex, thus the snapshot will be
restored and made available in the OpenSearch cluster.
$ esops migrate reindex \
--source dev-es001 --target poc-os01 \
--indices 'backlog-reports-2026.05.*' \
--requests-per-second 5000
SOURCE context=dev-es001 dialect=elasticsearch version=9.3.3 url=https://dev-es001.infra.local
TARGET context=poc-os01 dialect=opensearch version=3.6.0 url=https://poc-os01.infra.local:9200
STATUS SOURCE TARGET DOCS DETAIL
OK backlog-reports-2026.05.10 backlog-reports-2026.05.10 820 created=820 updated=0 deleted=0
OK backlog-reports-2026.05.11 backlog-reports-2026.05.11 790 created=790 updated=0 deleted=0
OK backlog-reports-2026.05.12 backlog-reports-2026.05.12 845 created=845 updated=0 deleted=0
SUMMARY status=OK blockers=0 warnings=0That too went rather quick, but then again it wasn’t a lot of data.
With the migrate reindex command we see again the
status=OK in the output and I like that. What we can do is
to run the migrate verify to check again that the snapshot
restore was successful.
$ esops migrate verify \
--source dev-es001 --target poc-os01 \
--indices 'backlog-reports-2026.05.*'
SOURCE context=dev-es001 dialect=elasticsearch version=9.3.3
TARGET context=poc-os01 dialect=opensearch version=3.6.0
STATUS SOURCE TARGET SOURCE_DOCS TARGET_DOCS DIFF DETAIL
OK backlog-reports-2026.05.10 backlog-reports-2026.05.10 820 820 +0 -
OK backlog-reports-2026.05.11 backlog-reports-2026.05.11 790 790 +0 -
OK backlog-reports-2026.05.12 backlog-reports-2026.05.12 845 845 +0 -
SUMMARY status=OK blockers=0 warnings=0Yes, it really looks like that all the data was properly reindexed from Elasticsearch to OpenSearch. ## Result
As the migrate verify didn’t had any issues, means that
we now should see the indexes in our OpenSearch cluster.
$ esops --context poc-os01 index list
NAME HEALTH STATUS PRI REP DOCS STORE
backlog-reports-2026.05.10 green open 1 1 820 194.5kb
backlog-reports-2026.05.11 green open 1 1 790 157.0kb
backlog-reports-2026.05.12 green open 1 1 845 166.7kbYes! And with status green too, so that is good! So we have the same amount of documents and they also have similar size, so everything is completely migrated/reindexed. So looking back from the process, it is an easy way to migrate from Elasticsearch from OpenSearch.
Production considerations
In our light development environment, everything finished in seconds.
In production you should plan for: * Longer snapshot creation and
restore times (hours for large clusters) * Proper throttling
(--requests-per-second and max_bytes_per_sec)
* Matching or larger cluster sizing on the target side * Manual
migration of templates, ingest pipelines, or ILM/ISM policies (they
don’t transfer automatically) * A short parallel-run period with the old
cluster as a safety net (Just to be sure)
Final thoughs
To get back to the beginning of thus blog post, what started as an exercise of faith proved rather simple after all. By relying solely on the native S3 snapshot storage, we successfully migrated indexes from Elasticsearch into an entirely new OpenSearch cluster without any data loss. We have minimal downtime, and I think also very importantly, zero changes to anything else in the application.
After migrating, the indexes automatically had the green status in our light-dev setup. Of course, in production the procedure would have take longer (as it would be larger in size). In case you find yourself facing a similar task, there’s no need to overanalyze. Snapshot and reindex migration can be as foolproof as it gets, as long as you put enough verification steps in place.
May the force be with you!