Cambiar de bucket sin cortar el servicio, con discos read-through
Laravel 13.26 trae un driver de disco que lee de dos sitios y va copiando por el camino. Migrar de S3 a R2 deja de ser una noche en vela.
Migrar los ficheros de una aplicación de un bucket a otro es de esas tareas que nadie quiere que le toquen. El código es fácil; lo incómodo es el orden. Copias todo, y mientras copias la gente sigue subiendo cosas. Cambias la configuración, y descubres que faltaban tres carpetas. Y en medio, alguien pide una factura de 2019 que se quedó atrás.
La salida clásica es escribir en los dos discos a la vez durante unas semanas,
con un if repartido por media aplicación que luego nadie se atreve a quitar.
Laravel 13.26 trae otra forma: un driver de disco que lee de dos sitios y va mudando los ficheros solo, según se piden.
Cómo se configura
Es un disco más en config/filesystems.php, salvo que en vez de apuntar a un
sitio apunta a dos:
'assets' => [
'driver' => 'read-through',
'primary' => 's3',
'fallback' => 'legacy-s3',
],primary es el destino, donde quieres acabar. fallback es el bucket viejo,
del que aún cuelgan cosas. A partir de ahí tu código no cambia: sigues llamando
a Storage::disk('assets') como siempre.
Cuando alguien pide un fichero, Laravel mira primero en s3. Si no está pero sí
en legacy-s3, lo sirve desde el viejo y lo copia al nuevo para la próxima
vez. La migración la va haciendo el tráfico real, fichero a fichero, sin script
ni ventana de mantenimiento.
Qué va a cada disco
Esta parte conviene tenerla clara antes de tocar producción, porque no todo se comporta igual:
- Lecturas: primero el primario; si no está, el de reserva, y se copia al primario.
- Escrituras: solo al primario. Lo nuevo nace ya en el sitio bueno.
- Listados de directorios: solo el primario.
- Existencia y metadatos: valen los dos discos, y aquí no se copia nada.
Ese exists() que mira los dos sin copiar es un detalle fino y agradecido: no
quieres que comprobar si un fichero existe te dispare una transferencia entre
buckets.
El fallo que no te tumba la petición
¿Y si la copia al disco nuevo falla? Igual el bucket está lleno, o las credenciales de escritura no son las que creías.
Por defecto Laravel se calla y sirve el fichero igual. La lectura funciona, que es lo que le importa a quien está esperando; simplemente ese fichero seguirá en el bucket viejo y se reintentará la próxima vez.
Cuando estés migrando de verdad querrás lo contrario, porque un fallo silencioso significa que la mudanza no avanza y no te enteras:
'assets' => [
'driver' => 'read-through',
'primary' => 's3',
'fallback' => 'legacy-s3',
'throw_on_promotion_failure' => true,
],Mi consejo: activarlo en cuanto empieces, verlo unos días con los errores a la vista, y decidir después si prefieres el silencio.
Dónde está la trampa
Esto no es una migración completa, y conviene decirlo claro: solo se muda lo que alguien pide. Los ficheros que nadie toca —el 90% de un bucket con años— se quedan donde están para siempre. El disco read-through te quita la urgencia y el corte de servicio, pero al final vas a necesitar igualmente un barrido que copie el resto antes de poder apagar el bucket viejo.
Y ojo con los listados. Si en algún sitio recorres directorios con files() o
allFiles(), esas llamadas solo ven el primario: lo que siga en el bucket
viejo no aparecerá. Si tienes un panel de administración que lista una carpeta,
o un comando que recorre ficheros para generar un informe, revísalo antes de
poner esto en marcha. Es el tipo de fallo que no revienta nada y que descubres
tres semanas después, cuando alguien pregunta por qué faltan documentos en una
pantalla.
Y para un homelab
El caso obvio es S3 a R2 para dejar de pagar salida de datos, pero funciona igual con un MinIO en casa: dejas tu almacenamiento propio como primario y el bucket de pago como reserva, y la factura va bajando sola conforme el tráfico se va llevando los ficheros a tu servidor. Lo bueno es que si un día el trasto de casa se cae, los ficheros que aún no habían migrado siguen respondiendo desde el sitio de siempre.
La novedad viene en la versión 13.26 de
Laravel, que trae más cosas
—#[DebounceFor] en listeners encolados y Queue::forward(), que merecen su
propio artículo—. Los detalles de comportamiento de este driver están en la
documentación de File Storage.