← blogENhome

AWS · Bases de datos · 2026-09-03 · 6 min de lectura

El typo que ensayó una migración de producción

El MySQL de producción de un cliente tenía que mudarse — cluster nuevo, cutover escalonado servicio por servicio, nada de big bang. Las migraciones de bases de producción son esa clase de cambio donde el plan vale exactamente lo que valen las partes que ensayaste. Así que ensayamos todo, en tres niveles de fidelidad creciente, antes de tocar producción.

El tercer nivel venía con regalo: el cluster Aurora compartido de dev había sido creado, hacía tiempo, con un typo en el nombre — stagging. Renombrar un cluster de Aurora como corresponde implica crear uno nuevo y mudar todo… que es exactamente una migración. Así que el fix del typo se convirtió en el ensayo general: mismo procedimiento, mismo tooling, aplicaciones reales, datos reales, y un resultado útil incluso si el ensayo fuera lo único logrado.

Nivel 1: un lab en Docker que rompe cosas a propósito

El diseño de la migración es replicación binlog con GTID auto-positioning, más filtros de replicación para cortar schema por schema: cuando el servicio de un schema se muda al cluster nuevo, un filtro lo blinda de las escrituras residuales que siguen llegando del lado viejo. Un ensayo end-to-end anterior había validado el canal de replicación en sí (snapshot cifrado cross-account, GTID auto-position, CDC con lag cero) — pero no los filtros. Y los filtros son lo que hace viable una migración escalonada.

Dos contenedores MySQL 8.0.46 (GTID activo, binlog ROW), cero infraestructura del cliente, cero costo, 30–45 minutos por corrida. Lo que salió de ahí:

El lab también reproduce los modos de falla a propósito, con su recuperación: una escritura sin filtrar del origen pisando en silencio una fila del destino (sin error, sin alarma — el más aterrador); error 1032 cuando el destino borró la fila; 1062 por colisión de clave primaria — y estos dos últimos detienen el applier para todos los schemas, incluidos los no migrados; y 1396 cuando los dos lados crean el mismo usuario (CREATE USER IF NOT EXISTS es la vacuna). Seis ejercicios de error deliberado entraron al runbook, cada uno con la salida real esperada de cada comando. Si la persona que ejecuta el procedimiento del día D ya vio el error 1062 y lo arregló, deja de ser un incidente — es un ítem de checklist.

Nivel 2: las preguntas que solo Aurora puede responder

Un lab en Docker no puede decirte cómo se comportan los parameter groups de Aurora. Quedaban dos cosas abiertas, y cualquiera de las dos podía cambiar el procedimiento del día D: ¿replicate-wild-ignore-table con ApplyMethod=immediate toma efecto sin reboot (filtros de replicación en Aurora)? ¿Y el parameter group valida la sintaxis del filtro como lo hace el motor por la vía SQL?

Para eso era el ensayo general.

Nivel 3: el ensayo general (gracias, typo)

El procedimiento de producción completo, corrido contra el cluster mal nombrado como origen y un cluster nuevo bien nombrado como destino — con los ~20 servicios reales que lo usan conectados todo el tiempo.

Preparar el origen. Binlog ROW más la escalera GTID online (enforce_gtid_consistency=ONgtid_mode=ON_PERMISSIVEON, según documenta MySQL) — con las aplicaciones conectadas. Cuatro reinicios, medidos: 10 s, 9 s, 8 s, ~8 s; todas las apps se reconectaron solas. Ahora el plan de producción dice "cuatro reinicios de menos de diez segundos cada uno", con evidencia, en lugar de "algunos reinicios".

Destino y replicación. Cluster nuevo creado vía Terragrunt desde el snapshot post-GTID (restore ~25 min); replicación binlog con auto-position enganchada, lag 0 sostenido durante toda la convivencia.

Las respuestas de Aurora. ApplyMethod=immediate sobre el parámetro del filtro toma efecto sin reboot — el cluster nunca salió de in-sync. Hallazgo extra: RDS combina su filtro interno mysql.% con el tuyo en lugar de reemplazarlo. Y el blindaje aguantó sobre Aurora real: con el filtro activo, el origen no pudo pisar filas del destino con nada de lo que le tiramos.

El gotcha que nadie tenía en la lista. El security group del destino no tenía regla de egreso — Terraform elimina el egress permisivo por defecto salvo que lo re-declares — y el hilo de IO de la replicación se quedó en Connecting, sin ningún error visible en ningún lado. Un cuelgue silencioso e indefinido como único síntoma de una omisión de una línea en el SG. Eso hoy es un check de pre-vuelo en el runbook de producción.

El cutover: 15 minutos de punta a punta.

congelar 21 servicios ECS (desired=0)          ~2 min
drenaje: WAIT_FOR_EXECUTED_GTID_SET            = 0 al instante
integridad: 772 tablas / ~93 GiB
  conteo exacto de filas en ambos lados,
  en paralelo                                  ~5,5 min → diff idéntico
repuntar: 19 secrets de Secrets Manager        58 s  (rollback: AWSPREVIOUS)
servicios arriba y estables                    2 min 32 s
filtro definitivo multi-schema, sin reboot     37 s

Conexiones verificadas en el destino, cero en el origen.

Un teardown que termina el trabajo. Canal de replicación detenido y reseteado (destino standalone), snapshot final tomado, y el cluster mal nombrado destruido vía Terragrunt — siete recursos, dejó de facturar. Un ensayo que deja el cluster viejo corriendo "por las dudas" no está terminado; es una fuga de costo con valor sentimental.

Qué se compró con esto

El plan de migración de producción ahora contiene cero pasos sin ensayar. Cada duración que figura — reinicios, restore, drenaje, validación por conteo, rotación de secrets, recuperación de servicios — es una medición de esta corrida, no una estimación. Y las partes que asustan (el pisado silencioso de datos, la trampa del filtro-antes-de-drenar, el cuelgue del SG) ya pasaron todas una vez, sobre fierros que nadie lloraría, con la recuperación escrita.

Lecciones

AuroraMySQLGTIDbinlogmigracionesTerragrunt