DEV Community

Cover image for Definiendo nuestras infraestructuras para desarrollo y testing con Docker
Dailos Rafael D铆az Lara
Dailos Rafael D铆az Lara

Posted on

Definiendo nuestras infraestructuras para desarrollo y testing con Docker

馃嚞馃嚙 English version

馃幆 Objetivo

Cuando estamos creando una nueva aplicaci贸n o funcionalidad, normalmente necesitamos enviar peticiones a recursos independientes como pueden ser una base de datos o servicios con comportamiento controlado pero obviamente, realizar estas tareas contra servidores en la nube tiene un coste.

En este tipo de situaciones es cuando el aislamiento de sistemas que nos proporcionan los contenedores de Docker, es realmente 煤til.

En este art铆culo vamos a ver c贸mo podemos usar Docker para levantar una infraestructura m铆nima que nos permita ejecutar las tareas de desarrollo y/o testing, localmente.

El principal objetivo de este texto es mostrar c贸mo utilizar un 煤nico archivo docker-compose.yml para ambos entornos, empleando diferentes archivos .env para personalizar cada contenedor espec铆fico tanto para desarrollo como para testing.

Adem谩s, nos centraremos en c贸mo arrancar un nuevo contenedor para testing, ejecutar los tests que sean pertinentes y finalmente, apagar dicho contenedor.

馃捇 Configuraci贸n del sistema

Si vamos a hablar sobre Docker, es obvio que necesitamos tenerlo instalado en nuestro sistema. Si a煤n no lo tienes, puedes seguir las indicaciones dadas en la documentaci贸n oficial, para el sistema operativo que corresponda.

Otro elemento que vamos a necesitar tener instalado en nuestro sistema es docker-compose. De nuevo, si a煤n no lo tienes instalado, puedes seguir las indicaciones de la documentaci贸n oficial.

Por 煤ltimo, dado que este ejemplo est谩 orientado a aplicaciones basadas en JavaScript/TypeScript, necesitamos tener instalado NodeJS (documentaci贸n oficial).

馃彈 Inicializaci贸n del proyecto

馃敟 Si ya tienes inicializando tu propio proyecto basado en NodeJS, puedes saltarte esta secci贸n 馃敟

Vamos a inicializar nuestro proyecto NodeJS abriendo una consola de comandos, en el directorio donde queramos trabajar, y escribimos el siguiente comando:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Esta acci贸n nos crear谩 una 煤nico archivo package.json en la ra铆z de nuestro proyecto, con el siguiente contenido:

Ahora podemos instalar Jest ejecutando la siguiente instrucci贸n en nuestra consola de comandos, para incluir esta librer铆a en nuestro proyecto:

npm i -D jest
Enter fullscreen mode Exit fullscreen mode

El siguiente paso es crear la estructura m谩s b谩sica de directorios para el proyecto.

/
|-- /docker # <= Nuevo directorio.
|-- /node_modules
|-- /src # <= Nuevo directorio.
|-- package-lock.json
|-- package.json
Enter fullscreen mode Exit fullscreen mode

馃惓 Definiendo la configuraci贸n de Docker

Vamos a tener dos entornos principales (development y test) y la idea es tener un 煤nico archivo docker-compose.yml para gestionar los contenedores de ambos entornos.

馃搫 Definici贸n del archivo docker-compose.yml

Para conseguir nuestro objetivo, dentro del directorio /docker vamos a crear un 煤nico archivo llamado docker-compose.yml, el cual contendr谩 el siguiente c贸digo:

Como podemos apreciar, hay varias l铆neas marcadas como coupling smell. Esto significa que, con la configuraci贸n actual, podemos ejecutar un 煤nico contenedor de Docker destinado principalmente para tareas de desarrollo. Por lo tanto, est谩 altamente acoplado a su entorno de ejecuci贸n.

驴No ser铆a genial si fu茅semos capaces de reemplazar esas configuraciones definidas directamente en el c贸digo, por referencias las cuales vinieran establecidas por alg煤n tipo de archivo de configuraci贸n?

鈿 Archivos .env para contenedores Docker

!S铆! Podemos usar archivos .env de la misma manera que ya los usamos para nuestras aplicaciones, pero para configurar contenedores de Docker.

Lo primero que necesitamos hacer es modificar el archivo docker-compose.yml que acabamos de crear para usar plantillas basadas en llaves, para definir nombres de constantes que reemplazaremos con los valores indicados en nuestros archivos .env. De este modo, el contenido del archivo docker-compose.yml quedar谩 de la siguiente manera:

Como podemos ver, hemos reemplazado los valores directamente escritos en el c贸digo por referencias del tipo ${CONSTANT_NAME}. El nombre de las variables escrito entre llaves ser谩 el nombre de los valores definidos en nuestros archivos .env. De esta manera, cuando arranquemos el comando docker-compose usando una opci贸n espec铆fica de la l铆nea de comandos que veremos m谩s adelante, el contenido del archivo .env ser谩 reemplazado en nuestro archivo docker-compose.yml antes de que se cree el contenedor de Docker.

Ahora es el momento de definir nuestros entornos as铆 que modificamos el contenido del directorio /docker para que quede tal que as铆:

/
|-- /docker
|   |-- /dev # <= Nuevo directorio y archivo.
|   |   |-- .docker.dev.env
|   |-- /test # <= Nuevo directorio y archivo.
|   |   |-- .docker.test.env
|   |-- docker-compose.yml
|-- /node_modules
|-- /src
|-- package-lock.json
|-- package.json
Enter fullscreen mode Exit fullscreen mode

Por cada entorno hemos creado un 煤nico subdirectorio: dev y test.

Dentro de cada subdirectorio de entorno hemos creado un archivo .env espec铆fico: .docker.dev.env y .docker.test.env.

馃檵鉂 驴Ser铆a posible nombrar los archivos de entorno s贸lo como .env?

S铆, es posible y adem谩s, no habr铆a ning煤n problema en ello pero... un nombre de archivo tan descriptivo es una ayuda para nuestro rol como profesionales del desarrollo. Dado que en un mismo proyecto es muy probable que haya m煤ltiples archivos de configuraci贸n, es 煤til ser capaz de diferenciarlos cuando tenemos varios de ellos abiertos, en el editor de c贸digo, al mismo tiempo. Esta es la raz贸n por la que los archivos .env tienen uno nombres tan descriptivos.

Ahora pasaremos a definir el contenido de nuestros archivos de entornos, para que queden de la siguiente manera:

y...

Hay cuatro propiedades a las que debemos prestar atenci贸n a la hora de diferenciar entre los dos archivos:

  • CONTAINER_NAME
  • EXTERNAL_PORT
  • VOLUME_NAME
  • CONFIGURATION_PATH

La propiedad CONTAINER_NAME nos permite definir el nombre del contenedor que veremos despu茅s de que 茅ste ha sido creado y adem谩s, cuando ejecutamos el comando docker ps -a para listar todos los contenedores presentes en nuestro sistema.

EXTERNAL_PORT es una propiedad realmente sensible ya que nos permite definir el puerto que el contenedor tendr谩n publicado y a trav茅s del cual, nuestra aplicaci贸n podr谩 conectarse con 茅l. Es realmente importante tener cuidado con este par谩metro porque algunas veces nos interesar谩 tener levantados ambos entornos al mismo tiempo (development y test), pero si hemos definido el mismo puerto de acceso para ambos contenedores, el sistema nos lanzar谩 un error al lanzar el segundo contenedor, ya que el puerto estar谩 ocupado.

La propiedad VOLUME_NAME definir谩 el nombre del almacenamiento de datos en nuestro sistema.

Finalmente, en caso de que hayamos definido cualquier tipo de conjunto de datos para inicializar nuestra base de datos antes de usarla, la propiedad CONFIGURATION_PATH nos permitir谩 definir d贸nde est谩 ubicado ese conjunto de datos.

馃檵鈥嶁檧锔忊潛 Oye pero, 驴qu茅 pasa con la propiedad COMPOSE_PROJECT_NAME?

Esa es una magn铆fica pregunta.

Nuestro primer objetivo es crear un contenedor espec铆fico por cada entorno, bas谩ndonos en el mismo archivo docker-compose.yml.

Ahora mismo, si ejecutamos nuestro docker-compose para development, por ejemplo, crearemos el contenedor con esa definici贸n de entorno y el archivo docker-compose.yml quedar谩 enlazado a dicho contenedor.

De este modo, si intentamos ahora arrancar el mismo archivo pero utilizando la configuraci贸n para testing, el resultado final ser谩 que hemos actualizado el contenedor previo de development, sin la configuraci贸n para el entorno de testing. 驴Por qu茅? Pues porque el archivo de composici贸n est谩 enlazado al contenedor que arrancamos inicialmente.

Para conseguir nuestro objetivo satisfactoriamente, empleamos la propiedad COMPOSE_PROJECT_NAME dentro de cada archivo .env y le asignamos valores diferentes dependiendo del entorno al que pertenezca.

De esta manera, cada vez que ejecutemos el archivo de composici贸n, dado que el nombre de proyecto es diferente para cada archivo .env, las modificaciones que se apliquen s贸lo afectar谩n al contenedor que corresponda con dicho nombre de proyecto.

馃檵鉂 Vale, bien, pero hemos usado la propiedad COMPOSE_PROJECT_NAME s贸lo dentro de nuestros archivos .env y no en el archivo docker-compose.yml. 驴C贸mo es posible que afecte al resultado final?

Es posible porque esa propiedad es le铆da directamente por el comando docker-compose y no es necesario que est茅 incluida dentro del archivo docker-compose.yml.

En este enlace puedes encontrar toda la documentaci贸n oficial about COMPOSE_PROJECT_NAME.

馃す鈥嶁檪锔 Inicializando la base de datos

馃敟 Advertencia: El proceso que se expone a continuaci贸n est谩 dirigido a inicializar el contenido de una base de datos MongoDB. Si quieres usar un motor diferente, necesitar谩s adaptar este proceso as铆 como la configuraci贸n del docker-compose.yml para ello. 馃敟

El concepto m谩s b谩sico que debemos saber, si es que no lo sabemos ya, es que cuando un contenedor basado en MongoDB se ejecuta por primera vez, todos los archivos con extensi贸n .sh o .js ubicados en el directorio /docker-entrypoint-initdb.d dentro del propio contenedor, son ejecutados.

Esto nos proporciona una manera para inicializar nuestra base de datos.

Si quieres profundizar en esta propiedad, puedes consultar la documentaci贸n de la imagen oficial de MongoDB en Docker.

馃И Configuraci贸n del entorno de testing

Para ver c贸mo podemos hacer esto, vamos a empezar por el entorno de testing as铆 que antes de nada, tenemos que crear la siguiente estructura de archivos dentro del directorio /docker/test de nuestro proyecto:

/
|-- /docker
|   |-- /dev
|   |   |-- .docker.dev.env
|   |-- /test
|   |   |-- /configureDatabase # <= Nuevo directorio y archivo.
|   |   |   |-- initDatabase.js
|   |   |-- .docker.test.env
|   |-- docker-compose.yml
|-- /node_modules
|-- /src
|-- package-lock.json
|-- package.json
Enter fullscreen mode Exit fullscreen mode

El contenido del archivo initDatabase.js ser谩 el siguiente:

Este script est谩 dividido en tres elementos diferentes.

La constante apiDatabases contiene todas las definiciones de bases de datos que queremos crear para nuestro contenedor.

Cada definici贸n de base de datos contendr谩 su nombre (dbName), un array de usuarios (dbUsers) los cuales estar谩n autorizados para operar con la base de datos (incluyendo la definici贸n de sus privilegios de acceso) y el conjunto de datos con los que inicializaremos la base de datos.

La funci贸n createDatabaseUser est谩 destinada a gestionar la informaci贸n contenida en cada bloque del apiDatabases, procesar los datos de usuarios y crearlos dentro de la base de datos indicada.

Finalmente, el bloque try/catch contiene la magia porque en este bloque iteramos sobre la constante apiDatabase, conmutamos entre bases de datos y procesamos la informaci贸n.

Una vez que hemos analizado este c贸digo, si recordamos el contenido de nuestro archivo docker-compose.yml, dentro de la secci贸n volumes definimos la siguiente l铆nea:

- ${CONFIGURATION_PATH}:/docker-entrypoint-initdb.d:rw

Adem谩s, para el entorno de testing, dentro del archivo .docker.test.env, configuramos lo siguiente:

CONFIGURATION_PATH="./test/configureDatabase"

Con esta acci贸n, el proceso docker-compose est谩 copiando el contenido de la ruta indicada por CONFIGURATION_PATH dentro del directorio del contenedor /docker-entrypoint-initdb.d:rw antes de que 茅ste se arranque por primera vez. As铆 es como estamos definiendo el script de configuraci贸n de nuestra base de datos, para que sea ejecutado al iniciarse el contenedor.

馃檵鈥嶁檧锔忊潛 Para esta configuraci贸n no est谩s usando ning煤n conjunto de datos iniciales. 驴Por qu茅?

Porque esta ser谩 la base de datos de testing y la intenci贸n es que se almacenen y eliminen datos ad-hoc en base a los tests que est茅n ejecut谩ndose en un momento concreto. Por esta raz贸n no tiene sentido que inicialicemos la base de datos con informaci贸n que vamos a crear/editar/eliminar din谩micamente.

馃洜 Configuraci贸n del entorno de desarrollo

Esta configuraci贸n es muy similar a la de testing.

Lo primero que tenemos que hacer es modificar el subdirectorio /docker/dev de nuestro proyecto, para que quede tal que as铆:

/
|-- /docker
|   |-- /dev
|   |   |-- /configureDatabase # <= Nuevo directorio y archivos.
|   |   |   |-- initDatabase.js
|   |   |   |-- postsDataToBePersisted.js
|   |   |   |-- usersDataToBePersisted.js
|   |   |-- .docker.dev.env
|   |-- /test
|   |   |-- /configureDatabase
|   |   |   |-- initDatabase.js
|   |   |-- .docker.test.env
|   |-- docker-compose.yml
|-- /node_modules
|-- /src
|-- package-lock.json
|-- package.json
Enter fullscreen mode Exit fullscreen mode

Los archivos postsDataToBePersisted.js y usersDataToBePersisted.js s贸lo contienen informaci贸n est谩tica definida dentro de constantes independientes. Esta informaci贸n ser谩 almacenad en la base de datos indicada, dentro de la colecci贸n especificada.

La estructura de dichos contenidos ser谩 la siguiente:

Por otro lado, el contenido del archivo initDatabase.js es bastante similar al del entorno de testing pero un poco m谩s complejo ya que ahora tenemos que gestionar colecciones y datos. De este modo, el resultado final es este:

En este script hay varias partes que necesitamos analizar.

En la cabecera tenemos un bloque compuesto por dos llamadas a la funci贸n load() encaminadas a importar los datos preparados y almacenados en las constantes que declaramos en los otros archivos JavaScript.

馃敟 Hay que prestar atenci贸n a que la ruta indicada para hacer referencia a los archivos de datos, es relativa al interior de la estructura de ficheros del contenedor de Docker y no a la de nuestro sistema. 馃敟

鈩癸笍 Si quieres aprender m谩s acerca de c贸mo ejecutar MongoDB archivos JavaScript en su consola de comandos, echa un vistazo a su documentaci贸n oficial.

Despu茅s de "importar" las definiciones de las constantes usersToBePersisted y postsToBePersisted mediante el uso de la funci贸n load(), estas est谩n disponibles globalmente dentro del contexto de nuestro script de inicializaci贸n.

El siguiente bloque a analizar es el de la constante apiDatabases donde, adem谩s de los campos dbName y dbUsers que ya vimos en la configuraci贸n de testing, en este caso el array dbData es un poco m谩s complejo.

Cada objeto declarado dentro del array dbData define el nombre de la colecci贸n as铆 como el conjunto de datos que debe ser almacenado en dicha colecci贸n.

Ahora nos encontramos con la definici贸n de la constante collections. Es la definici贸n de un mapa de funciones el cual contiene las acciones que se deben ejecutar por cada colecci贸n definida en el bloque apiDatabases.dbData.

Como podemos ver, en estas funciones estamos invocando directamente instrucciones nativas de MongoDB.

La siguiente funci贸n que nos encontramos es createDatabaseUsers la cual no tiene diferencias con la que definimos para el entorno de testing.

Justo antes de terminar el archivo, encontramos la funci贸n populateDatabase.

En esta funci贸n es donde vamos a trav茅s de las colecciones de bases de datos, insertando los datos asignados y aqu铆 es donde invocamos al mapa de funciones collections.

Finalmente tenemos el bloque try/catch donde ejecutamos las mismas acciones que para el entorno testing pero hemos incluido la llamada a la funci贸n populateDatabase.

De esta manera es como hemos podido configurar el script de inicializaci贸n para nuestra base de datos del entorno de desarrollo.

馃З Comando de Docker Compose

Una vez que hemos definido el archivo de composici贸n as铆 como el conjunto de datos que inicializar谩 nuestra base de datos, tenemos que definir los campos mediante los cuales, operaremos nuestros contenedores.

馃敟 Hay que prestar especial atenci贸n al hecho de que las rutas empleadas est谩n referenciadas a la ra铆z de nuestro proyecto. 馃敟

馃専 Configurando los 煤ltimos detalles para NodeJS

El 煤ltimo paso es definir los scripts necesarios dentro de nuestro archivo package.json.

Para proporcionar una mejor modularizaci贸n de los scripts, es muy recomendable que se dividan en diferentes scripts at贸micos y luego, crear otros scripts diferentes para agrupar aquellos que sean m谩s espec铆ficos.

Por ejemplo, en este c贸digo hemos definido los scripts dev_infra:up, dev_infra:down, test:run, test_infra:up and test_infra:down que son at贸micos porque definen una acci贸n simple y ser谩n los encargados de arrancar y para los contenedores para cada entorno, as铆 como de ejecutar la suite de test.

Por el contrario tenemos los scripts build:dev y test que son compuestos ya que cada uno involucra varios scripts at贸micos.

馃 FAQ

驴Qu茅 pasa si la suite de testing se para repentinamente porque alguno de los tests ha fallado?

No hay que preocuparse por esto porque es verdad que la infraestructura de testing continuar谩 ejecut谩ndose pero tenemos dos opciones:

  1. Mantener en ejecuci贸n el contenedor ya que la pr贸xima vez que ejecutemos la suite de tests, el comando docker-compose actualizar谩 el contenido del contenedor.
  2. Ejecutar manualmente el script de apagado del contenedor de testing.

驴Qu茅 sucede si en lugar de una base de datos, necesitamos ejecutar alg煤n servicio m谩s complejo como una API?

S贸lo necesitamos configurar los contenedores/servicios necesarios dentro del archivo docker-compose.yml, prestando especial atenci贸n a la configuraci贸n .env para cada entorno.

No importa lo que queramos incluir en nuestros contenedores. Lo importante aqu铆 es que vamos a ser capaces de arrancarlos y detenerlos cuando nuestro proyecto lo necesite.

馃憢 Conclusiones finales

Con esta configuraci贸n podemos incluir la gesti贸n de la infraestructura necesaria para nuestros proyectos con NodeJS.

Este tipo de configuraciones nos proporciona un nivel de desacoplamiento que aumenta nuestra independencia durante la fase de desarrollo, ya que vamos a tratar elementos externos a nuestro c贸digo como una caja negra con la cual interactuar.

Otro punto interesante de esta estrategia es que cada vez que arrancamos el contenedor mediante el comando docker-compose, 茅ste es totalmente renovado lo que nos permite asegurar que nuestras suites de tests van a ejecutarse sobre sistemas completamente limpios.

Adem谩s, mantendremos limpio nuestro propio sistema ya que no necesitaremos instalar ning煤n tipo de aplicaci贸n auxiliar porque todas ellas, estar谩n incluidas en diferentes contenedores que compondr谩n nuestras infraestructura de pruebas.

S贸lo una advertencia a este respecto, trata de mantener el contenido de dichos contenedores lo m谩s actualizado posible para, de ese modo, hacer las pruebas contra un entorno lo m谩s parecido posible al que podemos encontrarnos en producci贸n.

Espero que este contenido te sea 煤til. Si tienes cualquier pregunta, si茅ntete totalmente libre de contactar conmigo. Aqu铆 est谩n mis perfiles de Twitter, LinkedIn y Github.

馃檹 Reconocimientos y agradecimientos

  • Jonatan Ramos por darme la pista del COMPOSE_PROJECT_NAME para crear archivos docker-compose.yml 煤nico que se comparten entre diferentes entornos.

Top comments (0)