1. Introducción

Hola a todos y bienvenidos a un nuevo post de nuestro querido blog! En esta ocasión veremos como instalar un proxy inverso en Docker con el fin de balancear la carga entre las peticiones que llegan desde internet a los microservicios que podamos tener dentro de la red interna de nuestra organización. En el post Spring Cloud Zuul analizamos como llevar a cabo la creación de un proxy inverso dentro del universo de Spring Cloud. Sin embargo en esta ocasión nos apoyaremos en Nginx. Veremos como hacerlo de dos formas. En primer lugar haciéndolo manualmente configurando el servidor nginx a través de su fichero de configuración default.conf. Y en segundo lugar haciéndolo dinámicamente haciendo uso de la imagen docker jwilder/nginx-proxy que realizará la configuración del balanceo de forma agnóstica teniendo en cuenta el número de instancias de un servicio dentro de Docker.

Podéis ampliar información sobre el servidor nginx y el balanceo de carga haz click aquí

Además os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Docker 17.06.2-ce
  • jwilder/nginx-proxy
  • nginx
  • Java 8
  • Spring Boot 1.5.7
  • Gradle 3.1

2. Antes de empezar…

Vamos a crear una imagen Docker de un microservicio de SpringBoot de la misma forma que se hizo en el post Spring Boot Docker con el fin de arrancar varias instancias para proceder a realizar el balanceo de carga sobre éstas.

group 'com.jorgehernandezramirez.springboot'
version '1.0-SNAPSHOT'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.7.RELEASE")
        classpath('se.transmode.gradle:gradle-docker:1.2')
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'maven'
apply plugin: 'docker'

sourceCompatibility = 1.8

springBoot {
    mainClass = "com.jorgehernandezramirez.springboot.reverseproxy.Application"
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
}

task buildDocker(type: Docker, dependsOn: build) {
    push = project.hasProperty('push')
    applicationName = jar.baseName
    tag = 'jorgehernandezramirez/spring-boot-docker-reverse-proxy'
    dockerfile = file('src/main/docker/Dockerfile')
    doFirst {
        copy {
            from jar
            into stageDir
        }
    }
}

Fichero Dockerfile que utilizará el plugin gradle-docker para proceder a construir la imagen.

FROM frolvlad/alpine-oraclejdk8:latest
VOLUME /tmp
ADD reserveproxy-1.0-SNAPSHOT.jar app.jar
RUN sh -c 'touch /app.jar'
ENTRYPOINT [ "sh", "-c", "java -jar /app.jar" ]

Main que arranca SpringBoot

package com.jorgehernandezramirez.springboot.reverseproxy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public Application(){
        //For Spring
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Exponemos la url /random que devolverá un número aleatorio que se genera en el arranque del contexto de Spring y que nos permitirá identificar en qué contenedor nos encontramos

package com.jorgehernandezramirez.springboot.reverseproxy.rest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class RandomController {

    private Double randomValue;

    public RandomController(){
        //Para Spring
    }

    @PostConstruct
    public void initialization(){
        this.randomValue = Math.random();
    }
 
    @RequestMapping("/random")
    public Double getRandomValue() {
        return randomValue;
    }
}

Cronstruimos nuestra imagen docker y la almacenamos dentro del registro de imágenes locales

gradle buildDocker

Verificamos finalmente que se creado la imagen correctamente

docker images

3. Configuración manual del proxy inverso – nginx

Antes de empezar arrancamos dos instancias de nuestro microservicio de forma manual de tal forma que una escuche en el puerto 8080 y la otra en el puerto 8081. Para ello hacemos

java -jar -Dserver.port=8080 build/libs/reserveproxy-1.0-SNAPSHOT.jar
java -jar -Dserver.port=8081 build/libs/reserveproxy-1.0-SNAPSHOT.jar

Se muestra el fichero docker-compose.yml en donde se define el servidor nginx. Además se monta la configuración en el directorio /etc/nginx/conf.d

nginx:
    image: nginx
    volumes:
      - "./conf:/etc/nginx/conf.d"
    ports:
      - "80:80"

Para llevar a cabo el balanceo de carga es necesario utilizar la directiva upstream que nos permitirá definir las diferentes instancias que tenemos arrancadas de nuestro microservicio. En nuestro caso una escucha en el puerto 8080 y la otra en el puerto 8081

upstream miupstream {
    server 192.168.1.34:8080;
    server 192.168.1.34:8081;
}

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location /proxy {
        proxy_pass http://miupstream/;
    }
}

Nótese que debemos atacar al subpath /proxy para que se redirija al proxy inverso definido en el upstream

Arrancamos el servidor

docker-compose up

Atacamos al proxy inverso sucesivas veces a la url /random

curl http://localhost/proxy/random

obteniendo en ocasiones el número aleatorio generado por un contenedor

0.2152137939205018

y obteniendo en ocasiones el número aleatorio generado por el otro contenedor

0.7152468169848423

4. Configuración dinámica del proxy inverso – jwilder/nginx-proxy

El principal problema que tenemos en la configuración manual es que necesitamos conocer previamente el número de instancias de nuestro servicio de tal forma que si queremos añadir una nueva, el proxy inverso necesitaría una actualización de su fichero de configuración y un reinicio para contemplarlas. Lo que se pretende ahora es que la creación y borrado de nuevas instancias sea agnóstica para nosotros, de tal forma que el proxy inverso pueda cambiar su configuración dinámicamente. Para ello haremos uso de la imagen jwilder/nginx-proxy.

Se muestra el fichero docker-compose.yml en donde se define el proxy inverso (jwilder/nginx-proxy) y los microservicios en Spring Boot.

my-spring-boot-example: 
  image: jorgehernandezramirez/spring-boot-docker-reverse-proxy:1.0-SNAPSHOT
  ports:
    - "8080"
  environment:
    VIRTUAL_HOST: 'localhost'

nginx:
    image: jwilder/nginx-proxy
    volumes:
      - "/var/run/docker.sock:/tmp/docker.sock:ro"
    ports:
      - "80:80"

Es importante destacar los siguientes aspectos en la configuración:

  • Se monta el fichero docker.sock en modo lectura sobre el proxy inverso. Dicho fichero contiene información relevante sobre los contenedores que se encuentran arrancados dentro de Docker y servirá para que nuestro proxy inverso pueda realizar la configuración adecuada sobre el fichero de configuración de nginx que se debe encontrar dentro del filesystem del contenedor en /etc/nginx/conf/default.conf
  • Los contenedores sobre los que queremos que se realice el balanceo deberán establecer a través de la variable de entorno VIRTUAL_HOST el nombre del upstream para el que se quiere que se configure dicho balanceo.

Arrancamos dos instanacias del servicio my-spring-boot-example y una del servicio nginx

docker-compose up --scale my-spring-boot-example=2

Atacamos al proxy inverso sucesivas veces a la url /random

curl http://localhost/random

obteniendo en ocasiones el número aleatorio generado por un contenedor

0.1152737919206015

y obteniendo en ocasiones el número aleatorio generado por el otro contenedor

0.6159248969818463

4. Referencias