Cómo crear y desplegar microservicios con Spring Boot, Spring Cloud Netflix y Docker

0
14662

En este tutorial vamos a aprender cómo crear microservicios con Spring Boot, cómo configurar servicios que utilicen los componentes Eureka y Zuul de Spring Cloud Netflix y cómo desplegarlos en contenedores Docker.

Índice de contenidos

Introducción

La arquitectura de microservicios es una arquitectura que recientemente, ha ganado mucha popularidad, existiendo casos de éxito reconocidos mundialmente como Netflix o Amazon. Esta arquitectura conlleva un gran esfuerzo, pero para aplicaciones que necesiten ser escalables y muy flexibles es una arquitectura ideal. Si quieres saber más sobre esta arquitectura, puedes consultar esta guía.

En primer lugar, vamos a explorar cómo Spring Cloud Netflix nos ayuda a aplicar patrones fundamentales en la arquitectura de microservicios, como lo son el patrón de Descubrimiento de Servicios (mediante Eureka) y el patrón de API Gateway (mediante Zuul). Si quieres leer más información al respecto de Spring Cloud Netflix, puedes consultar este tutorial de nuestro compañero Jose Mangialomini.

Una vez creados estos servicios, los desplegaremos utilizando Docker. Esta parte es el motivo detrás de este tutorial, ya que hice un Trabajo de Fin de Grado sobre esta arquitectura, en el que me quedé con ganas de desplegar los servicios utilizando Docker. Si os pica la curiosidad podéis verlo aquí. Dicho esto, vamos a por ello.

Entorno

  • Hardware: MacBook Pro 13′ (2,6GHz IntelCore i5, 8GB DDR3 RAM).
  • Sistema Operativo: MacOS Catalina 10.15.7
  • Maven: Versión 3.6.3
  • Java: Versión 1.8.0_261
  • Docker Engine: Versión 19.03.13

Servicio de Spring Cloud Netflix: Eureka

Primero vamos a crear el servicio Eureka que actuará como un servidor en el que se registrarán todas las instancias de microservicios que despleguemos.

Crearemos los proyectos de Spring Boot con Spring Initializr, que es una herramienta que nos creará el esqueleto del proyecto, junto con las dependencias que nosotros elijamos. La configuración en Spring Initializr debe ser la siguiente:

InitializrEureka

Tras crearlo, lo descomprimimos y abrimos con nuestro IDE favorito. No hace falta que toquemos su pom.xml, ya que vendrá de serie con la configuración y dependencias que hemos definido, pero si se nos ha olvidado algo o queremos añadir cualquier cosa, siempre podremos hacerlo mayor sin problema.

Según la configuración que hemos elegido, debería quedar así:

Tras comprobar que las dependencias están correctamente definidas, vamos a la clase Main del proyecto, que tendremos que anotar con @EnableEurekaServer para activar la autoconfiguración como Servidor Eureka.

Tras habilitar la configuración de Eureka, definimos la configuración de la aplicación en el archivo application.yml. Este archivo está ubicado en la carpeta src/main/resources y originalmente se llamará application.properties, pero personalmente, prefiero la extensión .yml, ya que resulta más legible, pudiendo cambiarla simplemente renombrando el archivo.

En él, daremos nombre a la aplicación, configuraremos el puerto del servidor embebido Tomcat y las propiedades de configuración de Eureka, que en este caso son fetch-registry y register-with-eureka con valor false, para que no se auto-registre, ya que el servidor Eureka contiene también un cliente de Eureka dentro de sí mismo.

Una vez terminados todos estos pasos, procedemos a generar el fichero .jar de esta aplicación, que posteriormente incluiremos en un contenedor para poder ejecutarla. Ejecutamos el siguiente comando para generarlo:

Es muy importante que siempre que realicemos algún cambio en la aplicación, volvamos a ejecutar este comando para actualizar el fichero .jar para poder disponer de esos cambios dentro de este fichero, ya que simplemente guardando los archivos no conseguiríamos esto.

Servicio de Spring Cloud Netflix: Zuul

Tras crear el servicio Eureka, vamos a crear el servicio Zuul. Este servicio implementa el patrón de API Gateway, actuando como una «puerta» a través de la cuál entrarán las peticiones a nuestra aplicación y distribuyéndolas a los servicios correspondientes.

Creamos el proyecto con Spring Initializr con la siguiente configuración y dependencias:

InitializrZuul

Si nos damos cuenta, este proyecto tendrá la versión 2.3.6 de Spring Boot, pero en el de Eureka hemos elegido la 2.4, ya que Zuul no está disponible para esa versión. ¿Esto podría causar que nuestra aplicación no funcionase? Por supuesto que no, ya que aquí es donde entra en juego una de las características más atractivas de los microservicios, que es su independencia de la implementación en la comunicación mediante APIs de tipo REST. Eureka está construido con este tipo de API, por lo que puede recibir peticiones desde aplicaciones Java o no-Java, permitiéndonos registrar servicios de todo tipo. Puede parecer una diferencia muy sutil y casi imperceptible, pero es de una importancia vital aclarar este concepto de cara a desarrollar aplicaciones basadas en microservicios.

Una vez creado, lo abrimos con el IDE que queramos y procedemos a configurarlo. Antes de nada, nos aseguramos de que su pom.xml tenga el siguiente aspecto:

El siguiente paso será añadir la anotación @EnableZuulProxy en su clase Main, que activará la configuración de Zuul para esta aplicación.

Una vez activada la configuración, debemos definirla en el application.yml. Este servicio es bastante especial, ya que tenemos que registrar las rutas de los microservicios que queramos exponer en su configuración, además de registrarlo en Eureka.

En este caso, definimos una ruta para el futuro microservicio que vamos a crear. La ruta de registro en Eureka es un poco particular, ya que en los ejemplos habituales suele ser algo como localhost:8761, pero en nuestro caso, vamos a usar el nombre del servicio de eureka (porque localhost en Docker no nos vale). Haremos todo esto de la siguiente manera:

Al igual que en el anterior servicio, terminamos la creación de este generando el .jar ejecutando en la raíz del proyecto el comando:

Microservicio de ejemplo

Este servicio es un ejemplo muy simple, pero podríamos implementar cualquier cosa que nos imaginemos, teniendo siempre claro que los microservicios casi siempre deben tener una única responsabilidad.

Lo crearemos con la siguiente configuración y dependencias:

InitializrMicroservice

Lo descomprimimos y abrimos con el IDE que nos apetezca en ese momento y comenzamos a desarrollar la funcionalidad más simple del mundo. Su pom.xml debería quedar así:

Una vez comprobado el pom.xml, debemos anotar la clase Main con @EnableDiscoveryClient, para que Eureka pueda registrar este servicio.

Ahora vamos a desarrollar la funcionalidad de este microservicio que será simplemente, devolver una cadena que nos diga el puerto en el que está configurado su servidor embebido Tomcat.

Para ello, desarrollaremos un Controlador, una Interfaz de Servicio y una implementación de la misma, dentro del mismo paquete que la clase Main.

La interfaz debería quedar así:

Una vez creada la interfaz, la implementaremos en otra clase:

Y por último, el controlador:

Una vez implementados el controlador y el servicio, vamos a terminar definiendo la configuración de este servicio en su application.yml, que será muy sencilla:

Una vez guardados todos los archivos, generamos el .jar de la aplicación ejecutando en la raíz del proyecto el comando:

Creación de imágenes

Tras el proceso de desarrollo, entramos en el mundo de DevOps, donde crearemos los archivos y configuraciones necesarias para desplegar nuestros tres servicios en Docker.

El proceso es bastante sencillo, ya que Docker nos ofrece Dockerfile como herramienta para crear imágenes de contenedores. Una imagen es una plantilla a través de la cuál, Docker creará los contenedores cuando así queramos, pero NO es un contenedor (es muy importante tener los conceptos claros para poder saber de qué estamos hablando, ya que es muy común confundir los términos imagen y contenedor).

El proceso es el siguiente. Iremos a la raíz de cada uno de los proyectos (o el directorio que queramos, pero en este ejemplo trabajaremos en el raíz), y en ella crearemos un archivo llamado Dockerfile. En este archivo, definiremos tres propiedades, aunque el mundo de Docker va mucho más allá, este ejemplo básico nos servirá para poder ejecutar la aplicación. Este Dockerfile será el de Eureka:

  • La propiedad FROM es la imagen base que vamos a tomar, que será la imagen openjdk:8-jdk-alpine, constituida por una distribución de Linux extremadamente ligera y un jdk para ejecutar aplicaciones Java.
  • ADD nos servirá para crear un directorio en el que alojaremos el ejecutable de cada una de nuestras aplicaciones.
  • Y ENTRYPOINT será el comando que se ejecute cuando se levante el contenedor con esta imagen, el cuál ejecutará nuestra aplicación usando el fichero .jar.

Para los que estéis familiarizados con Docker, quizá notéis la ausencia de la propiedad EXPOSE, que nos permite exponer ese puerto en concreto al exterior. La explicación viene más adelante, pero tiene que ver con cómo haremos que se comuniquen los microservicios.
Ya que sabemos qué significan estas propiedades, podemos crear los Dockerfile del resto de aplicaciones.

Dockerfile de Zuul:

Dockerfile de GreetingMicroservice:

Creación de contenedores y despliegue

Una vez creadas las imágenes de los servicios, de alguna manera tenemos que usarlas para crear los contenedores en los que se ejecutarán estos servicios. Docker ofrece la herramienta docker-compose, que nos ahorrará ejecutar un comando para levantar cada contenedor, que en este ejemplo sólo serían tres, pero para casos con muchos más servicios, nos permite centralizar la configuración de despliegue, lo cuál es mucho más práctico.

La sintaxis de este fichero es bastante sencilla, y podemos alojarlo donde queramos, ya que lo único que necesitamos son las imágenes creadas anteriormente, que ya están subidas a nuestro repositorio de imágenes en local.

Deberíamos configurarlo tal que así:

Como apunte, es una buena práctica crear los servicios dentro de una red, lo que permitirá que los servicios que se ejecuten dentro de ella tendrán acceso a todos los puertos de los servicios que se estén ejecutando dentro de ella (por eso no necesitamos la propiedad EXPOSE en los Dockerfiles), además de bloquear todos los puertos al exterior, creando un entorno seguro para nuestras aplicaciones.

Los puertos que definimos en los servicios serán los únicos que vamos a permitir que se salten el «bloqueo» de la network, por ejemplo, para el servicio Eureka, usaremos el puerto del contenedor 8761 y será accesible desde mi host en el puerto 8761, de ahí la sintaxis «doble» 8761:8761.

Tenemos otras alternativas a crear una network, por ejemplo link, que sirven para conectar dos o más contenedores, pero su configuración es mucho más tediosa, además de ser legacy, lo que significa que Docker en algún momento eliminará esta característica.

Por último, sólo nos queda desplegar los contenedores. Lo haremos con el siguiente comando, donde -d significa que los contenedores se ejecutarán dejándonos la terminal libre y sin imprimir sus salidas en ella:

Podemos monitorizar el proceso de despliegue desde Docker Dashboard, donde la red que acabamos de crear tendrá todos los logs de los tres servicios.

Para ver que la aplicación funciona correctamente, podemos verlo en las siguientes direcciones.

  • En http://localhost:8761 veremos el panel de control de Eureka, que tiene información sobre los servicios desplegados y su salud.
  • En http://localhost:7000/greeting-service/greet veremos que el servicio nos responde con: Hello from port: 8001.

No podremos acceder directamente con el GreetingMicroservice, por una razón muy sencilla. A la hora de levantar el contenedor de este servicio, no hemos definido ningún puerto para que sea accesible desde el host, por lo que el acceso a sus recursos sólo será a través de Zuul.

Conclusiones

Con este ejemplo he querido ilustrar cómo funciona la arquitectura de microservicios con Spring Cloud Netflix y Docker, en la que todos los servicios serán registrados y monitorizados por Eureka, y solicitaremos los recursos a través de Zuul, y el mensaje que recibimos es el que ha sido creado por el GreetingMicroservice, pero devuelto por Zuul. Hemos creado una red de contenedores relativamente segura, en la que no podremos acceder a los microservicios que se estén ejecutando dentro, excepto a Eureka y a Zuul porque así lo hemos definido en el docker-compose.

Referencias

Dejar respuesta

Please enter your comment!
Please enter your name here