Siguiendo con la serie de tutoriales dedicados a Docker, vamos a ver cómo desplegar un microservicio desarrollado con Spring Boot en un contenedor Docker.
Posteriormente veremos como escalar y balancear este microservicio a través de HAProxy.
0. Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. El microservicio
- 4. Dockerizar el microservicio
- 5. Escalando la solución
- 6. Conclusiones
- 7. Referencias
1. Introducción
En el tutorial Introducción a los microservicios del gran José Luis
vimos una introducción al concepto de microservicios, cuales son sus ventajas e inconvenientes y cuando podemos utilizarlos.
El objetivo que perseguimos con el presente tutorial es desarrollar un microservicio con Spring Boot,
empaquetarlo dentro de una imagen Docker, dentro de la fase de construcción de maven, y una vez podamos
levantarlo, ver una posibilidad de escalabilidad gracias a Docker Compose y HAProxy.
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: MacBook Pro 15' (2.3 GHz Intel Core i7, 16GB DDR3 SDRAM)
- Sistema Operativo: Mac OS X El Capitan 10.11
- Software: Docker 1.11.1, Docker Machine 0.7.0, Docker Compose 1.7.1
- Software: Spring Boot 1.4.0.M3
3. El Microservicio
El objetivo del tutorial no es tanto el desarrollo de microservicios con Spring Boot, sino su empaquetamiento y despliegue,
por tanto vamos a implementar un microservicio
‘tonto’ cuya única funcionalidad es devolvernos un mensaje de hola.
El código lo podeís encontrar en mi cuenta de github aquí, lo primero que deberíamos implementar es un
@RestControler como el que describimos a continuación:
package com.autentia; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MicroServiceController { private final AddressService service; @Autowired public MicroServiceController(AddressService service) { this.service = service; } @RequestMapping(value = "/micro-service") public String hello() throws Exception { String serverAddress = service.getServerAddress(); return new StringBuilder().append("Hello from IP address: ").append(serverAddress).toString(); } }
Como podemos observar es un ejemplo muy sencillo, hemos declarado un controlador rest, al cual le hemos inyectado un servicio que recupera la IP del servidor, y devuelve un string del tipo
«Hello from, IP address xx.xx.xx.xx»
La clase principal de nuestro microservicio encargada levantar un tomcat embebido con nuestro microservicio tendría un aspecto parecido a este:
package com.autentia; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MicroServiceSpringBootApplication { public static void main(String[] args) { SpringApplication.run(MicroServiceSpringBootApplication.class, args); } }
Podemos levantar nuestro servicio de la siguiente manera:
mvn clean spring-boot run
Una vez levantado el microservicio podemos invocarlo de la siguiente manera:
curl http://localhost:8080/micro-service
Hasta ahora nada impresionante … pasemos al siguiente punto
4. Dockerizar el microservicio
En este apartado vamos a ver como podemos ‘empaquetar’ nuestro microservicio dentro de un contenedor docker,
para ello vamos a usar el plugin de maven spotify/docker-maven-plugin.
Antes de meternos de lleno en el uso de este plugin, vamos a generar un Dockerfile de nuestro microservicio,
para ello nos creamos un directorio src/main/docker y creamos nuestro Dockerfile de la siguiente manera:
FROM frolvlad/alpine-oraclejdk8:slim MAINTAINER jpacheco@autentia.com ADD micro-service-spring-boot-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
Repasemos el Dockerfile:
- FROM: Tomamos como imagen base frolvlad/alpine-oraclejdk8
esta imagen está basada en Alpine Linux que es una distribución Linux de sólo 5 MB, a la cual se le ha añadido la OracleJDK 8. - ADD: Le estamos indicando que copie el fichero micro-service-spring-boot-0.0.1-SNAPSHOT.jar al contenedor con el nombre app.jar
- EXPOSE: Exponemos el puerto 8080 hacia fuera (es el puerto por defecto en el que escuchará el tomcat embebido de nuestro microservicio)
- ENTRYPOINT: Le indicamos el comando a ejecutar cuando se levante el contenedor, como podemos ver es la ejecución de nuestro jar
El siguiente paso en añadir el plugin a nuestro pom.xml de la siguiente manera
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <docker.image.prefix>autentia</docker.image.prefix> </properties> ..... <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>${docker.image.prefix}/${project.artifactId}</imageName> <dockerDirectory>src/main/docker</dockerDirectory> <serverId>docker-hub</serverId> <registryUrl>https://index.docker.io/v1/</registryUrl> <forceTags>true</forceTags> <imageTags> <imageTag>${project.version}</imageTag> </imageTags> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> <executions> <execution> <id>build-image</id> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> <execution> <id>push-image</id> <phase>install</phase> <goals> <goal>push</goal> </goals> <configuration> <imageName>${docker.image.prefix}/${project.artifactId}:${project.version}</imageName> </configuration> </execution> </executions> </plugin>
En el apartado properties definimos:
- docker.image.prefix: que indica el prefijo de la imagen a generar
En el apartado configuration de la sección de plugins definimos los siguiente parámetros :
- imageName: Nombre de la imagen (prefijo + artifactId del proyecto)
- dockerDirectory: Directorio en el que se encuentra el Dockerfile definido anteriormente
- serverId: Identificador del registry de Docker (opcional: si queremos realizar un docker push a nuestro registry)
- registryUrl: URL del registry de Docker (opcional: si queremos realizar un docker push a nuestro registry)
- imageTag: Definimos las tags de nuestra imagen
- resource: Le indicamos el recurso que vamos a empaquetar dentro de la imagen (‘targetPath’ path base de los recursos, ‘directory’ directorio de los recursos, ‘include’ incluimos el jar resultante )
En el apartado executions vinculamos los goals del plugin a las fases de maven:
- build-image: Vinculamos a la fase package de maven, el goal docker:build que construye la imagen con el microservicio
- build-image: Vinculamos a la fase de install de maven, el goal docker:push que sube nuestra imagen al registro de docker
Una vez configurado podemos ejecutar alguno de los goals del plugin:
mvn clean package
Como se puede ver en los logs, después de realizar el empaquetado se construye la imagen. Comprobamos si se han generado la imagen:
docker images
Podemos observar que en nuestro registro local, están disponibles tanto la imagen base de la que hemos partido frolvlad/alpine-oraclejdk8:slim,
como nuestra imagen con los tags 0.0.1-SNAPSHOT y latest. El siguiente paso es arrancar un contenedor a partir de nuestra imagen
docker run -d -p 8080:8080 --name microservicio jpacheco/micro-service-spring-boot:0.0.1-SNAPSHOT
Con esto arrancamos nuestro contenedor, podemos comprobarlo ejecutando
docker ps
Una vez levantado el contenedor accedemos a nuestro servicio de manera análoga a la anterior sustituyendo ‘localhost’ por la IP de nuestro docker-machine
curl http://192.168.99.100:8080/micro-service
Podemos observar que la IP que devuelve es la IP interna del contenedor 172.17.0.2.
El último paso que nos quedaría para completar el ciclo seria realizar el ‘push’ de nuestra imagen a nuestro docker registry (es este ejemplo usaremos docker-hub,
como hemos definido en el pom.xml con los parámetros: serverId, registryUrl), nos faltaría añadir nuestras credenciales de docker-hub en el settins.xml de maven
<server> <id>docker-hub</id> <username>myuser</username> <password>mypassword</password> <configuration> <email>user@company.com</email> </configuration> </server>
Ya estamos listos para realizar un ‘push’ de nuestra imagen. Recordar que hemos vinculado el push a la tarea maven ‘install’
mvn install
Podemos acceder a nuestra cuenta de docker-hub y comprobar que se ha creado nuestra imagen
La cosa se empieza a poner interesante … ya tenemos nuestro microservicio empaquetado en un contenedor
y disponible en nuestro registry
5. Escalando la solución
El siguiente paso que vamos a estudiar, es como podemos lanzar varias instancias de nuestro microservicio
y como podemos balancear el trafico entre ellas.
Para esto vamos a usar HAProxy que es una herramienta open source que actúa como balanceador de carga (load balancer) ofreciendo alta disponibilidad,
balanceo de carga y proxy para comunicaciones TCP y HTTP.
Como no podía ser de otra manera, vamos a usar Docker para levantar el servicio HAProxy
Para ello vamos a usar docker-compose para definir tanto el microservicio, como el balanceador
microservice1: image: 'jpacheco/micro-service-spring-boot:latest' expose: - "8080" microservice2: image: 'jpacheco/micro-service-spring-boot:latest' expose: - "8080" loadbalancer: image: 'dockercloud/haproxy:latest' links: - microservice1 - microservice2 ports: - '80:80'
Como podemos ver en el fichero docker-compose.yml, hemos definido 2 instancias de nuestro microservicio (microservice1, microservice2) y un balanceador (loadbalancer) con enlaces a
los microservicios definidos anteriormente. Lo que conseguimos con esta imagen de HAProxy es exponer el puerto 80 y
redirigir a los 2 microservicios expuestos en el 8080 usando una estrategia round-robin.
Levantamos nuestro entorno con:
docker-compose up -d
y podemos observar como se levantan los 3 contenedores
Vamos a invocar a nuestro microservicio a través del balanceador:
Como podemos observar en la consola, el balanceador va accediendo cada vez a una instancia del microservicio, logrando el balanceo de carga que íbamos buscando … No está mal no?, el ejemplo va tomando ‘cuerpo’.
Pero.. ¿y si queremos levantar más instancias de nuestro microservicio? ¿Tenemos que modificar el docker-compose.yml, y añadir ‘microservice3…microserviceN’?
Revisando la documentación de la imagen HAProxy encontramos una solución a esta problemática, la idea es levantar una primera instancia del balanceador y del microservicio
y posteriormente en función de las necesidades, levantar más instancias del microservicio y que el balanceador se reconfigure para añadirlas. Veamos como quedaría el docker-compose.yml
version: '2' services: microservice: image: 'jpacheco/micro-service-spring-boot:latest' expose: - '8080' loadbalancer: image: 'dockercloud/haproxy:latest' links: - microservice volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - '80:80'
Repasemos las lineas más destacadas:
- version: ‘2’ estamos indicando que use la v2 de docker-compose (necesaria para este ejemplo)
- service: Tag raíz del que cuelgan nuestros contenedores
- microservice: loadbalancer Definición de nuestros contenedores
- volumes: El contenedor de HAProxy necesita acceder al ‘docker socket’ para poder detectar nuevas instancias y reconfigurarse
Vamos a levantar nuestros contenedores:
docker-compose -f docker-composeV2.yml up -d
Vemos como se han levantado una instancia del balanceador y otra del microservicio. Ahora vamos a escalar nuestro microservicio añadiendo 2 instancias más:
docker-compose -f docker-composeV2.yml scale microservice=3
Comprobamos que se han creado 2 nuevas instancias de nuestro microservicio, ahora vamos a probar que estas instancias se han añadido al balanceador:
Como podemos ver, cada petición es atendida por una instancia distinta …. podríamos ir añadiendo instancias según vayamos necesitando, bastaría con ejecutar
docker-compose -f docker-composeV2.yml scale microservice=<Instancias_vivas+Nuevas>
6. Conclusiones
Como hemos podido ver a lo largo del tutorial, la combinación de Spring Boot y Docker nos permite desarrollar facilmente microservicios, incluso montar infraestructuras que
permitan su escalabilidad. El siguiente paso sería investigar la posibilidad de escalarlo a través de un cluster de máquinas usando herramientas como Docker Swarn
o Kubernetes, pero eso os lo dejo a vosotros 😉
Un saludo.
¡Muchas gracias por tu post! Siempre son de utilidad. Sin duda, tenemos que empezar a desenvolvernos con los microservicios. Son una tendencia que cada día más se están convirtiendo en realidad. A nosotros, Chakray, nos encanta WSO2 como opción para implementar microservicios en una gran empresa.
De hecho, tenemos un ebook sobre cómo implementar una arquitectura de microservices con esta tecnología. ¡Espero que lo encontréis interesante!. Os dejo el link:
http://www.chakray.com/conoce-como-implementar-una-arquitectura-de-microservices/
Gran artículo muy interesante. De gran ayuda.
Muchas gracias.
Me ha resultado muy útil el tutorial y al final me he decantado por una solución algo más de andar por casa como el usar el reverse proxy con NginX (jwilder/nginx-proxy). Lo he probado en http://www.dondocker.com
Hola, quisiera saber que pasa con el consumo de RAM al empaquetarlo en un docker, lo digo por que un microservicio construido con spring-boot, al instanciar su propio contenedor consume alrededor de 500 a 600 mb, en un docker esto se incrementa?, es posible tener más de un microservicio en un docker?
Muy bueno el artículo, soy Gerente Regional de Intway Argentina
Saludos