Plataforma Inteligente de Gestión de Contenidos Digitales

Migrando Athento a Python 3.7 y Django 3.2

Durante las últimas semanas mi equipo ha trabajado en migrar nuestro producto estrella, Athento, a versiones más modernas de Python, Django y demás librerías. Ha sido un camino duro y lleno de esfuerzo con el que hemos crecido mucho profesionalmente y que nos ha aportado a todos mucho conocimiento de las tecnologías que empleamos en nuestra empresa.
Alejandro Villegas

Alejandro Villegas

Propósito y punto de partida

Athento es un producto complejo y que lleva ya bastantes años en desarrollo y evolución. Hasta este momento, el stack tecnológico de Athento estaba basado en Django 1.11 y Python 2.7, versiones de estas tecnologías que nos hacía falta renovar. Además de los problemas de soporte que se nos presentaban al utilizar tecnologías con ya algunos años de antigüedad, teníamos limitaciones en cuanto a la funcionalidad que podíamos ofrecer a nuestros clientes: muchos de los paquetes y librerías que queríamos usar para implementar esta funcionalidad requerían versiones más modernas de estas tecnologías. Aunque a menudo hemos conseguido encontrar un workaround para implementar nuevas características de nuestro producto, había llegado la hora: había que actualizar.

Nuestro punto de partida era un producto en estas tecnologías, con una centena de instalaciones y que teníamos que cambiar a un stack tecnológico más moderno con la menor afectación a los más de 10K usuarios que trabajan con Athento en su día a día. Más de 230.000 líneas de código por migrar, más de 80 apps de Django y un reto adicional: seguir proporcionando soporte a nuestros clientes, aplicando correcciones, e implementando algunas funcionalidades nuevas en la versión antigua del producto.

Análisis y comienzo

Nuestros primeros pasos fueron evaluar la estrategia a seguir y las actualizaciones que íbamos a aplicar, y en qué orden. Ninguno de nosotros había realizado una actualización de este calibre. Teníamos claro que cualquier plan que pudiéramos diseñar en este momento iba a estar sujeto a cambios y que debíamos ser flexibles y pacientes a la hora de probar nuevas ideas. Nuestra primera pregunta fue la siguiente: ¿Primero Python o primero Django, o ambos a la vez?.

Esta pregunta la resolvimos estudiando las compatibilidades. Partíamos de Python 2.7 y Django 1.11. Comprobamos que Django 1.11 soportaba versiones de Python hasta la 3.7. Tener Python 3.7 nos permitía incrementar nuestra versión de Django hasta la 3.2. Aunque no son las versiones más modernas de estas tecnologías, nos suponían una  amplia mejora  a las versiones que teníamos con, probablemente, un esfuerzo menor que el necesario para migrar a otras versiones más modernas. Además, las versiones de Python y Django candidatas contaban ya con el suficiente rodaje y bagaje como para que nos sintiéramos tranquilos. Así que nos decidimos por migrar todo el proyecto a Python 3.7 con Django 1.11 y, en un segundo paso, migrar de Django 1.11 a Django 3.2.

Actualizando a Python 3.7

Hacer una actualización de la versión de Python desde la 2.7 a la 3.7 nos ha supuesto dos tareas principales: actualizar el código y actualizar todos los requerimientos de pip del proyecto.

Para actualizar el código tuvimos que hacer un análisis completo de Athento con ayuda de herramientas como 2to3 que, básicamente, analizan el código y sugieren los cambios de sintaxis requeridos para convertir de Python 2 a Python 3. 

Analizando los resultados arrojados por la herramienta decidimos no optar por la conversión automática, sino obtener los cambios sugeridos y aplicarlos en caso de que tuvieran sentido. El motivo de esta decisión es que algunos cambios no nos parecían razonables: llamadas a funciones como print añadían un doble paréntesis o los resultados de ciertos iteradores o QuerySets de Django se convertían a listas (esto supone a veces la pérdida de rendimiento si la evaluación era lazy). Como Athento consta de más de 2000 ficheros fuente de Python 2, nos dividimos la tarea entre todo el equipo para terminarla lo antes posible. Los cambios más comunes que tuvimos que realizar al código fueron:

  • Cambiar las importaciones, dado que las importaciones dentro del proyecto funcionan de forma diferente en Python 3 (ahora requieren que la ruta de importación comience por el nombre del paquete)
  • Los tipos unicode y str ahora son uno solo
  • Algunos tipos han perdido algunas funciones, o éstas han cambiado su comportamiento (por ejemplo, has_key() de los diccionarios ha desaparecido, y el método keys() se comporta de forma diferente)
  • Al recoger una excepción se podía utilizar una coma para separar el tipo de excepción y el objeto excepción recogido; ahora hay que utilizar la palabra reservada ‘as’
  • Los métodos encode y decode de strings tienen comportamiento diferente
  • Algunos paquetes desaparecen (como commands, reemplazado por subprocess) o su estructura cambia (como urllib)

El uso de paquetes externos que instalamos con pip presentaba otro desafío. En primer lugar, hay ciertos paquetes que no están disponibles en Python 3, o que las versiones que teníamos instaladas no son compatibles. Además, dado que Athento es un producto complejo, puede ocurrir que actualizar las versiones de los paquetes resulte en alguna incompatibilidad entre ellas, que hemos tenido que solventar. Las lecciones aprendidas son las siguientes:

  • Conviene eliminar todos los paquetes de los requerimientos que no son necesarios. Esto es además una buena práctica en materia de seguridad. Sin embargo, para esto hay que tener un conocimiento profundo de la aplicación (que en nuestro caso es bastante grande)
  • Hay que revisar las versiones de cada paquete para ver su compatibilidad. Para ello se debe visitar sitios como pypi, readthedocs o los repositorios de cada paquete
  • A veces nos encontramos dependencias entre paquetes: la versión X de un paquete requiere tener otro en una versión posterior a la Y pero, lo estábamos instalando en otra versión incompatible. Un procedimiento común que teníamos ante estos fallos consistía en:
    • Eliminar de los requerimientos la dependencia que falla
    • Instalarla desde pip sin indicar su versión (buscará la adecuada)
    • Comprobar en la documentación si esa versión es compatible y no hay breaking changes con la que teníamos originalmente
    • Desinstalarla desde pip, y añadir la nueva versión en los requerimientos
  • En otras ocasiones, no había versiones Python 3 de algunos paquetes, pero podíamos acceder al repositorio Git que los contiene. En estos casos hemos podido hacer un fork, y reescribir el código para adaptarlo a Python 3.

A pesar de todo nuestro empeño y los cambios hechos había un número de paquetes que, aunque compatibles ya con Python 3.7, seguían siendo incompatibles con Django 1.11, y hubo que planear una migración de Django antes de lo esperado. También conviene señalar un fallo en nuestro procedimiento que podríamos haber orientado de otra forma: cuando la mayoría de los requerimientos estaban instalados, parte del equipo continuó arreglando el resto de requerimientos mientras otros se dedicaron a ir probando parte de la funcionalidad que ya podría funcionar. Tal vez hubiera sido más apropiado comenzar la fase de testing una vez terminada la migración completa a la nueva versión de Django.

Migrando a Django 3.2

El cambio de versión de Django, en general, fue más liviano que el cambio de versión de Python. Hubo que revisar, de nuevo, los requerimientos relacionados con Django, ya que algunos de ellos ya se incorporaban de forma nativa y otros requerían actualización.

Un par de casos que crearon cierta incertidumbre fueron el paquete Hijack, utilizado para implementar la funcionalidad de suplantar el login de usuario, y otros que servían para extender la funcionalidad de la administración de Django. Estas funcionalidades tienen mucha importancia dentro de Athento  ya que son muy utilizadas por nuestros equipos de proyectos y de soporte para depurar rápidamente posibles errores o facilitar la implantación de la plataforma en nuestros clientes. Finalmente, encontramos una versión adecuada de Hijack y optamos por utilizar Grapelli como sistema para la administración.

Tests y correcciones

Una vez solventados los problemas de código, actualizadas las versiones, y arrancado el proyecto, hay que comenzar las pruebas. En esta primera fase todo el equipo se involucró, probando todas y cada una de las funcionalidades de Athento. Aunque ahora la sintaxis del proyecto era correcta con Python 3, semánticamente hay cosas que han cambiado y que no funcionaban bien. 

La mayoría de los problemas de encode y decode sobre strings que habíamos cambiado durante la migración del código, provocaron efectos indeseados: la conversión de bytes a str y viceversa nos devolvía a veces el dato con el tipo incorrecto y hubo que reescribir ciertas secciones del código. Relacionado con lo anterior, la apertura de ficheros en modo binario o de texto ocasiona varios problemas. Tuvimos que estudiar bien el paquete io de Python 3.7 y reescribir parte del código utilizando las clases BytesIO, TextIO y TextIOWrapper, entre otras.

Destacar también que mientras todo el equipo estaba realizando pruebas y correcciones sobre esta nueva versión de Athento, había que seguir añadiendo funcionalidad y dando soporte a los usuarios de la anterior versión, en Python 2. Esto era un reto para el día a día del equipo de desarrollo, puesto que debíamos preparar base de datos y todo nuestro entorno de desarrollo para soportar ambas versiones, y a veces nos encontrábamos con ciertos problemas. Uno de ellos era la caché de Django, que podía ocasionarnos problemas al cambiar de una versión a otra. Esto lo solucionamos limpiando esta caché tras cada cambio:

from django.core.cache import cache; cache.clear()

Otro problema habitual es traer a la nueva versión de Python 3 todos los hotfix o nuevas funcionalidades que se añaden al proyecto en Python 3. Para ello aplicamos el siguiente procedimiento: añadir a git un remote de la versión Python 2 del proyecto, colocarnos en la rama principal en el proyecto en Python 3, y mergear los cambios. Esto es un proceso costoso, puesto que suelen aparecer algunos conflictos o que se haya escrito código no compatible con Python 3 y que haya que reescribir. Este proceso ahora forma parte de nuestro flujo de trabajo, hasta que la versión de Python 2 de Athento quede obsoleta y todos nuestros clientes hayan sido migrados a la nueva versión.

$ git remote add git@bitbucket.org:/.git 
$ git fetch  
$ git switch -c merge/merge-py2-py3
$ git merge /fast-track

Instalación en un nuevo servidor

Nuestro siguiente objetivo, una vez aplicadas todas las correcciones, fue realizar una instalación de Athento sobre un nuevo servidor limpio. La idea era facilitar a los compañeros de sistemas la instalación y configuración de entornos en la versión nueva y, al mismo tiempo, permitir a los equipos de soporte y de proyectos probar las funcionalidades del producto en la versión actualizada.  

Basándonos en lo existente para la versión anterior, realizamos una revisión y actualización de la paquetería necesaria a nivel de Sistema Operativo. El cambio más significativo es que muchos de los paquetes instalados tiene versión diferente dependiendo de si se utiliza Python2 o Python3. Como ejemplo, se puede tomar el del paquete zbar, donde hemos tenido que cambiar de python-zbar a python3-zbar.

Como se ha comentado anteriormente, desplegar la nueva versión de Athento permitió al resto de equipos dentro de la empresa poder probar el producto en un entorno lo más parecido posible a producción. Durante unos días se estuvo iterando entre las pruebas y correcciones, siguiendo el protocolo de tests establecido.

Migrando servidores de Athento

Una vez Athento estaba funcionando correctamente en su versión de Python 3, y habiendo ya colocado esta versión en varios servidores nuevos para distintos clientes, debíamos procedimentar la actualización de servidores que tienen Athento con Python 2.7 y Django 1.11 a las nuevas versiones. Para ello establecimos el siguiente protocolo con ayuda del equipo de sistemas de la empresa:

  • Se realiza un backup del producto, en su versión antigua.
  • Se instala el producto nuevo.
  • Se hace el cambio de enlaces simbólicos, configuración del servidor, configuración de base de datos, etc… desde el producto antiguo hacia el nuevo.
  • Se incorporan las personalizaciones que pueda tener el producto (realizadas, normalmente, por el equipo de proyectos) en la nueva instalación.
  • Se realizan las migraciones de los modelos de Django del proyecto nuevo (algunos han podido cambiar). Contaremos más detalle de esto a continuación.

Las migraciones son generadas normalmente por Django, y hay que aplicarlas porque algún modelo puede haber cambiado su implementación, especialmente aquellos modelos manejados por las dependencias externas del proyecto. Nos hemos encontrado con diferentes problemas al respecto. Por ejemplo, al generar migraciones nuevas y aplicarlas, obtenemos errores porque los modelos ya existen (recordamos, debemos utilizar la base de datos ya instalada para que el cliente no pierda ningún dato). Finalmente, hemos planteado un par de estrategias que tendrán que evaluarse dependiendo del estado de cada servidor. Por un lado, podemos utilizar el flag –fake al aplicar las migraciones. De esta forma se registran como aplicadas, aunque no cambien el modelo de datos almacenado en base de datos. A continuación, se realiza un proceso de pruebas de la aplicación y se arreglan los modelos que hayan quedado inconsistentes. Otra alternativa es generar migraciones desde la antigua versión del producto y copiarlas a la nueva. A continuación, volvemos a generarlas en la nueva versión, por lo que se añadirán a continuación de las anteriores y dejarán los modelos de la base de datos coherente. El problema de esta aproximación es que las migraciones generadas en la versión Python 2 del producto pueden contener código no ejecutable en Python 3, y tiene que ser corregido a mano. Emplear una estrategia u otra va a depender en gran medida de cada uno de los servidores.

Trabajo de mejora contínua

Después de toda esta aventura y proceso de aprendizaje continuamos actualizando a nuestros clientes a la nueva versión del producto, sobre la cual iremos añadiendo nueva funcionalidad. Como equipo de producto, seguiremos actualizando esta nueva versión con todos los arreglos que se añadan a la versión anterior. Además, seguiremos colaborando con los equipos de proyecto, soporte y sistemas en traer cada vez más clientes a esta versión del producto. Mientras todo esto ocurre, añadiremos nuevas características a Athento para que cada vez sea una herramienta más completa, útil, rápida, y fiable. Python 3 nos permitirá también ahondar en tecnologías como Machine Learning, otro de los grandes incentivos que hemos tenido para acometer esta migración.

Esperamos que esta experiencia ayude a la comunidad y a todos aquellos que se van a embarcar en un viaje como el que nosotros hemos emprendido.