Como preparar un plan de despliegue con Docker, Traefik y Letsencrypt para un Microservicio y una Aplicación Web Progresiva (PWA)

Traefik es un enrutador perimetral de código abierto que hace que la publicación de sus servicios sea una experiencia fácil y divertida. Recibe solicitudes en nombre de su sistema y descubre qué componentes son responsables de manejarlas.
Traefik también puede cumplir la función de proxy inverso y balanceador de carga, automatiza la generación de certificados SSL con Letsencrypt, permite configurar middlewares globales por ejemplo para redirección de http a https, prevención de ataques DDOS, y middlewares por servicio como por ejemplo para lanzar una autenticación con .htpasswd.

Lo que distingue a Traefik, además de sus muchas funciones, es que descubre automáticamente la configuración adecuada para sus servicios. La magia sucede cuando Traefik inspecciona su infraestructura, donde encuentra información relevante y descubre qué servicio atiende qué solicitud.

Traefik cumple de forma nativa con todas las principales tecnologías de clúster, como Kubernetes, Docker, Docker Swarm, AWS, Mesos, Marathon, y la lista continúa ; y puede manejar muchos al mismo tiempo. (Incluso funciona para software heredado que se ejecuta en bare metal).

Con Traefik, no hay necesidad de mantener y sincronizar un archivo de configuración separado: todo sucede automáticamente, en tiempo real (sin reinicios, sin interrupciones de conexión). Con Traefik, dedica tiempo a desarrollar e implementar nuevas funciones en su sistema, y ya no a configurar y mantener su estado de funcionamiento.

El objetivo principal de Traefik es hacerlo simple y que el usuario disfrute de ello.
Descripción del proyecto y como utilizarlo

A continuación se describe el proyecto traefik-sample, disponible en github, contiene una definición de plan de despliegue con docker-compose, además de:

  • Generación automática de certificados SSL con Letsencrypt
  • Middlewares globales (redirección http a https, rate limit para prevención de ataques DDOS
  • Integración con servicios (microservicios con Laravel, Nginx, PWA sea React o Vue, Redis, MySQL, Adminer y Portainer)
  • Middleware de servicio para lanzamiento de autenticación htpasswd
  • Configuración de logs (si deseas optimizarlo puedes usar logrotate)

Requisitos

  • git 2.2 o superior
  • docker 20 o superior
  • docker-compose 1.20 o superior
  • Configura tus claves públicas y privadas con Github para siempre usar SSH con git
  • Adrenalina y buena pasión por el código

## Clona el repositorio
Clona el proyecto de traefik-sample en tu directorio de trabajo, por ejemplo:
mkdir github
cd github
git clone git@github.com:marcotorres/traefik-sample.git
## Makefile
Este repositorio contiene un archivo Makefile con comandos preparados para iniciar los servicios de los contenedores, ver los los, estados de los servicios y poder ingresar a cada contenedor definido en el plan de despliegue del docker-compose.yml
.PHONY: help

CMD ?= ''

help: ## This help.
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

.DEFAULT_GOAL := help

start:  ## Start the application
	@echo "Starting the application"
	@docker-compose up -d

restart:  ## Restart the application
	@echo "Restarting the application"
	@docker-compose restart

stop:  ## Stop the application
	@echo "Stopping the application"
	@docker-compose down

status:  ## Status the application
	@echo "Showing the status for the application"
	@docker-compose ps

logs:	## Show the all Logs from the application
	@echo "Showing all logs for every container"
	@docker-compose logs -f --tail="50"

cli_api:  ## Enter to container console from API
	@echo "Entering to container console from API"
	@docker exec -ti api bash

cli_web:  ## Enter to container console from WEB
	@echo "Entering to container console from WEB"
	@docker exec -ti web sh

cli_db:  ## Enter to container console from MySQL
	@echo "Entering to container console from MySQL"
	@docker exec -ti mysql bash
## docker-compose.yml
A continuación se muestra el contenido del archivo docker-compose.yml, podemos observar las definición del plan de despliegue de cada servicio.
version: "3.8"

networks:
  infra:
  traefik:
    external:
      name: traefik
  default:
    driver: bridge

volumes:
  traefik_sample_data_portainer:
  traefik_sample_data_mysql:

services:

  traefik:
    image: traefik:v2.3
    container_name: traefik
    restart: unless-stopped
    networks:
      traefik:
        ipv4_address: 192.168.90.254
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
      - target: 8080
        published: 8080
        protocol: tcp
        mode: ingress
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/traefik.yml:/traefik.yml
      - ./traefik/acme.json:/acme.json
      - ./traefik/traefik.log:/traefik.log
      - ./traefik/rules:/rules
      - ./traefik/shared:/shared
    labels:
      - "traefik.enable=true"
      # router
      - "traefik.http.routers.traefik-rtr.entrypoints=websecure"
      - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$HOSTNAME`)"
      - "traefik.http.routers.traefik-rtr.tls=true"
      - "traefik.http.routers.traefik-rtr.tls.certresolver=le"
      # service api
      - "traefik.http.routers.traefik-rtr.service=api@internal"
      # middlewares
      - "traefik.http.routers.traefik-rtr.middlewares=rate-limit@file,myauth@file"

  api:
    image: webdevops/php-nginx:8.1-alpine
    container_name: api
    working_dir: /app
    restart: always
    networks:
      - traefik
      - infra
    volumes:
      - ./apps/api:/app:rw
      - ./nginx/nginxapi.conf:/etc/nginx/nginx.conf
    environment:
      - WEB_DOCUMENT_ROOT=/app/public
      - VIRTUAL_HOST=api.$HOSTNAME
    labels:
      - "traefik.enable=true"
      # router
      - "traefik.http.routers.api-rtr.entrypoints=websecure"
      - "traefik.http.routers.api-rtr.rule=Host(`api.$HOSTNAME`)"
      - "traefik.http.routers.api-rtr.tls=true"
      - "traefik.http.routers.api-rtr.tls.certresolver=le"

  web:
    image: webdevops/nginx:latest
    container_name: web
    working_dir: /app
    restart: always
    networks:
      - traefik
      - infra
    volumes:
      - ./apps/web:/app:rw
    environment:
      - WEB_DOCUMENT_ROOT=/app/build
      - WEB_ALIAS_DOMAIN=web.$HOSTNAME
      - WEB_DOCUMENT_INDEX=index.html
    labels:
      - "traefik.enable=true"
      # router
      - "traefik.http.routers.intranet-rtr.entrypoints=websecure"
      - "traefik.http.routers.intranet-rtr.rule=Host(`web.$HOSTNAME`)"
      - "traefik.http.routers.intranet-rtr.tls=true"
      - "traefik.http.routers.intranet-rtr.tls.certresolver=le"

  adminer:
    image: adminer:latest
    container_name: adminer
    restart: always
    networks:
      - traefik
      - infra
    volumes:
      - ./adminer/plugins-enabled:/var/www/html/plugins-enabled:rw
      - ./adminer/robots.txt:/var/www/html/robots.txt
    labels:
      - "traefik.enable=true"
      # router
      - "traefik.http.routers.adminer-rtr.entrypoints=websecure"
      - "traefik.http.routers.adminer-rtr.rule=Host(`adminer.$HOSTNAME`)"
      - "traefik.http.routers.adminer-rtr.tls=true"
      - "traefik.http.routers.adminer-rtr.tls.certresolver=le"
      # middlewares
      - "traefik.http.routers.adminer-rtr.middlewares=rate-limit@file,myauth@file"

  mysql:
    image: mysql:8.0.25
    container_name: mysql
    working_dir: /app
    restart: on-failure
    networks:
      - infra
    security_opt:
      - no-new-privileges:true
    ports:
      - "3306:3306"
    volumes:
      - ${PWD}/data:/app:rw
      - ${PWD}/mysql/my.cnf:/etc/mysql/conf.d/my-over.cnf
      - traefik_sample_data_mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: $MYSQL_DATABASE
      MYSQL_USER: $MYSQL_USER
      MYSQL_PASSWORD: $MYSQL_PASSWORD

  redis:
    image: redis:alpine
    container_name: redis
    restart: unless-stopped
    ports:
      - "6380:6379"
    networks:
      - infra
    security_opt:
      - no-new-privileges:true

  portainer:
    image: portainer/portainer:latest
    container_name: portainer
    restart: unless-stopped
    command: -H unix:///var/run/docker.sock
    networks:
      traefik:
        ipv4_address: 192.168.90.253
    security_opt:
      - no-new-privileges:true
    volumes:
      - itraefik_sample_data_portainer:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - TZ=$TZ
    labels:
      - "traefik.enable=true"
      # router
      - "traefik.http.routers.portainer-rtr.entrypoints=websecure"
      - "traefik.http.routers.portainer-rtr.rule=Host(`portainer.$HOSTNAME`)"
      - "traefik.http.routers.portainer-rtr.tls=true"
      - "traefik.http.routers.portainer-rtr.tls.certresolver=le"
      # services
      - "traefik.http.routers.portainer-rtr.service=portainer-svc"
      - "traefik.http.services.portainer-svc.loadbalancer.server.port=9000"
## traefik.yml
En esta parte observamos la definición del plan de despliegue de Traefik contenido en traefik.yml, en este punto recuerda que si estas haciendo pruebas con generación del certificado SSL, es mejor que lo hagas descomentando la sección del caServer, para más información sobre el rate limit de letsencrypt puedes revisar la siguiente documentación:
global:
  checkNewVersion: true
  sendAnonymousUsage: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
  traefik:
    address: ":8080"

api:
  dashboard: true
  insecure: false
  debug: true

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik
    swarmMode: false
  file:
    directory: /rules
    watch: true

log:
  filePath: "/logs/traefik.log"
  level: WARN # DEBUG, INFO, WARN, ERROR, FATAL, PANIC
  format: json

accessLog:
  filePath: "/logs/traefik.log"
  bufferingSize: 100
  format: json
  filters:
    statusCodes:
      - "400-499"
    retryAttempts: true
    minDuration: "10ms"
  fields:
    defaultMode: keep
    names:
      ClientUsername: drop
    headers:
      defaultMode: keep
      names:
        User-Agent: redact
        Authorization: drop
        Content-Type: keep

certificatesResolvers:
  le:
    acme:
      email: log@traefik-sample.com
      storage: "/acme.json"
      #caServer: https://acme-staging-v02.api.letsencrypt.org/directory # no comentar para pruebas indefinidas
      httpChallenge:
        entryPoint: web
## middlewares.toml
A continuación se muestra el contenido del archivo middlewares.toml, aquí tenemos la definición de los middlewares para una autenticación básica con htpasswd y prevención de ataques http con un rate limit por minuto de a lo mucho 100 request.
[http.middlewares]
    [http.middlewares.myauth]
        [http.middlewares.myauth.basicAuth]
            realm = "Traefik Basic Auth"
            usersFile = "/shared/.htpasswd"
    [http.middlewares.rate-limit]
        [http.middlewares.rate-limit.rateLimit]
            average = 100
            burst = 50
## .env, logs y acme.json
Para poder iniciar el proyecto no te olvides de crear el archivo .env basado en .env.example, puedes ejecutar los siguientes comandos como una pequeña configuración inicial.
cp .env.example .env && \
cp traefik/acme.json.example traefik/acme.json && \
cp traefik/logs/traefik.log.example traefik/logs/traefik.log && \
cp traefik/shared/.htpasswd.example traefik/shared/.htpasswd && \
chmod 0600 traefik/acme.json
## Docker network y repositorios propios
No te olvides de que en la carpera ./apps/api y ./apps/web deben de ir tus proyectos que están en Laravel (microservicio) y React o Vue (PWA) y que antes de levantar los servicios tengas creado un docker network externo, puedes crearlo con el siguiente comando:
docker network create --gateway 192.168.90.1 --subnet 192.168.90.0/24 traefik
## Make CLI
Levanta el proyecto ya sea con docker-compose o con make así:
make start
Otras utilidades del Makefile:
Más documentación del proyecto en Github, traefik-sample
Espero que esta guía te haya servido de ayuda, hasta una proxima oportunidad.