Cómo convertir aplicaciones php en aplicaciones de tenencia múltiple (II) por Juanda


Publicado el dom 26 noviembre 2017 por Juanda
Categoría: desarrollo

Etiquetas: php tutorial web http www aplicacion sofware multitenant


Hace algún tiempo escribí un tutorial donde se explicaba como convertir aplicaciones típicas PHP en aplicaciones de tenencia múltiple. En esa ocasión utilizaba contenedores dockers como estrategia. En este artículo explicaré como conseguir (casi) lo mismo de una manera MUY sencilla que no necesita el uso de docker.

Qué es una aplicación de tenencia múltiple

Cuando una aplicación está diseñada de manera que una misma instancia de la aplicación sirve para que sea utilizada por distintas organizaciones, cada una con sus propios usuarios y cada una viendo la aplicación como si fuese exclusivamente para ellos, se dice que la aplicación es de tenencia múltiple (o multitenant). Aclaro aquí que la misma aplicación instalada en varios servidores balanceados es considerada como una misma instancia, ya que se trata de una cuestión de dimensionamiento de la infraestructura para cubrir las necesidades del servicio.

La utilidad de este diseño se ve claro con el siguiente ejemplo: la consejería de educación de una comunidad autonoma desea ofrecer a los centros educativos una aplicación tipo LMS para la docencia on-line. Sería muy apropiado un diseño multitenant de cara al mantenimiento y gestión, pues no se necesita realizar una nueva instalación cuando un nuevo centro se añade al sistema.

Si la aplicación se va a desarrollar desde el principio no hay ningún problema en hacerlo multitenant, aumentará el coste de desarrollo, pues aumenta su complejidad, pero merecerá la pena una vez que comience a dar servicio.

Pero, ¿qué ocurre si se decide usar una aplicación existente que no es multitenant? Un ejemplo que ilustra este caso con claridad es el LMS Moodle. Es una aplicación muy estable, con un buen soporte en la comunidad, con mucha documentación y muchos miles de instalaciones a lo largo del mundo, lo que la convierte en una buena aplicación candidata para ofrecer a los centros. Pero, desgraciadamente, no es multitenant.

La buena noticia es que no está todo perdido; podemos hacer un truco para utilizar una sola instancia del código de la aplicación, aunque tendremos que mantener una base de datos por cada centro que la use. Y es ese truco el que vamos a explicar en este artículo.

Variables en el entorno del servidor web

La diferencia entre distintas instalaciones de una aplicación web típica suele limitarse a los parámetros de configuración y a los servicios que utiliza (bases de datos, es el caso típico), los cuales están referenciados en dichos parámetros de configuración.

Siguiendo con el caso de Moodle, la diferencia entre dos instalaciones se encuentran en la base de datos y en el directorio de datos que requiere para gestionar la cache y almacenar los ficheros subidos por los usuarios.

La clave para poder utilizar el mismo código para todos los centros está en utilizar la posibilidad que ofrecen los servidores web de definir variables de entorno propias para cada uno de sus virtualhosts: cada centro tendrá su dominio asociado a un virtualhost, y en el virtualhost de cada centro podemos definir variables de entorno que se corresponderán con los parámetros de configuración de la aplicación. Ahora se trata de que la aplicación sea capaz de acceder a las variables de entorno del virtualhost para definir sus parámetros de configuración. De esa manera, según el virtualhost que se este ejecutando, el código de la aplicación usará unos parámetros de configuración u otros, lo que produce que se use la base de datos que le corresponda al centro y cualquier otro servicio que precise la aplicación.

Los ejemplos que siguen son para el web server apache2 y PHP instalado como módulo de apache2, pero lo mismo se podría hacer con nginx y PHP instalado como proceso FastCGI (PHP-FPM).

Supongamos que tenemos N centros a los que queremos ofrecer Moodle. Cada centro tiene asociado un dominio. Como ejemplo supongamos los siguientes:

  • centro_1.educa.com
  • centro_2.educa.com
  • ...
  • centro_N.educa.com

Y a cada centro le corresponde un virtualhost cuyo fichero de configuración podría ser:

<VirtualHost *:80>
    ServerName centro_X.educa.com

    DocumentRoot /var/www/moodle
    <Directory /var/www/moodle>
        AllowOverride All
        Order Allow,Deny
        Allow from All
    </Directory>

    ErrorLog /var/log/apache2/moodle_error.log
    CustomLog /var/log/apache2/moodle_access.log combined
</VirtualHost>

De esta manera, todos los virtualhosts apuntan al mismo directorio /var/www/moodle, que es donde colocamos el código de moodle.

Ahora nos fijamos en que el único fichero de configuración de moodle es /config.php, y tiene esta pinta:

unset($CFG);  // Ignore this line
global $CFG;  // This is necessary here for PHPUnit execution
$CFG = new stdClass();

$CFG->dbtype    = 'mysql';      // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
$CFG->dblibrary = 'native';     // 'native' only at the moment
$CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
$CFG->dbname    = 'moodle';     // database name, eg moodle
$CFG->dbuser    = 'username';   // your database username
$CFG->dbpass    = 'password';   // your database password
$CFG->prefix    = 'mdl_';       // prefix to use for all table names

$CFG->wwwroot   = 'http://example.com/moodle';
$CFG->dataroot  = '/home/example/moodledata';

$CFG->directorypermissions = 02777;
$CFG->admin = 'admin';
require_once(__DIR__ . '/lib/setup.php');

Se trata de hacer que los valores de los parámetros de configuración que nos interesen se obtengan de las variables de entorno del virtualhost. En PHP eso se hace mediante el array superglobal $__SERVER. Por ejemplo, si en el virtualhost definimos una variable de entorno que se denomine DBNAME cuyo valor es database_centro_1, entonces, el valor del $_SERVER['DBNAME'] para ese virtualhost es precisamente database_centro_1. Y ese es el quid de la cuestión.

Añadimos a los ficheros de configuración las variables de entorno DBHOST, DBNAME, DBUSER, DBPASS, WWWROOT y DATAROOT, con los valores correspondientes a los parámetros de configuración que deseamos hacer dependientes de cada virtualhost:

<VirtualHost *:80>
    ServerName centro_X.educa.com

    SetEnv DBHOST "10.11.187.182"
    SetEnv DBNAME "base_datos_centro_X"
    SetEnv DBUSER "user_centro_X"
    SetEnv DBPASS "passw_centro_X"
    SetEnv WWWROT "http://centroX.educa.com"
    SetEnv DATAROOT "/var/moodledata/centro_X"

    DocumentRoot /var/www/moodle
    <Directory /var/www/moodle>
        AllowOverride All
        Order Allow,Deny
        Allow from All
    </Directory>

    ErrorLog /var/log/apache2/moodle_error.log
    CustomLog /var/log/apache2/moodle_access.log combined
</VirtualHost>

Y modificamos el fichero de configuración de moodle para que tome el valor de los parámetros de configuración de las variables de entorno que acabamos de definir:

unset($CFG);  // Ignore this line
global $CFG;  // This is necessary here for PHPUnit execution
$CFG = new stdClass();

$CFG->dbtype    = 'mysql';      // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
$CFG->dblibrary = 'native';     // 'native' only at the moment
$CFG->dbhost    = $__SERVER['DBHOST'];  // eg 'localhost' or 'db.isp.com' or IP
$CFG->dbname    = $__SERVER['DBNAME'];     // database name, eg moodle
$CFG->dbuser    = $__SERVER['DBUSER'];   // your database username
$CFG->dbpass    = $__SERVER['DBPASS'];   // your database password
$CFG->prefix    = 'mdl_';       // prefix to use for all table names

$CFG->wwwroot   = $__SERVER['WWWROOT'];
$CFG->dataroot  = $__SERVER['DATAROOT'];

$CFG->directorypermissions = 02777;
$CFG->admin = 'admin';
require_once(__DIR__ . '/lib/setup.php');

¡Y listo! hemos conseguido que todos los centros usen el mismo código base de la aplicación. No ha sido posible compartir también la misma base de datos, para eso habría que modificar el código de la aplicación muchísimo. Pero lo mismo puede ser una ventaja que cada centro tenga su base de datos, ya que de esta manera el crecimiento de las mismas es independiente y, llegado el caso, se pueden repartir entre distintos servidores de base de datos que nada tienen que ver entre ellos. Mientras que si todos los datos de todos los centros están en una misma base de datos, puede llegar el momento en que para mejorar la performance haya que usar complicados métodos de particionamiento horizontal (sharding) magníficos para complicar la vida de cualquier administrador de sistemas.

Automatizando el sistema

La incorporación de nuevos centros al sistema implicaría:

  1. Crear un nuevo virtualhost asociado al dominio del centro con las variables de entorno que se pasarán a la configuración de la aplicación.
  2. Crear una base de datos y un directorio de datos para el centro.

Tareas que pueden ser fácilmente automatizadas mediante scripts o mediante una aplicación.

Conclusión

Hemos descrito una sencilla estrategia que permite desplegar aplicaciones que han sido diseñadas para ser usada por una sola organización por instalación como si se tratasen de aplicaciones de tenencia múltiple.

La estrategia solo permite compartir el mismo código base, nunca la base de datos ni otros posibles servicios que necesite, pero aún así reduce la complejidad de mantener muchas instalaciones de la misma aplicación, especialmente cuando se realicen modificaciones al código base como puede ser al añadir plugins o aplicar parches que resuelvan bugs o actualizaciones.

Mientras no haya modificaciones en la estructura de la base de datos, el mantenimiento resulta bastante sencillo. En el momento en que haya que modificar la base de datos, hay que actuar sobre todas ellas, pero al menos hemos conseguido simplificar el mantenimiento del código.

Por último diremos que aunque hemos usado PHP y apache como ejemplo, esta misma estrategia es válida, para otras tecnologías como nginx, Java, Python, nodejs, etcétera con cambios en los detalles propios de cada una de ellas.