Cuando estamos usando Maven y queremos levantar contenedores de Docker para, por ejemplo, ejecutar los tests de integración, es bastante típico usar alguno de los plugins específicos que existen. Donde a día de hoy posiblemente el más habitual es docker-maven-plugin de Fabric8.
Estos plugins suelen ser un recubrimiento sobre un cliente de Docker que al final se conecta con el Docker daemon para realizar las acciones necesarias. Si bien para casos sencillos son muy convenientes, y de hecho seguramente deberían ser la opción por defecto, el problema que tienen este tipo de plugins es que están más pensados para crear imágenes en vez de ejecutarlas, y sobre todo si queremos levantar distintos contenedores con dependencias entre ellos… la cosa se complica ?.
En este tutorial vamos a ver cómo podemos usar el simple plugin de Maven exec-maven-plugin
para quitarnos estos intermediarios y hacer lo que queramos directamente con el comando docker
.
De hecho en este tutorial utilizaremos el comando docker compose
que nos permitirá más flexibilidad a la hora de levantar distintos contenedores de forma simultánea.
Podéis encontrar todo el código de este tutorial en: https://github.com/alejandropg/tutorial-maven-docker-exec. Incluido un ejemplo de cómo ejecutarlo integrado con los GitHub Actions.
Índice
- Entorno
- Maven
pom.xml
- El script para preparar el arranque de los tests de integración:
docker-compose-it-up.sh
- El script para limpiar el entorno después de los tests de integración:
docker-compose-it-down.sh
- La configuración de
docker compose
- Conclusiones
- Sobre el autor
1. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 16″ (Apple M1 Pro, 32GB LPDDR5, 1TB SSD).
- Sistema Operativo: macOS Monterey 12.0.1.
- Java version: 11.0.13, vendor: Azul Systems, Inc., arch: “aarch64”, family: “mac”.
- Apache Maven 3.8.3.
- Docker Desktop 4.1.1 (69879) y también probado en 4.2.0 (70708).
2. Maven pom.xml
Para usar el plugin exec-maven-plugin
simplemente haremos la siguiente configuración en nuestro Maven pom.xml
.
...
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
<executable>docker-compose-it-up.sh</executable>
</configuration>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
<executable>docker-compose-it-down.sh</executable>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
Aquí se ve como simplemente nos enganchamos a la fase de pre-integration-test
y post-integration-test
para ejecutar un script para arrancar “up
” y parar “down
” el Docker compose.
He elegido usar un script y no hacerlo directamente aquí porque eso nos puede dar mucha flexibilidad a la hora de ejecutar cualquier otro tipo de configuración de arranque que necesitemos para nuestros tests de integración. Lo veremos más adelante con un ejemplo.
3. El script para preparar el arranque de los tests de integración: docker-compose-it-up.sh
docker-compose-it-up.sh
es un script sh normal y corriente, por lo que tendremos a nuestro alcance toda la potencia de la shell.
#!/usr/bin/env sh
DOCKER_COMPOSE_FILE=docker-compose-it.yml
_docker_compose_get_image_name() {
image_name=$(fgrep --no-filename --max-count=1 image "${DOCKER_COMPOSE_FILE}" | cut -d':' -f2-)
# remove leading whitespace characters
image_name="${image_name#"${image_name%%[![:space:]]*}"}"
# remove trailing whitespace characters
image_name="${image_name%"${image_name##*[![:space:]]}"}"
echo "${image_name}"
}
_docker_compose_pull_image() {
docker image inspect "${1}" 2>&1 1>/dev/null
image_exist=$?
if [ $image_exist != 0 ]; then
echo '[INFO] Pull IT Docker images...'
docker compose -f "${DOCKER_COMPOSE_FILE}" pull
fi
}
# We don't want that the images download affect to the timeout,
# so pull images before up if they are not in the local image repository
image_name=$(_docker_compose_get_image_name)
_docker_compose_pull_image "${image_name}"
echo '[INFO] Up IT Docker containers...'
expect <<EOD
set timeout 5
spawn docker compose -f "${DOCKER_COMPOSE_FILE}" up
expect "database system is ready to accept connections"
EOD
Este script seguramente es lo más complicado de todo el tutorial. Al principio encontramos un par de funciones de ayuda donde el propósito de estas es una pequeña optimización para no hacer el docker compose pull
constantemente y ahorrar unos preciosos segundos cuando lo estamos ejecutando en local. Es decir, si analizamos en detalle la función _docker_compose_pull_image
, podemos ver que lo que hace es comprobar si la imagen ya está en local o no. De no encontrarla intentará hacer un pull
de todo el Docker compose.
Luego la otra parte con “truco” es el uso del expect
. La intención del expect
es no terminar la ejecución del script hasta que se encuentre en la salida la línea especificada. De esta forma garantizamos que los tests de integración no empezarán a ejecutarse hasta que esté debidamente levantado el contenedor.
expect "database system is ready to accept connections"
Esto sería el equivalente a, por ejemplo, cuando usando el docker-maven-plugin
de Fabric8 y hacemos:
<wait>
<log>database system is ready to accept connections</log>
</wait>
También se define un timeout
de 5 segundos, de forma que si en este tiempo no se encuentra la línea especificada el script terminará abruptamente. Precisamente por este timeout es por lo que separamos el pull
de las imágenes del up
del Docker compose, ya que si no lo separamos será muy fácil que salte el timeout la primera vez que lo ejecutemos, cuando intente descargar las imágenes.
4. El script para limpiar el entorno después de los tests de integración: docker-compose-it-down.sh
De nuevo docker-compose-it-down.sh
es un script sh normal donde podremos ejecutar todo lo que necesitemos para limpiar el entorno tras ejecutar los tests de integración.
#!/usr/bin/env sh
echo '[INFO] Down IT Docker containers...'
docker compose --file docker-compose-it.yml down
Se ve como en nuestro caso simplemente paramos el Docker compose. Además aquí son comandos normalitos y no necesitamos usar ningún “truco” como vimos con el expect
en el punto anterior.
5. La configuración de docker compose
Para configurar el Docker compose usamos un fichero de configuración normal y corriente: docker-compose-it.yml
services:
it-ddbb:
image: postgres:13.3-alpine
ports:
- 5432:5432
volumes:
- it-ddbb:/var/lib/postgresql/data:delegated
environment:
POSTGRES_PASSWORD: password
volumes:
it-ddbb:
En el ejemplo solo levantamos un servicio, pero como es un fichero normal de configuración de Docker compose podríamos hacer cualquier cosa que este permita, como levantar n servicios, con dependencias entre ellos, montar volúmenes…
6. Conclusiones
Con este ejemplo estamos yendo un poco a “bajo nivel”, pero si os fijáis tampoco tanto, puesto que hay pocas lineas que no tuviéramos que poner de todas formas: nombre de la imagen, nombre de los volúmenes, en que fase del ciclo de Maven queremos engancharlo…
Y sin embargo conseguimos un aumento de flexibilidad enorme, ya que ponemos a nuestro alcance toda la potencia de la shell, de Docker y de Docker compose.
Así que desde luego esta puede ser una buena opción a tener en cuenta si vemos que se nos complica la cosa a la hora de levantar contenedores dentro del ciclo de vida de Maven.
7. Sobre el autor
Alejandro Pérez García (@alejandropgarci).
Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster.
Socio fundador de Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”.