====== 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 у сервисов, на основе чего и принимает решение об отправке запроса в тот или иной контейнер; * Запущенное приложение в докер имеет определенные метки (домен, порт); Схематично вышеописанное представлено на изображении ниже: {{https://fatalex.cifro.net/lib/plugins/ckgedit/fckeditor/userfiles/image/devops/docker/Untitled-Diagram1-1024x736.png?nolink&}} Структура тестового “проекта” следующая: . ├── 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 ==== Конфигурацию Traefik можно выполнять как через отдельный файл конфигурации, подключаемый в контейнер, так и частично через метки контейнеров – их не обязательно прописывать в конфиг, достаточно указать в нужном сервисе. Пример будет ниже.
**Терминация TLS в данном случае должна выполняться на стороне Traefik, но в рамках статьи этот момент сознательно опущен**, чтобы не усложнять материал. Например, для реального рабочего проекта на основе Traefik, настройки TLS были выполнены на вышестоящем прокси-сервере с Nginx, т.к. именно он являлся точкой входа для других несвязанных сервисов, а потому все TLS-сертификаты выполнялись на ином сервере. Но это частный случай, и всё зависит от задачи. В любом случае, Traefik позволяет терминировать TLS при необходимости.
* Конфигурационный файл 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**. ==== Запуск проекта dev1 ==== После того, как 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 ==== Запуск проекта dev2 ==== Запуск второго инстанса выполняется аналогично, например, в /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, т.е. в одном месте * при необходимости добавить новую копию проекта, можно просто выполнить его запуск, указав в метках новый домен и новую внутренюю сеть Минусов, как таковых, в данном случае нет, разве что дополнительная сложность, но это плата за удобство