Platform engineering · playbook

Elasticsearch v7 → v9. Multi-clúster. Plataforma viva. Cero regresión.

La migración corrió detrás de una utilidad shape-compare que diffeaba cada índice por servicio antes de cada cutover — la red de seguridad que hizo de "cero regresión" una meta realmente alcanzable. Esto es el playbook, no un case study de vendor.

v7 → v9

Salto de versión mayor

Multi-clúster

Topología

Por servicio

Granularidad del cutover

0

Regresiones

El problema

Muchas plataformas maduras acaban con una topología Elasticsearch multi-clúster — uno legacy en v7, un par nuevos en v9, uno o dos para productos específicos. El clúster legacy está EOL, bloqueando mejoras de query-performance, y nadie quiere ser quien empieza "la migración de ES" un viernes.

Las migraciones así son fáciles en papel ("solo reindexar") e infierno en la práctica. Los servicios se escriben contra la API del cliente Java v7 y hacen asunciones sutiles sobre la forma de la respuesta. Los schemas driftean con el tiempo — campos añadidos en v7.8 se comportan distinto en v9.0. Algunos servicios guardan su propio `mapping.json` y lo re-aplican al arrancar. Otros no.

La vara de "éxito" no es solo "v7 está fuera". Es: cada servicio que lee de Elasticsearch devuelve resultados idénticos a los del día anterior al cutover. Por cada endpoint. Sobre el dataset completo de producción. Es la única definición de hecho que importa.

El enfoque — tres pilares

Los cutovers big-bang fallan en silencio. Las migraciones por fases que se arrastran meses fallan ruidosamente. El punto dulce: cutover por-servicio con un pre-flight check que tiene que salir verde antes de movernos.

1. Un cliente v9 canónico

Un `ElasticV9Service` único que envuelve `@elastic/elasticsearch` v9 y expone la misma superficie de métodos que los servicios ya usaban contra v7. Dual-compatible durante la ventana de migración: si v9 devuelve un shape mismatch, el servicio puede caer a v7 para reads. Los writes siempre van primero a v9. Esto permite migrar reads per-endpoint, no per-servicio.

2. Utilidad shape-compare como red de seguridad

Antes de que ningún servicio flipera, un script Node corre ambos clústeres contra un set muestreado de queries y diffea la forma del documento de respuesta campo por campo. Cualquier diff != 0 bloquea el cutover hasta que (a) arreglamos el servicio, (b) arreglamos el mapping v9, o (c) documentamos un diff tolerado. Corre como job de CI en cada PR que toca código de Elasticsearch.

3. Per-servicio SPEC → BUILD → QA

Cada migración pasó por mi Forge harness: un documento SPEC definía el servicio, los endpoints, los diffs esperados y los criterios de aceptación. Build corría el shape-compare y el cambio de implementación. QA corría el test plan con el owner del servicio afectado — solo flipeábamos tras un score verde. Audit trail completo por migración.

Qué revisa realmente el shape-compare

La utilidad corre un set de queries representativas contra ambos clústeres, saca los top-N documentos de cada uno, y compara forma a nivel de campo — no valores. Los valores driftean constantemente; la forma es el invariante.

Presencia de campo

Todo campo top-level y anidado en el documento v7 debe existir en el documento v9. Campo faltante → cutover bloqueado con mensaje claro: "el campo X estaba en v7, falta en v9 para el índice Y".

Tipo de campo

v7 y v9 tratan `keyword` vs `text` distinto para algunos analyzers. Si un campo era `keyword` en v7 y `text` en v9, servicios downstream con filtros exactos se rompen en silencio. La herramienta marca cualquier diff de tipo.

Shape de arrays anidados

Los arrays profundamente anidados (events de Cosmos, audit trails, cualquier doc con `items[].attributes[].value`) son el campo de minas clásico. Si un campo anidado desaparece en v9 porque la ruta de ingesta drop-eaba strings vacíos, los servicios que iteran se rompen. El shape check lo captura antes de la primera query de usuario.

Diffs tolerados

Algunos diffs están bien — p. ej. v9 añadió una config `_source` excludes que suprimía un campo que nunca usamos. Los diffs tolerados van en una allowlist YAML por índice. Nada se ignora en silencio; todo cambio aceptado es explícito y revisable.

Fases de cutover (por servicio)

Un servicio a la vez. Reads flipean endpoint-por-endpoint. Writes dual-write hasta que todos los reads están en v9. Cada fase es reversible hasta el decommission final.

  1. Fase 0

    Shape-compare verde en cada índice que el servicio lee. SPEC firmado por el owner del servicio.

  2. Fase 1

    Dual-write activo: cada ruta de ingesta escribe a v7 Y v9. Los servicios siguen leyendo de v7.

  3. Fase 2

    Read cutover endpoint-por-endpoint. Un endpoint a la vez, con bake period de 24h cada uno. Cualquier regresión observada → flip-back inmediato.

  4. Fase 3

    Todos los reads en v9. Writes siguen dual. Observar 7 días. Validar analytics / dashboards downstream contra el clúster nuevo.

  5. Fase 4

    Writes dejan de ir a v7. El servicio está totalmente en v9.

  6. Fase 5

    Clúster v7 decomisionado por índice (no todo a la vez). Cualquier query path huérfano aparece como 404 en logs — arreglar, re-decomisionar.

Qué haría distinto

Escribir la utilidad shape-compare primero, no último

Inicialmente pensé "voy a ojear los índices". Dos días dentro del primer servicio estaba ojeando a las 2am. Shape-compare me llevó un día y ahorró semanas de depuración. Cualquier migración stateful sobre ~3+ servicios debería tener una desde el día uno.

La YAML de diffs tolerados es la documentación real

La allowlist de shape diffs aceptables se vuelve el documento más útil del proyecto. Es un registro machine-readable de cada decisión sobre qué cambio era OK. Yo-del-futuro lo re-lee cada vez que se toca un campo.

La reversibilidad es la feature

Cada fase tiene un camino de flip-back. El bake por-endpoint de la Fase 2 significa que realmente ejercitas el flip-back al menos una vez — en nuestro caso, un campo donde el `_source` de v9 filtraba distinto. Flip de 30 segundos, cero impacto a cliente. Con big-bang habría sido un rollback entero de clúster a las 3am.

No confíes en "funciona en mi laptop" para ES

Elasticsearch en dev mode usa defaults distintos a prod (réplicas, `refresh_interval`, settings por tier). El shape matches en dev pero no en prod. Shape-compare siempre corre contra staging — nunca local.

¿Migrando Elasticsearch (o cualquier servicio con estado)?

Hago consultoría de migraciones en sistemas de datos con estado: Elasticsearch v7/v8 → v9, upgrades mayores de Postgres con validación DR, migraciones de protocolo Kafka, hardening de Redis. El patrón es el mismo — red de seguridad shape-compare, SPEC → Build → QA por servicio, fases reversibles. Remoto, pagado — típicamente 3–8 semanas por migración.

Escríbeme a vikgm.dev@gmail.com

Kubernetes desde cero

La otra historia de infra dura: bootstrap Kubernetes reproducible en 10 scripts sobre bare metal con Cilium eBPF + ArgoCD GitOps + secretos Vault + Cloudflared Zero Trust.