928616 docs (artmone 2621 / epz 820604 / products 105922) migrated via reindex-from-remote. Pre-migration snapshot к kreknin repo, target stack 33 получил `reindex.remote.whitelist=elasticold.kzntsv.site:443` через Portainer API. Consumer configs (books-api + books-task-runner + books-job-scheduler) sed'd elasticold→elasticsearch.kzntsv.site, 3 containers restarted. Per-doc byte-match verified, internal smoke от books-api → новый ES 200 OK. Source ES оставлен running per user (rollback ready).
8.6 KiB
migrate-elasticsearch-to-books-vds
Goal
Переехать ES индексы с Windows host (elasticold.kzntsv.site, ES 7.10.1) на books VDS (elasticsearch.kzntsv.site, ES 7.10.0, single-node behind traefik basicAuth). Переключить books-api / books-task-runner / books-job-scheduler через default.json на новый endpoint. Source ES оставить running как rollback (не выключать).
Approach
Reindex-from-remote (не snapshot/restore).
Why:
- Source = Windows docker, нет
path.repoconfigured + нет bind для snapshot dir → snapshot подход требует restart source = consumer downtime. - Target = books VDS Portainer stack 33, modify env через Portainer API = recreate target only, consumers unaffected (они пока на source).
- Same Lucene 8.7.0 → docs reindex transparently через
_reindexAPI. - 730mb total → ~5 min over public network.
Trade-off: reindex использует target's dynamic mapping. Pre-create target indices с source mappings/settings (snapshot уже сохранён в .scratch/).
Source/target inventory
Source (Windows, localhost:9200, behind elasticold.kzntsv.site traefik basicAuth):
- ES 7.10.1, Lucene 8.7.0,
discovery.type=single-node - 3 indices:
artmone2621 docs, 1.4mbepz820604 docs, 699.7mbproducts105922 docs, 26.6mb
- Compose:
C:\Users\vitya\projects\docker\diskstation\elasticsearch\docker-compose.yml - Bind:
./data:/usr/share/elasticsearch/data. No snapshot path. - basicAuth user
books, hash$apr1$vyxr1l5z$fpxHmNBfAx8cHrQTje4Fx/
Target (books VDS 89.253.255.133, Portainer stack 33 elasticsearch, behind elasticsearch.kzntsv.site):
- ES 7.10.0, Lucene 8.7.0, single-node, yellow (replica unassigned — expected)
- 1 index:
read_me(4.8kb placeholder) path.repo=/snapshots, kreknin fs repo registered- Bind:
/usr/docker/elasticsearch/{data,snapshots}→/usr/share/elasticsearch/{data,/snapshots} - basicAuth user
books, same hash как source → same password
Consumers (all on books VDS, configs bind-mounted /opt/books/<svc>/config/default.json):
books-api(stack 23): port 3021, Nitrobooks-task-runner(stack 22, sibling of job-scheduler+mongo)books-job-scheduler(stack 22)
Все три имеют block:
"elasticsearch": {
"node": "https://elasticold.kzntsv.site/",
"auth": { "username": "books", "password": "<same>" }
}
Key files
C:\Users\vitya\projects\docker\diskstation\elasticsearch\docker-compose.yml— source ES compose (no edit planned)C:\Users\vitya\projects\.admin\.scratch\source-mappings.json— frozen source mappings 2026-05-25C:\Users\vitya\projects\.admin\.scratch\source-settings.json— frozen source settings 2026-05-25- books VDS
/opt/books/api/config/default.json:27— consumer config (idem task-runner, job-scheduler) - books VDS Portainer stack 33 — target ES compose (modify env via Portainer API)
- books VDS
/usr/docker/elasticsearch/snapshots/— kreknin fs repo, pre-migration backup target
Execution plan
- Pre-migration ES snapshot books VDS (kreknin repo,
pre-migration-<date>snapshot name) — instant rollback for target. - Save source mappings/settings — done (
.scratch/source-{mappings,settings}.json). - Modify target ES stack via Portainer API: add env
reindex.remote.whitelist=elasticold.kzntsv.site:443. Recreate stack (~10s target downtime; consumers unaffected — они на source). - Pre-create target indices с source mappings/settings (number_of_replicas=0 для single-node clean green).
- POST
/_reindexper index, wait_for_completion=false, slices=auto. Poll task status. - Verify counts: target
_cat/indicesdoc.counts == source. Verify random doc fetch by_idmatches. - Update consumer configs: replace
elasticold.kzntsv.site→elasticsearch.kzntsv.siteв/opt/books/{api,task-runner,job-scheduler}/config/default.json(sed in-place). - Bounce consumers через
docker restart books-api books-task-runner books-job-scheduler(mongo / db не трогаем — bind-mount уже видит новый config). - Smoke: tail
docker logs --since 60s books-api books-task-runner books-job-scheduler→ нет ES connection errors. Trigger known query → response from target. - Don't disable source (per user) — Windows ES + elasticold traefik route остаётся live для rollback.
Decisions log
- 2026-05-25: подход reindex-from-remote, не snapshot/restore. Reason: source compose не имеет
path.repoconfigured + bind для snapshot — это потребовало бы restart source (= consumer downtime). Target compose в Portainer, restart target не задевает consumers (они пока на source). Same Lucene 8.7.0 → reindex transparent. - 2026-05-25: pre-create target indices с frozen source mappings — иначе reindex использовал бы dynamic mapping, possibly losing exact analyzer config. Mappings/settings уже извлечены в
.scratch/. - 2026-05-25: consumer config update через
sed -i(bind-mounted files), restart throughdocker restart(не Portainer stack recreate) — mongo / db в той же stack id 22 не должны bounce.
Open questions
number_of_replicas: source = 1 (yellow на single-node). Target тоже single-node → set to 0 на pre-create для green. Source оставить как есть. Resolved: set 0 на target pre-create.
Completed steps
- Source ES probed: 3 indices, 730mb total, ES 7.10.1
- Target ES probed: ES 7.10.0, 1 placeholder index
read_me, no collisions - Source mappings/settings frozen в
.scratch/ - basicAuth password identified из consumer config (same on both sides)
- Containers inventory: 3 consumer containers, all on books VDS
- Pre-migration ES snapshot
pre-migration-2026-05-25в kreknin repo (read_me only, SUCCESS — rollback point) - Target ES compose updated через Portainer API:
reindex.remote.whitelist=elasticold.kzntsv.site:443. Container restart 07:51:39 UTC. Yellow cluster post-restart (replica unassigned = expected). - 3 target indices pre-created с frozen source mappings/settings,
number_of_replicas=0→ green. - Reindex from remote: artmone (2621/2621, 25s), epz (820604/820604, ~5m async), products (105922/105922, ~25m async).
- Doc count parity src=dst для всех 3 indices.
- Per-doc sample fetch: byte-match _id=29359 (products) + _id=22023500 (epz).
- Consumer configs sed in-place:
elasticold→elasticsearch, .bak-pre-es-migration-2026-05-25 saved для rollback. - Restart 3 containers: books-api running, books-task-runner healthy, books-job-scheduler healthy.
- Direct internal smoke (
docker exec books-api node -e ...): API seeshttps://elasticsearch.kzntsv.site/,_cluster/health200 OK. - Post-migration source traefik access log = 0 elasticold hits за 5 min.
Closure note (2026-05-25)
Migration complete. 3 indices (artmone/epz/products, 820k+108k+2.6k docs, 850mb total target storage) on books VDS ES live. Consumers switched (books-api + books-task-runner + books-job-scheduler).
Source НЕ disabled (per user requirement) — Windows ES + elasticold.kzntsv.site traefik route остаются live для emergency rollback. Кешированный data на windows host остаётся в одном экземпляре до отдельного decommission решения.
Rollback: ssh root@89.253.255.133 'for f in /opt/books/{api,task-runner,job-scheduler}/config/default.json; do cp "$f.bak-pre-es-migration-2026-05-25" "$f"; done; docker restart books-api books-task-runner books-job-scheduler'.
Future ops: при decommission Windows ES — docker stop elasticsearch на windows + remove traefik label elasticold.kzntsv.site route. path.repo / kreknin snapshot pre-migration-2026-05-25 уже не несёт практически данных (один read_me index), может быть удалён вместе с next backup retention rotation.
Notes
- Source basicAuth password в
/opt/books/api/config/default.json(consumer config). Не дублировать в эту таску. books-job-scheduler-mongo— другая БД, не задевается.- Rollback path:
sed -i 's/elasticsearch\.kzntsv\.site/elasticold.kzntsv.site/g' /opt/books/*/config/default.json+ restart consumers.