Ir al contenido principal
Laravel, shipping fast.
Capítulo 20 · Despliegue

Volver atrás de forma segura

Julian Beaujardin

Revertir el código y revertir un esquema son dos operaciones distintas, y tratarlas como una sola es la manera en que un incidente de cinco minutos se convierte en uno de cinco horas.

Revertir el código, si despliegas por versiones, es casi instantáneo. La versión anterior sigue en disco con sus dependencias instaladas. Revertir significa apuntar otra vez el enlace simbólico hacia ella: el mismo cambio atómico que te trajo hasta aquí, ejecutado al revés.

Revertir un esquema no es instantáneo ni es seguro por defecto. Un método down() se escribe para deshacer limpiamente una migración, no para deshacerla mientras código en marcha puede depender de lo que elimina.

# Mal: revertir el esquema en el mismo movimiento
php artisan migrate:rollback --step=1
# ... y luego volver a la versión anterior

Los dos comandos son reales y los dos funcionan. El problema es la suposición que hay detrás. Si algo escribió en las columnas nuevas entre el despliegue y la vuelta atrás, migrate:rollback elimina esas columnas y esos datos desaparecen. No se revierten. Desaparecen. Has cambiado un bug por una pérdida de datos.

# Bien: revertir el código y dejar el esquema
# volver a la versión anterior
# las columnas nuevas se quedan; la vieja no las leía

Como la migración era un añadido, a la versión vieja le da igual que webhook_url exista. Nunca la buscó. Revertir el código basta para detener la hemorragia. Si el esquema de verdad tiene que cambiar, eso es una migración nueva hacia delante, escrita con calma, y no un método down() ejecutado bajo la presión de un incidente contra una base de datos de cuyo estado nadie está seguro.

Cuando alguien de tu equipo esté atendiendo una alerta a las dos de la mañana, no recordará cuáles de tus migraciones es seguro revertir y cuáles borran algo. No le hagas adivinar. Con migraciones solo hacia delante y que solo añaden, la respuesta es siempre la misma: revierte primero el código, y normalmente ya está.

Secretos que nunca llegan al repositorio

Los archivos de configuración son el único lugar donde se llama a env():

// config/services.php
'statamic' => [
    'url' => env('STATAMIC_API_URL'),
],

No es una preferencia de estilo. php artisan optimize lee cada archivo de configuración una vez, resuelve cada llamada a env() en ese momento, y escribe el resultado en un único archivo cacheado. A partir de ahí Laravel no lee .env en absoluto. Una llamada a env() metida en un controlador, un job o un comando devuelve null para todo valor que venía de .env, y solo una vez que la configuración está cacheada, lo que en la práctica quiere decir solo en producción.

Así que env() se queda en los archivos de configuración, y todo lo demás llama a config('services.statamic.url'). En ese archivo no hay ningún token de proveedor: el Capítulo 4 lo movió a cada consumidor. También significa que cambiar un valor en el .env de un servidor no hace nada hasta que se reconstruye la caché de configuración, cosa que hace un despliegue y no hace la edición de un archivo. (Documentación de Laravel: Configuration › Configuration Caching.)

Los valores en sí nunca viven en el repositorio. Git ignora .env. Lo que se sube es el nombre de la clave, dentro de env(). El valor vive en el servidor, fijado mediante la gestión de entorno de tu proveedor de hosting, totalmente fuera de git pull. Un despliegue que trae código nuevo nunca toca los secretos, porque los secretos nunca fueron algo que un despliegue traiga.

Un secreto que ha estado en el repositorio ya no es un secreto. Quitarlo en un commit posterior no ayuda: está en el historial. Rótalo.

Ten claro cómo rotarías cada uno. El token de proveedor, la clave de aplicación, la contraseña de la base de datos. Si la respuesta para alguno es «no estoy seguro de qué se rompería», averígualo una tarde tranquila y no durante un incidente.

La clave de aplicación es la que más cuidado necesita, porque aquí hace más que firmar cookies. Cifra los settings de cada consumidor. Reemplázala sin cuidado y el token de proveedor de cada socio se vuelve ilegible de golpe. Laravel te deja rotarla con elegancia: fija la clave nueva como APP_KEY y lista la vieja en APP_PREVIOUS_KEYS. Los valores nuevos se cifran con la clave nueva, y los viejos todavía pueden leerse con la vieja. (Documentación de Laravel: Encryption › Gracefully Rotating Encryption Keys.)

Eso es media rotación. La clave vieja todavía abre todos los valores escritos antes del cambio. Para terminar, escribe otra vez cada valor cifrado, y hazlo mientras la clave vieja sigue listada:

// app/Console/Commands/ReencryptConsumers.php, en handle()
Consumer::withTrashed()->chunkById(100, function ($rows) {
    foreach ($rows as $consumer) {
        $consumer->forceFill([
            'settings' => $consumer->settings,
            'webhook_secret' => $consumer->webhook_secret,
        ])->saveQuietly();
    }
});

La asignación es la gracia del asunto. Cargar un modelo y guardarlo no escribe nada, porque nada cambió. Asignarle a un atributo cifrado su propio valor hace que Eloquent lo cifre otra vez, con la clave nueva. Solo cuando eso se ha ejecutado en todas partes sale la clave vieja de APP_PREVIOUS_KEYS. Quítala antes y las credenciales de todos los socios se vuelven ilegibles.

Recuerda dónde acaban los secretos. php artisan optimize escribe cada valor de configuración resuelto, secretos incluidos, en un archivo bajo bootstrap/cache. Mantén ese directorio y .env fuera de las copias de seguridad que no cifres y fuera de cualquier cosa que el servidor web pueda servir. Una copia de seguridad de la base de datos guardada junto a la clave que la descifra no protege nada.

Dos valores que producción debe fijar

El .env.example del que parte todo proyecto de Laravel está escrito para un portátil:

APP_ENV=local
APP_DEBUG=true

Cópialo a un servidor sin cambios y cada excepción no manejada responde con rutas de archivos, consultas y valores del entorno. Producción fija los dos:

APP_ENV=production
APP_DEBUG=false

Compruébalos después de cada cambio en la forma de aprovisionar el servidor, con php artisan about, y haz que el script de despliegue los compruebe también:

php artisan about --only=environment --json \
  | jq -e '.environment.environment == "production"
      and .environment.debug_mode == false'

jq -e termina con un código distinto de cero cuando la respuesta es falsa, así que el despliegue se detiene ahí.

Mejor que comprobar es hacer que el error sea imposible de entregar:

// app/Providers/AppServiceProvider.php, en boot()
$local = $this->app->environment('local', 'testing');

if (config('app.debug') && ! $local) {
    config(['app.debug' => false]);

    throw new RuntimeException('Debug is on outside local.');
}

La condición es «no local», y no «producción», para que también queden cubiertos un servidor de staging o un entorno con un nombre que nadie previó. No puede distinguir un portátil de un servidor al que se le dijo que es local, que es lo que hace una copia sin cambios de .env.example. De ese caso se encarga la comprobación del despliegue. El modo debug se apaga antes de lanzar la excepción, o el propio rechazo se renderizaría como una página de debug. Con eso puesto, una versión con el debug activado responde a todas las peticiones, la ruta de salud incluida, con un 500 a secas, y un despliegue que comprueba /up antes de mover el tráfico nunca cambia a ella.

Infraestructura que puedes repetir

Montar un servidor a mano, una vez, con cuidado, es fácil. Hacerlo igual la quinta vez, tarde, bajo presión, sin saltarte el paso que siempre te saltas, es donde falla la infraestructura hecha a mano.

La respuesta tiene la misma forma que todo lo demás en este libro: decide una vez, ponlo por escrito como código y ejecuta lo mismo todas las veces. Qué es ese código depende de dónde alojes la aplicación. Una plataforma gestionada lo reduce a un archivo de configuración. Un servicio de aprovisionamiento lo reduce a una receta. Tus propios servidores necesitan un script. En todos los casos la prueba es la misma: ¿podría alguien que nunca ha visto este sistema levantar un segundo entorno idéntico a partir de lo que hay en el repositorio?

Resumen del capítulo 20

Antes de desplegar:

  • El despliegue es un script del repositorio, y se ejecuta solo sobre código que pasó CI.
  • Las versiones se construyen en su propio directorio y se activan de forma atómica.
  • La ruta de salud se comprueba antes de mover el tráfico.
  • Las migraciones solo añaden, y se ejecutan con --force --isolated.
  • env() aparece solo en los archivos de configuración. Los secretos viven en el servidor.
  • APP_ENV=production y APP_DEBUG=false, verificados.

Después del cambio:

  • Recarga los workers, todas las veces.

Cuando algo sale mal:

  • Revierte primero el código. Deja el esquema como está.
  • Trata migrate:rollback como último recurso. Un método down() puede borrar datos.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.