Введение
Цель данной статьи: демонстрация запуска нескольких инстансов одного проекта в 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
Конфигурацию 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, т.е. в одном месте
- при необходимости добавить новую копию проекта, можно просто выполнить его запуск, указав в метках новый домен и новую внутренюю сеть
Минусов, как таковых, в данном случае нет, разве что дополнительная сложность, но это плата за удобство.
Следующим шагом уже будет использование системы оркестрации контейнеров, т.к. там куда больше возможностей по управлению разными копиями одно или нескольких проектов. А текущая же реализация подойдет для проведения каких-либо QA-тестов на небольшом проекте, если на нём ещё не используется kubernetes.
Спасибо. Очень полезная информация, целый день копал по данной специфике. Нашел тольуо у вас подобную раскладку.
Хотелось бы уточнить несколько моментов
1. Для чего содается контейнер с httpd в проекте. По тексту статьи это не понятно.
2. На 80 порту все прекрасно работает но как подключить 443 порт с Let’s Encrypt ?
рад, что информация помогла
к сожалению, подробно ответить вряд ли получится, т.к. все зависит от ситуации.
апач можно и не использовать, тут как захотите.
по 443 и ссл – советую поискать в интернете инфу. но как правило для терминирования тлс трафика внешние балансировщики используются и лучше смотреть в эту сторону, а за прокси-сервером оставить как есть – на 80 порту.