Docker Compose + Traefik: L7-маршрутизация для множества проектов

Цель данной статьи: демонстрация запуска нескольких инстансов одного проекта в docker compose, доступ к которым будет осуществляться через Traefik (прокси). Приведу пример, зачем всё это нужно и почему тут Traefik, а не Nginx.

Есть задача запускать множество окружений-песочниц, на которых QA-инженеры будут тестировать код. Песочницы должны динамически создаваться и удаляться при отрабатывании пайплайна в CI-системе (например Gitlab). Стэк – Nginx + httpd. Как правильно организовать данную схему?

Разумеется, приложение распилено на микросервисы, и можно написать плейбук или скрипты для запуска приложения в docker-compose и с нужным количеством реплик. Но как проксировать трафик извне до поднятых приложений?

Основная проблема заключается в следующем:

  • допустим, первый инстанс запущен, Nginx в контейнере слушает на порту 80 и данный порт проброшен на хост. Чтобы запустить второй инстанс, нужно мапить новый порт, например, 81. И так далее. Это очень неудобно при динамическом запуске и удалении окружений, т.к. нужно как-то вести учёт портов, что вообще звучит безумно;
  • можно не мапить порты на хост, каждый проект будет иметь запущенный Nginx на 80 порту, но в рамках своей сети, но тогда к приложению не будет доступа извне;

Как будет реализовано: каждый проект будет запускаться в своём отдельном сетевом namespace, т.е. через docker compose up -d (будет создана своя отдельная сеть под проект). Nginx в каждом проекте будет запущен на 80 порту, никаких конфликтов по портам на хосте не возникает. Остается открытым вопрос доступа к приложению. Здесь-то в игру и вступает Traefik, который решит описанную выше проблему.

Traefik – относительно новый продукт, который является L7-балансировщиком. Он написан для применения как раз в схемах с использованием микросервисов. А потому может использоваться как Ingress Controller в Kubernetes или же как прокси для standalone докер-контейнеров, что как раз и необходимо. Traefik запускается также в контейнере, а взаимодействие с другими контейнерами осуществляется через API-докера (с указанием пути до unix-сокета) и labels.

Как будет работать вся схема:

  • HTTP-запрос извне приходит на Traefik, где настроено прослушивание нужного домена и TLS при необходимости;
  • Traefik подключается к Docker через API и позволяет увидеть все labels у сервисов, на основе чего и принимает решение об отправке запроса в тот или иной контейнер;
  • Запущенное приложение в докер имеет определенные метки (домен, порт);

Схематично вышеописанное представлено на изображении ниже:

Структура тестового “проекта” следующая:

.
├── app
│  └── index.html
├── default.conf
└── docker-compose.yaml

  • каталоге app содержится код, в данном случае просто статический файл, внутри которого строка “It works! PS. dev2 proj” или “It works! PS. dev1 proj“, чтобы была разницу между инстансами
  • в default.conf содержится конфиг для Nginx
  • и непосредственно docker-compose.yaml

Конфигурацию Traefik можно выполнять как через отдельный файл конфигурации, подключаемый в контейнер, так и частично через метки контейнеров – их не обязательно прописывать в конфиг, достаточно указать в нужном сервисе. Пример будет ниже.

<blockquote>Терминация TLS в данном случае должна выполняться на стороне Traefik, но в рамках статьи этот момент сознательно опущен, чтобы не усложнять материал. Например, для реального рабочего проекта на основе Traefik, настройки TLS были выполнены на вышестоящем прокси-сервере с Nginx, т.к. именно он являлся точкой входа для других несвязанных сервисов, а потому все TLS-сертификаты выполнялись на ином сервере. Но это частный случай, и всё зависит от задачи. В любом случае, Traefik позволяет терминировать TLS при необходимости.</blockquote>

  • Конфигурационный файл traefik.yml, который монтируется в контейнер:
mkdir /opt/traefik && cd /opt/traefik
api:
  dashboard: true
  insecure: true

accessLog: {}

log:
  level: INFO

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

http:
  routers:
    host:
   entryPoints:
      - http
      rule: Host(`domain.ru`)

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false

В конфиге выше указывается минимальный набор:

  • настраивается прослушивание на 80 и 443 портах
  • активируется логирование и дашборд (http доступ в веб-интерфейс Traefik)
  • указывается провайдер – API docker

Данный конфиг монтируется внутрь контейнера. Для примера ниже представлен docker-compose.yaml:

version: '3.8'

services:
  traefik:
    image: traefik:v2.2
    volumes:
   - ./traefik.yml:/traefik.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - 80:80
      - 8080:8080
    restart: always
    networks:
      - default

networks:
  default:
    external:
      name: gateway

docker-compose.yaml – типовой файл, при запуске которого будет запущен Traefik в отдельной внутренней сети с именем default, которая создается автоматически. Но есть один примечательный и очень важный момент, связанный с этой сетью. Помимо сети по умолчанию (default), вручную создается отдельная сеть, в которую приходят запросы извне. В рамках данной статьи сеть названа gateway. И внутренняя сеть default линкуется с внешней сетью gateway.

  • Важно указать, что сеть gateway является внешней, т.е. при создании будет указано internal=false:
docker network create \
  --driver=bridge \
  --attachable \
  --internal=false \
  gateway

  • После того, как подготовлен конфиг Traefik, создана внешняя сеть gateway и подготовлен docker-compose.yaml, можно выполнять запуск Traefik:
docker-compose up -d

После запуска нужно убедиться, что ошибок в логах нет, и проверить дашборад (http://127.0.0.1:8080/dashboard/). Он не обязателен в рамках данной статьи, но просто наглядно демонстрирует, что Traefik запущен. Также в дашборде будут видны правила маршрутизации. По умолчанию доступ к дашборду по http.

После того, как Traefik запущен, подготавливается первый инстанс проекта. Напомню, что для примера в качестве демонстрации используется максимально простой вариант: Nginx с проксированием до apache.

Для первого инстанса используется следующий docker-compose.yaml:

version: "3"

services:
  nginx:
    image: nginx:latest
    volumes:
    - ./app/index.html:/app/index.html
    - ./default.conf:/etc/nginx/conf.d/default.conf
    labels:
   - "traefik.enable=true"
      - "traefik.http.routers.nginx-dev1.rule=Host(`dev1.domain.ru`)"
      - "traefik.http.services.nginx-dev1.loadbalancer.server.port=8080"
      - "traefik.docker.network=gateway"
    networks:
    - default
    - dev1

  httpd:
    image: httpd:latest
    volumes:
    - ./app/index.html:/usr/local/apache2/htdocs/index.html
    networks:
      - dev1

networks:
  default:
    external: true
    name: gateway
  dev1:
    internal: true

Пояснения по конфигу:

  • для сервиса Nginx, который является точкой входа трафика, назначены соответствующие метки с именем домена, порта – по ним Traefik будет понимать, что при запросах на dev1.domain.ru направлять трафик надо именно в этот контейнер
  • по умолчанию для всех сервисов используется внутренняя сеть dev1, с помощью которой сервисы могут взаимодействовать между собой, данная сеть описана в самом низу
  • помимо дефолтной внутренней сети, для сервиса Nginx также прописана дополнительная сеть с именем default – в конфиге указано, что она является внешней и линкуется с ранее созданной вручную сетью gateway, к которой также подключен Traefik

Таким образом, все внешние запросы при обращении на dev1.domain.ru из Traefik будут приходить в контейнер с Nginx, где указаны метки для dev1 соответственно – сеть gateway будет обеспечивать эту связанность. И при этом не рушится межсервисное взаимодействие в рамках одного проекта – это обеспечивает внутренняя сеть dev1.

Конфигурационный файл default.conf:

server {
    listen 8080;
    server_name _;

    root /app;
    index index.php index.html;

    location / {
   proxy_pass http://httpd:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Пояснения по конфигу:

  • конфиг-файл универсальный, т.е. между разными инстансами не нужно вносить в него никаких правок
  • сервис Nginx может обращаться к сервису httpd в рамках своей внутренней сети dev1, т.к. в docker-compose.yaml у сервиса апача задано имя “httpd”.

В файле app/index.html просто содержится статическая строчка для наглядного примера, в какой контейнер какого проекта пришёл запрос.

  • Все вышеописанные файлы можно скопировать в /opt/dev1 и выполнить запуск:
docker-compose up -d

Для проверки, что проект успешно запустился, можно зайти внутрь контейнера с Nginx и курлом проверить ответ:

docker exec -it project1_nginx_1 bash
root@6139a191e911:/# curl localhost:8080
It works!

PS. dev1 proj

Запуск второго инстанса выполняется аналогично, например, в /opt/dev2. Меняются лишь метки и наименование внутренней сети. Ниже представлен пример docker-compose.yaml для наглядной демонстрации разницы между первыми и вторым инстансом:

version: "3"

services:
  nginx:
    image: nginx:latest
    volumes:
    - ./app/index.html:/app/index.html
    - ./default.conf:/etc/nginx/conf.d/default.conf
    labels:
   - "traefik.enable=true"
      - "traefik.http.routers.nginx-dev2.rule=Host(`dev2.domain.ru`)"
      - "traefik.http.services.nginx-dev2.loadbalancer.server.port=8080"
      - "traefik.docker.network=gateway"
    networks:
    - default
    - dev2

  httpd:
    image: httpd:latest
    volumes:
    - ./app/index.html:/usr/local/apache2/htdocs/index.html
    networks:
      - dev2

networks:
  default:
    external: true
    name: gateway
  dev2:
    internal: true

После запуска второго проекта, если используется тестовый домен для проверки, можно вписать в /etc/hosts своего рабочего компьютера 127.0.0.1, на котором слушает Traefik, и выполнять проверку:

127.0.0.1 dev1.domain.ru
127.0.0.1 dev2.domain.ru

При обращении на первый или второй инстанс запросы будут соотвественно распределяться в нужный контейнер.

Изучив выше демонстрационный пример, можно без проблем настраивать сколько угодно копий одного проекта, при этом нет необходимости каждый раз вносить правки в Traefik – он динамически проверяет конфиграцию через API докера.

Из явных плюсов данного подхода можно отметить следующее:

  • вся L7-маршрутизация может быть выполнена на стороне Traefik, т.е. в одном месте
  • при необходимости добавить новую копию проекта, можно просто выполнить его запуск, указав в метках новый домен и новую внутренюю сеть

Минусов, как таковых, в данном случае нет, разве что дополнительная сложность, но это плата за удобство