En un ecosistema de microservicios, en el cual generalmente cada uno de ellos está desplegado en un contenedor independiente y efímero, es necesario mantener a todos los integrantes informados de la localización de sus similares.

Dentro del stack Spring Cloud Netflix, el encargado de llevar a cabo esta labor es Eureka. Hashicorp Consul, además de realizar las labores de descubrimiento de servicios, nos permite, en un mismo componente, mantener la configuración centralizada de las aplicaciones de todo el sistema.

En esta serie de dos artículos vamos a introducir Consul y su integración con Spring Cloud, para el descubrimiento de servicios y gestión de la configuración de forma centralizada.

Además, vamos a ver cómo un pequeño sistema de microservicios Java recogen sus valores de configuración y se comunican entre ellos gracias a Consul.

Hashicorp Consul

Consul es una herramienta de Hashicorp, cuya función principal es facilitar la comunicación entre servicios productores y consumidores, llevando a cabo el registro y descubrimiento de los mismos. Sus características más importantes son:

El concepto fundamental de Consul es Agente. Un agente es un proceso de larga duración que se ejecuta en cada nodo que forma parte del sistema Consul. Es el responsable de surtir de información de sí mismo y recoger información de los demás agentes. Se comunica vía HTTP y, además, integra un servidor DNS. Puede ejecutarse en dos modos: cliente o servidor.

Principales agentes de Consul

Ambos tipos de agentes son capaces de recibir peticiones. Los servicios que se registran en Consul pueden configurarse para que ataquen directamente a agentes de otro nodo incluidos agentes servidor, pero lo ideal sería tener un agente cliente corriendo en el propio nodo del servicio que sea el encargado de la comunicación y health check.

Si no se hace de esta manera y se centralizan todos los servicios del sistema en el mismo agente, en caso de caída de ese nodo, se pierde la disponibilidad de todos los servicios que maneja dicho agente.

Instalación y arranque de Consul server

Hay varias formas de instalar Consul. Se puede descargar el ejecutable directamente desde el portal web de Hashicorp Consul o utilizar una imagen Docker.

NOTA: Para la elaboración del artículo se ha trabajado con la versión 0.9.3 de Consul, ya que a partir de la versión 1.0.0 el API cambió. El documento JSON de respuesta a las peticiones de health check es distinto y provoca que los servidores Consul entiendan que los microservicios registrados no están sanos. En ese momento, la última versión de la librería de Spring Cloud para descubrir servicios con Consul era la 1.2.1 y no era compatible. Posteriormente con la versión 1.3.0 de Spring Cloud Consul se corrigió el problema.

Vamos a arrancar un agente Consul servidor. Para ello vamos a hacer uso de la imagen oficial que provee Docker Hub, ejecutando:


docker run --name=consul-server consul:0.9.3 consul agent -dev -client
<CLIENT_ACCESS_IP> -bind <CLUSTER_ACCESS_IP>

Al arrancar el contenedor, se inicia Consul en modo desarrollo -dev, lo que genera un agente Consul servidor in memory con todas las opciones de persistencia desactivadas.

Además, se le indica con -client y -bind las direcciones donde el agente expone sus interfaces HTTP/DNS para consumo de sus clientes (client address), y por donde se comunica con el resto de miembros del cluster (cluster address), respectivamente.

Como se ve en las trazas, se realiza el proceso de presentación de candidatos, votación y elección de líder.

A continuación, vamos a comprobar la lista de miembros del cluster de Consul, en el que se muestra el agente servidor arrancado:


docker exec -t consul-server consul members
-http-addr=<CONSUL_CLIENT_ACCESS_IP_PORT>

La imagen docker descargada incluye una interfaz gráfica (http://172.17.0.2:8500/ui) para administrar y comprobar toda la información necesaria relativa a Consul y los servicios registrados de una forma más cómoda.

Tiene varias secciones:

Spring Cloud Consul

Spring pone a nuestra disposición librerías para hacer uso de las capacidades de Consul de forma sencilla. Accediendo a Spring Initializr observamos dos posibles dependencias spring-cloud-consul:

Es este artículo nos vamos a centrar en el módulo de descubrimiento de servicios. En el siguiente se abordará la funcionalidad de configuración centralizada que también ofrece.

Consul Discovery

Dota a las aplicaciones que lo incluyan de la capacidad de Consul de descubrimiento y registro de servicios, atacando al API HTTP del mismo.

En el ejemplo que se muestra a continuación vamos a plantear el siguiente escenario:

Agente Consul cliente y servicio de respuesta

Primero vamos a crear el servicio que responde a solicitudes. Para ello se debe incluir la siguiente dependencia (no hay que olvidar que para ello estamos utilizando Gradle):

groovy

compile('org.springframework.cloud:spring-cloud-starter-consul-discovery')

Además, Consul necesita consultar la salud de los servicios registrados para tenerlos o no en cuenta al resolver las peticiones.

Para ello, por defecto, hace uso del endpoint /health, el cual disponibiliza Spring Boot Actuator de forma nativa y con solo incluir la dependencia tenemos health checking sin complicarnos demasiado:

groovy

compile('org.springframework.boot:spring-boot-starter-actuator')

A continuación, mediante propiedades de configuración, hay que indicar al servicio dónde está el agente Consul, y al agente Consul indicarle dónde se encuentra el propio servicio que se va a registrar.

También vamos a indicar al agente Consul que se registre por IP y la ruta de los health checks:


 spring:
  application:
    name: consul-response-service #Sobreescribe el nombre por defecto que asigna Consul
  cloud:
      consul:
         host: 172.17.0.3                 #Direccion del agente Consul
         port: 8500                       #Puerto HTTP del agente Consul
         discovery:
            port: 8080                    #Puerto del servicio
            prefer-ip-address: true       #Registrar el servicio con la IP no con el hostname
            healthCheckPath: /health      #Ruta para que Consul haga los health checks  

Por último, se debe anotar la aplicación con @EnableDiscoveryClient para que se registre en Consul. Además de registrarse, esa anotación permite localizar a los demás servicios que se hayan dado de alta en el sistema:


 @SpringBootApplication
 @EnableDiscoveryClient
 public class ConsulRequestApplication {...}

Una vez arrancado, Spring hace una llamada al agente configurado en la aplicación para darse de alta (PUT /v1/agent/service/register), indicando en el body un Json con los datos del servicio:


 {
   "ID":"consul-response-service-8080",
   "Name":"consul-response-service",
   "Tags":[ ],
   "Address":"172.17.0.3",
   "Port":8080,
   "Check":{
      "Interval":"10s",
      "HTTP":"http://172.17.0.3:8080/health"
   }
 }

Posteriormente, el agente Consul hace una petición hacia /health (o el endpoint indicado en configuración) para efectuar el health-check y marca el servicio como sano.

Esta petición la repite en un intervalo por defecto de 10s para seguir monitorizando el servicio, el cual es configurable cambiando la propiedad Interval.

Para la creación del docker que contiene tanto el agente Consul cliente como la aplicación java, he usado un dockerfile cuyo entrypoint ejecuta un script que arranca tanto el agente como la app.

Dockerfile:

 FROM openjdk:8-jdk-alpine
 VOLUME /tmp
 ADD build/libs/consul-response-service-0.0.1-SNAPSHOT.jar app.jar
 ADD consul consul
 ADD start.sh start.sh
 RUN \["chmod", "+x", "/start.sh"\]
 ENTRYPOINT /start.sh

Script start.sh:


./consul agent -data-dir=/tmp/consul -join=172.17.0.2 -bind=172.17.0.3 -client=172.17.0.3 &
 java ./app.jar && fg

Como se puede ver en el dockerfile, se incluye tanto el jar de la aplicación como el ejecutable de Consul, para posteriormente arrancarlos ambos en el script.

Para arrancar el agente cliente Consul esta vez no hacemos uso de la imagen docker oficial, sino que usamos el ejecutable (v0.9.3) sin las etiquetas -dev o -server, las cuales indican que se debe levantar en modo servidor.

Al igual que el agente servidor, indicamos las client y bind address e incluimos la etiqueta -join en la que se referencia la dirección de cualquier nodo del cluster, para que este nuevo agente se comunique con él y forme parte del mismo.

Se deberían informar todos los Consul servers del cluster, ya que si solo referenciamos uno y se cae, el agente quedará aislado. Esto aplica tanto a los agentes cliente como los servidor. Para ello se repite la etiqueta -join= todas las veces necesarias.

Al no ser modo desarrollo se le debe indicar el directorio de persistencia de información -data-dir.

Una vez arrancado el contenedor, se ve el servicio registrado en la UI de Consul, y el agente cliente unido al cluster:

Aplicación cliente

A continuación, vamos a generar un contenedor con el servicio cliente, que preguntará al agente Consul servidor dónde está el servicio productor de la información y solicitará una respuesta.

Para ello, también anotaremos la aplicación Spring Boot con @EnableDiscoveryClient, pero al ser un servicio de consulta externo no queremos que se registre en Consul, solo que sea capaz de hacer solicitudes. Configuramos sus propiedades de la siguiente manera:


 spring:
   application:
     name: consul-request-service
   cloud:
       consul:
          host: 172.17.0.2
          port: 8500
          discovery:
             register: false

Se referencia a la IP y puerto que el servidor Consul ha habilitado para conexiones cliente HTTP y se indica que no se desea que se registre en el sistema.

Una vez hecho esto, ya se puede hacer uso del nombre del servicio en las comunicaciones entre los mismos, liberando a las aplicaciones de conocer las direcciones IP y puertos donde se despliegan.

Por ejemplo, utilizando la interfaz DiscoveryClient se puede obtener la lista de servicios con el nombre indicado.

Internamente hace una petición GET a "/v1/health/service/" para recuperar los servicios registrados con ese nombre que están “sanos” y obtener sus direcciones para luego seleccionar el primero y obtener la dirección. Después con ésta URI ya se puede hacer la solicitud de información:


 @Autowired
 DiscoveryClient discoveryClient;  
 Optional<ServiceInstance> serviceInstance =
      discoveryClient.getInstances("consul-response-service").stream().findFirst();
 serviceInstance.get().getUri().toString();

Pero hay formas más sencillas de obtener las direcciones de los servicios. Por ejemplo, si la comunicación va a hacerse a través de RestTemplate, se puede hacer uso de Ribbon, el balanceador de carga en cliente de Spring Cloud Netflix, anotándolo con @LoadBalanced.


 @Bean
 @LoadBalanced
 public RestTemplate restTemplate() {
    return new RestTemplate();
 }

Así, con tan solo hacer la petición con ese Bean anotado, Spring es capaz de resolver la ruta consultando a Consul.


 @Autowired
 RestTemplate restTemplate;
 ResponseEntity<ResponseDTO> response =
      restTemplate.getForEntity("http://consul-response-service/consul-response",
      ResponseDTO.class);

Para probar el sistema completo, generamos el contenedor de la aplicación cliente con el dockerfile que se muestra a continuación y lanzamos una petición GET al endpoint del cliente para que recupere el json con la respuesta del servicio descubierto, y la muestre por pantalla:

 FROM openjdk:8-jdk-alpine
 ADD build/libs/consul-request-service-0.0.1-SNAPSHOT.jar app.jar
 ENTRYPOINT java -jar ./app.jar

Y hasta aquí el descubrimiento de servicios con Spring Cloud y Consul. Hemos visto qué es Consul y su arquitectura, cómo Spring nos facilita mucho su uso y un ejemplo de cómo montar un sistema básico de contenedores para microservicios Java.

Como se ha podido ver, Consul es una buena alternativa a Eureka para orquestar los microservicios que formen parte de nuestro ecosistema. A diferencia de Eureka, el cluster de Consul tiene un servidor que actúa como líder y responde las peticiones.

En caso de caída se elige uno nuevo y este cambio es transparente para las aplicaciones cliente (si tienen su propio agente Consul cliente configurado correctamente). Eureka, en cambio, si un servidor se cae, son las aplicaciones cliente las que activamente comunican con el siguiente servidor Eureka configurado en ellas.

Además, ofrece interfaz DNS para comunicarse, algo que Eureka no por lo que no estamos atados a comunicación REST y además incluye almacenamiento KV, lo que permite centralizar la configuración en el mismo sistema.

En el siguiente artículo profundizaremos en la funcionalidad de configuración centralizada de Consul y su integración con Spring.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.