====== 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, т.е. в одном месте
* при необходимости добавить новую копию проекта, можно просто выполнить его запуск, указав в метках новый домен и новую внутренюю сеть
Минусов, как таковых, в данном случае нет, разве что дополнительная сложность, но это плата за удобство