En este tutorial veremos como asociar el ciclo de vida de un contenedor docker al ciclo de vida de un contexto de Spring mediante el uso de beans.
Índice de contenidos
- 1. Introducción
- 2. Entorno
- 4. Configurando el contexto de Spring
- 5. La clase DockerManager
- 6. La prueba
- 7. Conclusiones
- 8. Referencias
1. Introducción
En el presente tutorial vamos a mostrar una de las múltiples maneras mediante las cuales podemos gestionar el ciclo de vida de uno o varios contenedores de Docker, en concreto mediante un objeto asociado al ciclo de vida del contexto de Spring.
Como veíamos en el tutorial de Jorge Pacheco, Docker for dummies, Docker es una herramienta que nos proporciona un entorno mediante el cual realizar «virtualizaciones ligeras».
Como pre-requisito necesitaremos tener el entorno de docker instalado en nuestro equipo, así como algunas nociones básicas de la tecnología, así que si aún no te has pasado por el anterior tutorial, este es el momento.
Nota: A lo largo del tutorial se va a hacer referencia a la IP asignada a la máquina virtual que genera docker-machine (192.168.99.100), está puede cambiar dependiendo de la configuración propia de cada sistema. Para obtener dicha IP solo hace falta ejecutar el comando «docker-machine ip default», donde «default» es el nombre de la máquina virtual de la que se va a hacer uso.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: MacBook Pro 17′ (2.66 GHz Intel Core i7, 8GB DDR3 SDRAM).
- Sistema Operativo: Mac OS X Lion 10.10.3.
- NVIDIA GeForce GT 330M 512Mb.
- Crucial MX100 SSD 512 Gb.
- Eclipse Mars 4.5.0.
- Java 1.8
- MySQL 5.7
- Docker Toolbox 1.8.2
- Docker 1.7.1
-
Spring Framework v4.2.1.RELEASE
- spring-core
- spring-context
- spring-jdbc
- spring-tx
- spring-test
- JUnit 4.12
- MyBatis 3.3.0
3. Preparando el entorno
Para demostrar el funcionamiento de esta configuración vamos a proceder con un ejemplo sencillo: pruebas de integración contra una base de datos MySQL «dockerizada«.
La preparación del entorno no tiene misterio alguno. Hemos procedido con la creación de un proyecto Maven y definido las dependencias para Spring, JUnit, MyBatis y la base de datos, como puede verse a continuación.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>autentia.examples</groupId> <artifactId>springwithdocker</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springwithdocker</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>4.2.1.RELEASE</spring.version> <mybatis.version>3.3.0</mybatis.version> <mybatis.spring.bridge.version>1.2.3</mybatis.spring.bridge.version> <mysql.connector.version>5.1.36</mysql.connector.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- Spring dependencies --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <!-- Mybatis dependencies --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.bridge.version}</version> </dependency> <!-- MySQL dependencies --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies> </project>
Además de esto, nos hemos hecho con la última versión del contenedor de MySQL a través del repositorio de MySQL en DockerHub.
4. Configurando el contexto de Spring
Procedemos con la configuración del contexto de Spring que utilizaremos en nuestras pruebas de integración, y que alojaremos en la carpeta src/test/resources de nuestro proyecto.
applicationContext-test.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> <context:component-scan base-package="autentia.examples.springwithdocker.mappers" /> <tx:annotation-driven /> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <bean name="dockerManager" class="autentia.examples.springwithdocker.DockerManager" /> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="autentia.examples.springwithdocker.models"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="autentia.examples.springwithdocker.mappers" /> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://192.168.99.100:3307/prueba" /> <property name="username" value="root"/> <property name="password" value=""/> </bean> </beans>
La mayoría de las declaraciones se corresponden con configuración de lo que sería una aplicación Java que hace uso de MyBatis (podéis repasar este tutorial de Rubén Aguilera por si algo no os cuadra MyBatis con Maven y Spring).
Además introducimos un bean de la clase DockerManager, la explicamos a continuación.
5. La clase DockerManager
Esta es la clase que hemos creado para proceder con las operaciones necesarias para poner en funcionamiento el contenedor Docker de MySQL.
Nos hemos ayudado de las anotaciones @PostConstruct y @PreDestroy, que nos permiten establecer lógica que se ejecutará en la inicialización de un bean y en la destrucción del mismo.
DockerManager.java
public class DockerManager { @PostConstruct public void initialize() throws IOException{ final ProcessBuilder pb = new ProcessBuilder("/Users/jrodriguez/Documents/laboratory/workspace2/springwithdocker/src/test/resources/dockerInitScript.sh"); final Process p = pb.start(); ... } @PreDestroy public void destroy() throws IOException{ final ProcessBuilder pb = new ProcessBuilder("/Users/jrodriguez/Documents/laboratory/workspace2/springwithdocker/src/test/resources/dockerDestroyScript.sh"); final Process p = pb.start(); ... } ... }
Como vemos, la lógica de los métodos consta de la ejecución de un script de línea de comandos. Estos scripts mantienen las instrucciones necesarias para poner en marcha el contenedor de Docker y pararlo, respectivamente.
dockerInitScript.sh
#!/bin/bash export DOCKER_HOST=tcp://192.168.99.100:2376 export DOCKER_CERT_PATH=/Users/jrodriguez/.docker/machine/machines/default export DOCKER_TLS_VERIFY=1 export DOCKER_MACHINE="default" export PATH=$PATH:/usr/local/bin:/usr/local/mysql/bin dockermachinestatus=$(docker-machine status default | grep "Stopped") if [[ ! -z "$dockermachinestatus" ]]; then docker-machine start default fi eval "$(docker-machine env default)" mysqlimage=$(docker images | grep "jrodriguez/mysql") if [[ -z "$mysqlimage" ]]; then echo "Building mysql image" docker build -t jrodriguez/mysql /Users/jrodriguez/Documents/laboratory/workspace2/springwithdocker/src/test/resources/ else echo "Mysql image already builded" fi mysqlcontainer=$(docker ps -a | grep "mysql1") if [[ -z "$mysqlcontainer" ]]; then docker run --name mysql1 -d -p 3307:3306 jrodriguez/mysql sleep 20 mysql -h 192.168.99.100 -P 3307 -uroot -pprueba123 prueba < /Users/jrodriguez/Documents/laboratory/workspace2/springwithdocker/src/test/resources/Dump20150914.sql else docker unpause mysql1 fi
En este script de bash realizamos tareas rutinarias como son:
- Levantar la máquina virtual con docker-machine.
- Compilar una nueva imagen de mysql a partir de la original.
- Lanzar el contenedor docker con la imagen de mysql, en caso de ser la primera vez volcamos un dump con la estructura base de la base de datos, en caso contrario «despausamos» contenedor.
Cada una de las acciones establece previamente una comprobación para ser realizada solo cuando sea necesario.
Como se puede ver en el script, ha sido necesario establecer un tiempo de pausa en la ejecución del script puesto que es necesario esperar a que el contenedor y el servidor MySQL estén listos para trabajar antes de continuar con la recuperación del dump de la base de datos, así como con las pruebas de integración. De otra manera, al intentar ejecutar las pruebas de integración, recibiríamos un mensaje informando de que no se puede conectar contra la base de datos ó que la base de datos no mantiene la estructura adecuada.
Para compilar la nueva imagen de docker hacemos uso del siguiente Dockerfile, en el que establecemos algunos valores básicos para el uso del contenedor:
Dockerfile
FROM mysql:5.7 ENV MYSQL_ROOT_PASSWORD prueba123 ENV MYSQL_DATABASE prueba ENV MYSQL_USER usuario_prueba ENV MYSQL_PASSWORD usuario_prueba EXPOSE 3306
Por último el script que parará la ejecución de docker, cuando el contexto de Spring sea destruido:
dockerDestroyScript.sh
#!/bin/bash export DOCKER_HOST=tcp://192.168.99.100:2376 export DOCKER_CERT_PATH=/Users/jrodriguez/.docker/machine/machines/default export DOCKER_TLS_VERIFY=1 export DOCKER_MACHINE="default" export PATH=$PATH:/usr/local/bin:/usr/local/mysql/bin docker pause mysql1
En este tutorial, hemos optado por pausar el contenedor de docker para que las ejecuciones sucesivas de las pruebas de integración sean lo más rápidas posibles y por ello se utilizan las instrucciones docker pause y docker unpause.
Siempre se puede optar por una aproximación más conservadora y, modificando el script dockerDestroyScript.sh, parar el contenedor, destruir la imagen y forzar que cada vez que se ejecute una prueba se compile una imagen nueva y levante la nueva instancia desde 0.
6. La prueba
DockerManagerExampleTest.java
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("file:src/test/resources/applicationContext-test.xml") @Transactional @Rollback(true) public class DockerBeanExampleTest { @Autowired private NombreMapper nombreMapper; @Test public void test() { final Nombre nombre = new Nombre(11, "11"); nombreMapper.insert(nombre); final List<Nombre> nombres = nombreMapper.getAll(); assertThat(nombres.size(), equalTo(1)); } }
En el código se puede ver un mapper de MyBatis con el que realizamos dos operaciones (inserción y recuperación) contra la base de datos y posteriormente comprobamos si coincide el número de registros que se espera.
Si todo esta bien, al ejecutar la prueba podrá verse en el terminal la salida por pantalla referente a las instrucciones que se van ejecutando desde el script.
7. Conclusiones
Como hemos visto, se puede enlazar de una manera fácil y rápida el ciclo de vida de un contenedor Docker con el ciclo de vida del contexto de Spring.
En ocasiones en las que los tiempos de respuesta no sean cruciales esta forma de trabajar puede ventajosa, como por ejemplo en el caso de querer realizar pruebas de integración específicas.
Sin embargo, se desaconseja por completo en situaciones donde:
- los tiempos sean importantes, ya que la recarga de contextos los penalizará considerablemente
- haya sincronización de eventos, ya que hay que tener en cuenta los tiempos de arranque de cada servicio
- …