Traefik: un portero buena onda para tener múltiples aplicaciones en nuestro servidor

Tiempo de lectura: 12 minutos

Una de las cosas que nunca vi en la universidad fue el cómo poner en producción varias aplicaciones en un mismo servidor y cómo hacer que direcciones web apunten a ellas para que los usuarios puedan interactuar desde sus navegadores.

Algún tiempo después de egresar, contraté mi primer VPS y comencé a aprender. Para quién algún no tenga experiencia, haré un pequeño resumen del recorrido que se sigue desde que el usuario escribe una ruta en su navegador hasta que esta se ejecuta. Si se manejan con esto, pueden saltar a la siguiente sección.

  • El usuario escribe la ruta en un navegador. http / https y todo el blabla que viene después. Esto se conoce como un nombre de dominio.
  • El navegador busca «resolver» la ruta. Esto significa convertir el dominio a una dirección IP a la que establecer la conexión. Para esto, consulta a un módulo específico del sistema operativo que almacena direcciones conocidas y sus correspondientes IPs.
  • Si no encuentra esta dirección o dominio, se conecta con un servidor DNS que traduce el dominio a una dirección IP.
  • ¿Cómo se hace esta configuración? Cuando uno contrata un dominio a un proveedor (por ejemplo: name.com, nic.cl, godaddy.com), se le da la opción de configurar «registros» en un servidor DNS. Entre los registros más comunes están los registros tipo A. Un registro tipo A relaciona un dominio como «ejemplo.com» o «aplicacion.ejemplo.com» y lo asocia a una dirección IP como «123.123.123.123».
  • Cuando ya el navegador tiene la dirección IP, se genera una conexión hasta la máquina referida por esa dirección.
  • La máquina / servidor referida recibe esta conexión y de alguna forma debe redirigir a la aplicación correspondiente. Para eso existen varias opciones, siendo las más comunes apache y nginx.

Cuando comencé a configurar servidores para este proceso, noté que una de la soluciones más comunes y que venía por defecto en muchas configuraciones era apache. Apache es un servidor HTTP que puede escuchar las conexiones que entren a la máquina (principalmente en los puertos 80 y 443) y responder a ellas. Mediante ciertos bloques de configuración puede indicar qué aplicaciones deben responder a tales solicitudes, convirtiéndose en lo que se conoce como Proxy Inverso. Su popularidad se ha dado de gran manera dado a que por defecto tiende a ser compatible con aplicaciones en PHP que son puestas en ciertas carpetas para ser procesadas.

Como soy un tipo que tiene muchas mañas estéticas con los programas que ocupa, no me gustó la configuración y busqué otra alternativa. Así conocí nginx. Básicamente, para el caso que nos convoca, hace lo mismo que apache y permite redirigir las conexiones a ciertas aplicaciones. Como principalmente desarrollo en Node.js, mis aplicaciones generan servidores HTTP que corren en un puerto específico, en vez de en el puerto 80 y en una carpeta específica como las aplicaciones PHP. Así que ocupaba la instrucción proxy_pass localhost:PUERTO de nginx para redirigir las conexiones al puerto de la aplicación correspondiente. Y esa ha sido mi solución hasta ahora, que conocí Traefik.

Llegando a Traefik

La repetición termina siempre por aburrirme de usar ciertas herramientas. Si un proceso no se vuelve más ágil con el tiempo, me empieza a desagradar y voy buscando formas de automatizarlo o facilitarlo. Con el tiempo me había venido aburriendo de hacer archivos .conf para usar nginx como proxy inverso (además que para levantar sitios en WordPress, nunca me resultaba la configuración con nginx 😓) y me puse a buscar alternativas. Hay varias alternativas que encontré. Algunas con más o menos desempeño que otras, o con desarrollos enfocados a características distintas como el balanceo de carga u otras. Pero no me había animado a probarlas. Hasta que un día probé un software con un 1-click install en Digital Ocean y noté que la configuración de proxy inverso nombraba a Traefik. Y ahí me puse a averigura y decidí probarlo.

Hasta el logo es terrible buena onda. Créditos del logo en Github.

Traefik se promociona en su página como "El proxy inverso moderno que la nube estaba esperando". Luego siguen con su descripción: "Traefik es el proxy inverso open source y balanceador de carga líder para aplicaciones HTTP y basadas en TCP que es fácil, dinámico, automático, rápido, lleno de características, probado en producción, que provee métricas y se integra con cada tecnología principal para clusters… ¡No es de sorprender su popularidad!".

Se quieren harto los cabros, ¿cierto? Y al parecer con cierta razón. Es una aplicación empaquetada como un sólo binario en go, uno de los lenguajes que han ido adquiriendo fuerza en el último tiempo. También puede ser instalada usando docker, que es lo que hice yo.

Mi caso de uso

Hace tiempo venía queriendo usar traefik pero mis VPS tienen varias aplicaciones en producción y no quería arriesgarme a romper algo desactivando nginx de la nada. Pero en uno de los VPS tenía aplicaciones más pequeñas y decidí hacer una prueba. Para validar traefik, tenía que complir ciertos objetivos para mi caso de uso:

  1. Poder levantarse usando docker-compose, porque me da flojera usar otras cosas.
  2. Redirigir rutas que apunten a mi servidor a aplicaciones Node.js que corren en puertos específicos.
  3. Poder definir la configuración de cada servicio en su docker-compose respectivo, que es una de las cosas que promete Traefik y que me ahorraría la flojera de andar configurando archivos de nginx cada vez que levanto un servicio nuevo.
  4. Permitir el tráfico seguro con TLS y usar certificados de Let’s Encrypt.

Spoiler: Los 4 objetivos se cumplieron, así que estamos playa.

Para este caso, vamos a usar un sitio que hice en un arranque de ocio: emojizador.com. Este sitio convierte el texto que uno escribe a emojis. Está compuesto de dos aplicaciones: Una aplicación para el frontend que está hecha en Nuxt.js (💓) y el backend que recibe texto por HTTP y devuelve el equivalente en emojis.

A veces se me ocurren estupideces como esta.

Objetivo 1: Ejecutar

Mi punto de partida con Traefik fue este artículo. Acá se muestra cómo usar Traefik y redirigir conexiones a un puerto específico definido como otro servicio dentro de un mismo archivo docker-compose.

Para los que no trabajan con docker ni docker-compose, les comento un poco de qué se trata. Docker es una aplicación que te permite ejecutar aplicaciones de manera aislada al resto del entorno. Sirve para "empaquetar" la aplicación con su propio entorno, biblioteca, etc. Es más bacán que una máquina virtual. Y docker-compose es una utilidad para lanzar varias aplicaciones con docker usando un archivo de configuración, lo que hace que el proceso de poner en producción una aplicación sea super fácil. Les recomiendo que aprendan ambas cosas y su vida se hará más feliz. Hay muchos tutoriales en internet, pero si les gustaría una explicación en mis palabras, comenten y escribo un artículo del tema.

El primer objetivo es lanzar Traefik usando docker-compose. Para eso, vamos a crear una carpeta "traefik" y dentro de ella un archivo docker-compose.yml que va a definir una instancia de traefik 2.2 que es la versión más reciente.

version: "3.7"
networks:
  traefik:
    name: traefik
services:
  traefik:
    image: "traefik:v2.2"
    container_name: "traefik"
    restart: always
    command:
      #- "--log.level=DEBUG"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
    ports:
      - "80:80"
    networks:
      - traefik
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

Para ejecutar este archivo, debemos estar en la misma carpeta y ejecutar:

docker-compose up -d

Si tenemos algún error, antes debemos cerrar cualquier proceso que esté reservando el puerto 80. En mi caso tenía nginx en un VPS con Centos 7, así que lo cerré con lo siguiente:

sudo systemctl stop nginx
sudo systemctl disable nginx

Con eso ya tenemos corriendo Traefik en la puerta de nuestro servidor. Ahora cualquier aplicación conexión que llegue al puerto 80 pasará por traefik y desde ahí será derivada donde corresponda.

¿Qué acabamos de configurar?

  • Si vamos a usar docker para lanzar Traefik, necesitamos que nuestros servicios sean visibles entre sí. Para eso, al comienzo del archivo creamos una red llamada traefik. Aquí es importante la versión del docker-compose para definir la network, porque primero probé con la versión 3.3 y no me reconoció esa sintaxis. Con la versión 3.7 no aseguramos.
  • Si definimos todos nuestros servicios en el mismo archivo docker-compose, no es necesario crear la network ya que docker-compose automáticamente crea una red para los servicios de un mismo archivo. Pero yo trabajaré con varios servicios y por modularidad prefiero usar una red aparte.
  • Definimos un servicio que usa la imagen «traefik:v2.2» que es la versión más reciente. También definimos el nombre «traefik» para el contenedor que se generará.
  • Configuramos que el contenedor se reinicie en caso de fallas con «restart: always».
  • Con «ports: 80:80» definimos que el puerto 80 de traefik se integre al puerto 80 de nuestra máquina anfitriona.
  • Montamos un volumen para que traefik pueda leer el funcionamiento de docker y así poder detectar todos los otros servicios que se estén ejecutando. Además de docker, existen otros «proveedores» que se pueden usar. Esta configuración es particular de docker. Más info acá.

Existen varias formas de configurar Traefik. Una forma es através de archivos de configuración TOML. Pero si configuramos desde docker-compose.yml, podemos usar la intrucción command y pasar todos los parámetros de configuración que necesitemos.

  • Si queremos que Traefik muestre mucha información de debug, podemos descomentar la intrucción – «–log.level=DEBUG». Por defecto viene comentada porque es MUCHA información.
  • «–providers.docker=true»: Activamos Docker como un proveedor de Traefik. Podríamos activar otros proveedores de ser necesario.
  • «–providers.docker.exposedbydefault=false»: Evita que todos los contenedores de Docker sean procesados por Traefik, así podemos definir explícitamente la configuración por cada contenedor.
  • «–entrypoints.web.address=:80»: Definimos un punto de entrada llamado web en el puerto 80. Luego podemos decirle a nuestros contenedores que pasen a través de los entrypoints que definamos.

Objetivo 2: Redirigir y Objetivo 3: Configurar

Ahora lo que interesa es redirigir las conexiones que lleguen al entrypoint definido. Como comenté, el caso de uso es el sitio "emojizador.com". Para este caso vamos a definir algunas cosas.

  • Tenemos dos aplicaciones: un frontend y un backend. Cada uno corre en un contenedor y funciona a través del puerto 3000.
  • Ambas aplicaciones están definidas en un mismo archivo docker-compose.yml.
  • Vamos a configurar la ruta «emojizador.com» para apuntar al frontend y la ruta «api.emojizador.com» para apuntar al backend.

Una de las cosas bacanes que hace Traefik es que permite que los servicios que uno levanta a través de docker-compose incorporen la configuración necesaria para hacer las redirecciones. A diferencia de herramientas como nginx donde uno debe crear un archivo de configuración que apunte al servicio. Aquí traefik automáticamente va detectando lo que los servicios le "avisan". Esto se hace a través de "labels", que son un mecanismo de docker para añadir metadata a los contenedores.

Primero, antes de configurar Traefik, debemos configurar en nuestro proveedor de dominios los registros DNS tipo A que apunten a la IP de nuestro servidor (reemplazando las IPs de ejemplo):

  • Registro tipo A emojizador.com -> 1.1.1.1
  • Registro tipo A api.emojizador.com -> 2.2.2.2

Antes de Traefik, yo tenía los servicios configurados en un archivo docker-compose.yml en la carpeta "emojizador". Así se veía la configuración:

version: "3.7"

services:
  emojizer_backend:
    build: ./backend/
    container_name: emojizer-backend
    restart: always
    ports:
      - "3911:3000"
  emojizer_frontend:
    build: ./emojis-web/
    container_name: emojis-frontend
    restart: "no"
    ports:
      - "3912:3000"

Con archivos de configuración de nginx redirigía la rutas a los puertos: 3911 y 3912 respectivamente. En estos casos se ocupa la instrucción build para crear las imágenes, ya que el código está en carpetas, cada una definida con un Dockerfile.

Para integrar estas imágenes con Traefik, vamos a añadir configuración adicional usando labels. Así queda el archivo:

version: "3.7"

networks:
  traefik:
    external: true

services:
  emojizer_backend:
    build: ./backend/
    container_name: emojizer-backend
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.emojizador-back.rule=Host(api.emojizador.com)"
      - "traefik.http.routers.emojizador-back.entrypoints=web"
      - "traefik.port=3000"
      - "traefik.docker.network=traefik"
  emojizer_frontend:
    build: ./emojis-web/
    container_name: emojis-frontend
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.emojizador-front.rule=Host(emojizador.com)"
      - "traefik.http.routers.emojizador-front.entrypoints=web"
      - "traefik.port=3000"
      - "traefik.docker.network=traefik"

Cosas que pasan en este nuevo docker-compose.yml:

  1. Definimos la red traefik como una red externa para luego conectar los contenedores a la red con la instrucción «networks: – traefik».
  2. Eliminamos la instrucción ports, ya que no será necesario que los servicios se asocien a un puerto de la máquina para que los pueda ver nginx. Ahora traefik se encargará de la redirección y los puede encontrar usando la network «traefik».
  3. Configuramos labels que le darán información a traefik de cómo redirigir hacia nuestros servicios.

Las labels requieren una explicación algo más detallada, así que las iré explicando de a una.

  • – «traefik.enable=true»: Esta instrucción le dice a traefik que queremos que se encargue de redirigir hacia este contenedor en particular.
  • Para hacer redirecciones, se crea un objeto router para cada servicio. Esto se define con la notación «traefik.http.routers.NOMBRE-DEL-ROUTER», donde NOMBRE-DEL-ROUTER es un nombre que nosotros definimos para el servicio. Para cada una de estas definiciones podemos añadir parámetros adicionales.
  • En este caso definimos el parámetro «rule» que tiene como argumento un Host que indica la URL que será redirigida hacia este servicio.
  • El segundo parámetro que se define es el parámetro «entrypoints», donde definimos el entrypoint «web» que creamos en la configuración de traefik. Esto hará que se redirijan las solicitudes que lleguen al puerto definido en el entrypoint, siendo en este caso el 80.
  • – «traefik.port=3000»: Especificamos a qué puerto llegarán las conexiones. Ambas usan el puerto 3000 y gracias a la mezcla de traefik y docker no se cruzan.
  • – «traefik.docker.network=traefik»: Le decimos a traefik en qué red de docker están funcionando estos servicios.

Ahora podemos echar a correr estos servicios con "docker-compose up -d" y automáticamente le enviará la configuración a traefik y habilitará las rutas. Nada de configuración queda fuera de la definición del servicio, lo que es una de las cosas que me empujaron a abandonar nginx.

Ahora las rutas "http://emojizador.com" y "http://api.emojizador.com" son accesibles desde el navegador.

Objetivo 4: Asegurar

Una de las cosas importantes, y ahora casi obligatorias para levantar aplicaciones web, es asegurar la aplicación con un certificado SSL. Esto nos da conexiones más seguras a través de HTTPS y nos muestra el candadito verde bonito del navegador que deja algo más tranquilos a nuestros usuarios.

Este era el último punto que necesitaba para convencerme de comenzar mi migración a Traefik. Es una de las cosas que me costaba entender al ser un recién egresado y que fui aprendiendo a punta de tropezones.

A grandes rasgos, la conexión a cada una de nuestras rutas se asegura a través de un certificado. Este certificado se solicita a un ente externo. Una "Autoridad". Existen diversas empresas que dan esto como un servicio, pero también existe un servicio gratuito que entrega certificados llamado Let’s Encrypt. En todo el tiempo en que llevo desarrollando aplicaciones web, no me he visto en la necesidad de usar algo que no sea Let’s Encrypt, así que eso vamos a ver en este caso: cómo usar un certificado de Let’s Encrypt para poder usar HTTPS en nuestras conexiones.

Para eso, vamos a volver a configurar el archivo docker-compose.yml con el que lanzamos traefik. Vamos a agregar algunas labels que principalmente harán dos cosas:

  • Definir un nuevo entrypoint para el puerto 443, que es el puerto usado por las conexiones HTTPS.
  • Configurar un resolver para que obtenga el certificado que necesitamos. En este caso usaremos a Let’s Encrypt.
version: "3.7"

networks:
  traefik:
    name: traefik

services:

  traefik:
    image: "traefik:v2.2"
    container_name: "traefik"
    restart: always
    command:
      - --log.level=DEBUG
      - --entrypoints.web.address=:80
      - --entrypoints.web-secure.address=:443
      - --providers.docker=true
      - --certificatesresolvers.le.acme.email=correo@ejemplo.com
      - --certificatesresolvers.le.acme.storage=/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
    ports:
      - 80:80
      - 443:443
    networks:
      - traefik
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

Se agregaron las siguientes labels:

  • –entrypoints.web-secure.address=:443: define un nuevo entrypoint llamado «web-secure» en el puerto 443.
  • Se definen varios parámetros para un objeto llamado «le» (por Let’s Encrypt) perteneciente a la propiedad «certificateresolvers». De ser necesario, podríamos definir otros resolvers adicionales.
  • –certificatesresolvers.le.acme.email=correo@ejemplo.com: define el correo que se usará como cuenta de Let’s Encrypt.
  • –certificatesresolvers.le.acme.storage=/acme.json: define un archivo en el que se guardará la configuración de los certificados que se descarguen. Como queda dentro del contenedor, no tendremos que preocuparnos del contenido. Ahora, si queremos verlo podemos montarlo como un volumen.
  • –certificatesresolvers.le.acme.tlschallenge=true: habilita el desafío tls, necesario para validar la titularidad del dominio que se quiere certificar.

Reiniciamos el contenedor de traefik con "docker-compose up -d" y ya tenemos lista una parte. Ahora falta que nuestros servicios digan que quieren usar TLS y resolver sus certificados a partir de Let’s Encrypt.

Nos vamos a la carpeta emojizador y editamos el docker-compose.yml. Es super poco lo que se agrega y lo que se cambia.

version: "3.7"

networks:
  traefik:
    external: true

services:
  emojizer_backend:
    build: ./backend/
    container_name: emojizer-backend
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.emojizador-back.rule=Host(api.emojizador.com)"
      - "traefik.http.routers.emojizador-back.entrypoints=web-secure"
      - "traefik.http.routers.emojizador-back.tls=true"
      - "traefik.http.routers.emojizador-back.tls.certresolver=le"
      - "traefik.port=3000"
      - "traefik.docker.network=traefik"
  emojizer_frontend:
    build: ./emojis-web/
    container_name: emojis-frontend
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.emojizador-front.rule=Host(emojizador.com)"
      - "traefik.http.routers.emojizador-front.entrypoints=web-secure"
      - "traefik.http.routers.emojizador-back.tls=true"
      - "traefik.http.routers.emojizador-back.tls.certresolver=le"
      - "traefik.port=3000"
      - "traefik.docker.network=traefik"

Si lo comparamos con el archivo anterior, cambió lo siguiente:

  • Se cambió el entrypoint de cada servicio de «web» a «web-secure», por lo que ahora se podrá acceder usando el puerto 443 a las rutas.
  • Se añadió la opción «tls=true» que habilita el uso de TLS para asegurar las rutas.
  • Se especifica el resolvedor de TLS como «tls.certresolver=le», donde «le» corresponde al resolver de Let’s Encrypt que definimos anteriormente.

Reiniciamos los contenedores con "docker-compose up -d" y listo.

Sí, listo.

Esperamos unos momentos, porque Traefik automáticamente detecta los cambios hechos y dice "Oh, ahora estos contenedores quieren obtener certificados de Let’s Encrypt. Voy al toque a pedirlos.". No hay que usar el comando certbot ni configurar tareas cron para que el certificado se renueve automáticamente. Traefik se encarga de todo eso automáticamente por nosotros.

Conclusión

¿Bacán, cierto?

Partamos con decir que Traefik cumplió mis objetivos. Facilitó tareas que me daban flojera, automatizó cosas que me aburría configurar y deja todo mucho más ordenado que mi instalación anterior. Ahora agregar servicios nuevos será tan fácil como añadir el contenedor a la red y configurarlo con las labels correctas. Traefik automáticamente los detectará y configurará, dejándome minutos valiosos que puedo invertir en jugar más Animal Crossing New Horizons.

Aún hay casos de uso que me falta por explorar, como levantar y configurar servicios con otros stacks como por ejemplo levantar varias instancias de WordPress en paralelo, o configuraciones relacionadas con balanceo de carga. Pero ya me convencí de que vale la pena aprender más de esta herramienta. Para un primer acercamiento, me encantó la facilidad de configuración y la claridad de esta misma.

Espero que les haya gustado también y se animen a probar Traefik.

Si quieren leer los tutoriales que usé de referencia para adaptar a mi caso de uso, pueden encontrarlos acá:

Si quieren revisar la idiotez de página que hice, acá está:

Sin más que añadir, los dejo invitados a compartir y comentar. Ojalá que esta lectura les haya servido de entretención si están haciendo cuarentena, y si no, cuídense harto.

También los dejo invitados a que se suscriban a nuestro Newsletter para que puedan recibir noticias de nuestras publicaciones y actividades.

Si eres un desarrollador de software de la Región del Maule, te invitamos a participar de la comunidad Maule Devs 😎

También te podría gustar...