Implementación de tiempo de inactividad cero de Laravel

Publicado: 2022-03-11

Cuando se trata de actualizar una aplicación en vivo, hay dos formas fundamentalmente diferentes de hacerlo.

En el primer enfoque, hacemos cambios incrementales en el estado de nuestro sistema. Por ejemplo, actualizamos archivos, modificamos propiedades del entorno, instalamos necesidades adicionales, etc. En el segundo enfoque, desmantelamos máquinas completas y reconstruimos el sistema con nuevas imágenes y configuraciones declarativas (por ejemplo, usando Kubernetes).

Implementación de Laravel simplificada

Este artículo cubre principalmente aplicaciones relativamente pequeñas, que pueden no estar alojadas en la nube, aunque mencionaré cómo Kubernetes puede ayudarnos mucho con implementaciones más allá del escenario "sin nube". También discutiremos algunos problemas generales y consejos para realizar actualizaciones exitosas que podrían ser aplicables en una variedad de situaciones diferentes, no solo con la implementación de Laravel.

A los efectos de esta demostración, usaré un ejemplo de Laravel, pero tenga en cuenta que cualquier aplicación PHP podría usar un enfoque similar.

Versionado

Para empezar, es fundamental que conozcamos la versión del código actualmente implementada en producción. Puede estar incluido en algún archivo o al menos en el nombre de una carpeta o archivo. En cuanto al naming, si seguimos la práctica estándar del versionado semántico, podemos incluir más información que un solo número.

Al observar dos versiones diferentes, esta información adicional podría ayudarnos a comprender fácilmente la naturaleza de los cambios introducidos entre ellas.

Imagen que muestra la explicación del control de versiones semántico.

El control de versiones del lanzamiento comienza con un sistema de control de versiones, como Git. Digamos que hemos preparado una versión para la implementación, por ejemplo, la versión 1.0.3. Cuando se trata de organizar estos lanzamientos y flujos de código, existen diferentes estilos de desarrollo, como el desarrollo basado en troncales y el flujo de Git, que puede elegir o combinar según las preferencias de su equipo y las especificaciones de su proyecto. Al final, lo más probable es que terminemos con nuestros lanzamientos etiquetados correspondientemente en nuestra rama principal.

Después de la confirmación, podemos crear una etiqueta simple como esta:

git tag v1.0.3

Y luego incluimos etiquetas mientras ejecutamos el comando push:

git push <origin> <branch> --tags

También podemos agregar etiquetas a confirmaciones antiguas usando sus hashes.

Obtener archivos de publicación en su destino

La implementación de Laravel lleva tiempo, incluso si se trata simplemente de copiar archivos. Sin embargo, incluso si no toma mucho tiempo, nuestro objetivo es lograr un tiempo de inactividad cero .

Por lo tanto, debemos evitar instalar la actualización en el lugar y no debemos cambiar los archivos que se están publicando en vivo. En su lugar, debemos implementar en otro directorio y hacer el cambio solo una vez que la instalación esté completamente lista.

En realidad, hay varias herramientas y servicios que nos pueden ayudar con las implementaciones, como Envoyer.io (del diseñador de Laravel.com Jack McDade), Capistrano, Deployer, etc. Todavía no los he usado todos en producción, así que no puedo hacer recomendaciones o escribir una comparación exhaustiva, pero permítanme mostrar la idea detrás de estos productos. Si algunos (o todos) de ellos no pueden cumplir con sus requisitos, siempre puede crear sus scripts personalizados para automatizar el proceso de la mejor manera que le parezca.

Para los propósitos de esta demostración, digamos que nuestra aplicación Laravel es atendida por un servidor Nginx desde la siguiente ruta:

/var/www/demo/public

Primero, necesitamos un directorio para colocar los archivos de lanzamiento cada vez que hacemos una implementación. Además, necesitamos un enlace simbólico que apunte a la versión de trabajo actual. En este caso, /var/www/demo servirá como enlace simbólico. Reasignar el puntero nos permitirá cambiar rápidamente de versión.

Manejo de archivos de implementación de Laravel

En caso de que estemos tratando con un servidor Apache, es posible que debamos permitir los siguientes enlaces simbólicos en la configuración:

Options +FollowSymLinks

Nuestra estructura puede ser algo como esto:

 /opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2

Puede haber algunos archivos que necesitemos conservar a través de diferentes implementaciones, por ejemplo, archivos de registro (si no estamos usando Logstash, obviamente). En el caso de la implementación de Laravel, es posible que deseemos conservar el directorio de almacenamiento y el archivo de configuración .env. Podemos mantenerlos separados de otros archivos y usar sus enlaces simbólicos en su lugar.

Para obtener nuestros archivos de lanzamiento del repositorio de Git, podemos usar comandos de clonación o archivo. Algunas personas usan git clone, pero no puedes clonar una confirmación o etiqueta en particular. Eso significa que se obtiene todo el repositorio y luego se selecciona la etiqueta específica. Cuando un repositorio contiene muchas ramas o un historial grande, su tamaño es considerablemente mayor que el archivo de lanzamiento. Entonces, si no necesita específicamente el repositorio de git en producción, puede usar git archive . Esto nos permite obtener solo un archivo de archivo por una etiqueta específica. Otra ventaja de usar este último es que podemos ignorar algunos archivos y carpetas que no deberían estar presentes en el entorno de producción, por ejemplo, las pruebas. Para esto, solo necesitamos establecer la propiedad export-ignore en el .gitattributes file . En la Lista de verificación de prácticas de codificación segura de OWASP, puede encontrar la siguiente recomendación: "Elimine el código de prueba o cualquier funcionalidad que no esté destinada a la producción, antes de la implementación".

Si buscamos la versión del sistema de control de versiones de origen, git archive y export-ignore podrían ayudarnos con este requisito.

Echemos un vistazo a un script simplificado (necesitaría un mejor manejo de errores en producción):

desplegar.sh

 #!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo

Para implementar nuestro lanzamiento, podríamos simplemente ejecutar lo siguiente:

deploy.sh v1.0.3

Nota: En este ejemplo, v1.0.3 es la etiqueta git de nuestro lanzamiento.

¿Compositor en producción?

Es posible que haya notado que el script está invocando a Composer para instalar dependencias. Aunque vea esto en muchos artículos, puede haber algunos problemas con este enfoque. Generalmente, es una mejor práctica crear una compilación completa de una aplicación y avanzar esta compilación a través de varios entornos de prueba de su infraestructura. Al final, tendrá una compilación probada exhaustivamente, que se puede implementar de manera segura en producción. Aunque cada compilación debe ser reproducible desde cero, no significa que debamos reconstruir la aplicación en diferentes etapas. Cuando hacemos que composer se instale en producción, esta no es realmente la misma compilación que la probada y esto es lo que puede salir mal:

  • El error de red puede interrumpir la descarga de dependencias.
  • Es posible que el proveedor de la biblioteca no siempre siga el SemVer.

Un error de red se puede notar fácilmente. Nuestro script incluso dejaría de ejecutarse con un error. Pero un cambio importante en una biblioteca puede ser muy difícil de precisar sin ejecutar pruebas, lo que no se puede hacer en producción. Al instalar dependencias, Composer, npm y otras herramientas similares se basan en el control de versiones semántico: mayor.menor.parche. Si ve ~1.0.2 en composer.json, significa que debe instalar la versión 1.0.2 o la última versión del parche, como 1.0.4. Si ve ^ 1.0.2, significa que debe instalar la versión 1.0.2 o la última versión secundaria o parche, como 1.1.0. Confiamos en que el proveedor de la biblioteca aumentará el número principal cuando se introduzca algún cambio importante, pero a veces este requisito se pasa por alto o no se sigue. Ha habido tales casos en el pasado. Incluso si coloca versiones fijas en su composer.json, sus dependencias pueden tener ~ y ^ en su composer.json.

Si es accesible, en mi opinión, una mejor manera sería usar un repositorio de artefactos (Nexus, JFrog, etc.). La compilación de lanzamiento, que contiene todas las dependencias necesarias, se crearía una vez, inicialmente. Este artefacto se almacenaría en un depósito y se recuperaría para varias etapas de prueba desde allí. Además, esa sería la compilación que se implementará en producción, en lugar de reconstruir la aplicación desde Git.

Mantener el código y la base de datos compatibles

La razón por la que me enamoré de Laravel a primera vista fue cómo su autor prestó mucha atención a los detalles, pensó en la conveniencia de los desarrolladores y también incorporó muchas mejores prácticas en el marco, como migraciones de bases de datos.

Las migraciones de base de datos nos permiten tener nuestra base de datos y código sincronizados. Ambos cambios se pueden incluir en una sola confirmación, por lo tanto, en una sola versión. Sin embargo, esto no significa que cualquier cambio pueda implementarse sin tiempo de inactividad. En algún momento durante la implementación, se ejecutarán diferentes versiones de la aplicación y la base de datos. En caso de problemas, este punto podría incluso convertirse en un período. Siempre debemos intentar que ambos sean compatibles con las versiones anteriores de sus compañeros: base de datos antigua-aplicación nueva, base de datos nueva-aplicación antigua.

Por ejemplo, supongamos que tenemos una columna de address y necesitamos dividirla en address1 y address2 . Para mantener todo compatible, es posible que necesitemos varios lanzamientos.

  1. Agregue dos nuevas columnas en la base de datos.
  2. Modifique la aplicación para usar nuevos campos siempre que sea posible.
  3. Migre los datos de address a nuevas columnas y suéltelos.

Este caso también es un buen ejemplo de cómo los pequeños cambios son mucho mejores para la implementación. Su reversión también es más fácil. Si estamos cambiando la base de código y la base de datos durante varias semanas o meses, podría ser imposible actualizar el sistema de producción sin tiempo de inactividad.

Algunas maravillas de Kubernetes

Aunque la escala de nuestra aplicación puede no necesitar nubes, nodos y Kubernetes, aún me gustaría mencionar cómo se ven las implementaciones en K8. En este caso, no realizamos cambios en el sistema, sino que declaramos lo que nos gustaría lograr y qué debería ejecutarse en cuántas réplicas. Luego, Kubernetes se asegura de que el estado real coincida con el deseado.

Cada vez que tenemos una nueva versión lista, construimos una imagen con nuevos archivos, etiquetamos la imagen con la nueva versión y la pasamos a K8s. Este último hará girar rápidamente nuestra imagen dentro de un grupo. Esperará antes de que la aplicación esté lista en función de la verificación de preparación que proporcionamos, luego redirigirá el tráfico a la nueva aplicación y eliminará la antigua. Es muy fácil tener varias versiones de nuestra aplicación ejecutándose, lo que nos permitiría realizar implementaciones blue/green o canary con solo unos pocos comandos.

Si está interesado, hay algunas demostraciones impresionantes en la charla "9 Steps to Awesome with Kubernetes by Burr Sutter".

Relacionado: Autenticación completa de usuarios y control de acceso: un tutorial de Laravel Passport, pt. 1