Traefik 2.0
В очередной раз сетуя что в nginx нет встроенной поддержки let's encrypt, Я пошёл на просторы интернета поискать что-нить новенькое. Из интересного Я нашёл nginx-le/nginx-le: Nginx with automatic let's encrypt (docker image). Но это дополнительная обвязка из костылей, что не особо радует. Опять наткнулся на traefik. Вспомнил что он обновился до версии 2, и решил потрогать его. Все равно ничего больше то и нет.
Придумаем себе проблему
Давайте предположим что нам необходимо сделать небольшое веб приложение. И конечно хочется что бы приложение было отказоустойчивым. Допустим с базой данных мы решим вопрос выбрав Managed Database. А вот как быть с приложением? Нет, конечно мы возьмем Elastic Load Balancer, это будет точкой входа для пользователя в браузере. А дальше что? У нас же конечно микросервисы. Их много, а балансировщик то один. Похоже надо еще прокси сервис, который сходит в разные приложения и соберет это под одним портом.
А еще конечно мы хотим что бы все было обмазано docker. И что бы можно было легко поднимать новые сервисы и масштабировать существующие.
Можно конечно взять старый добрый nginx. Но тогда придется втащить отдельно let's encrypt, отдельно service discovery, а потом все это дружить. И тут ненароком можно задуматься, а мож нафиг его и сразу поставить Kubernetes? На секунду, но задуматься.
И вот тут как раз и выходит на сцену Traefik. Он не только умеет let's encrypt, но и умеет напрямую ходить в docker engine и смотреть на контейнеры и их лейблы. Это даёт возможность написать docker-compose файлик в котором будет не только описание запуска контейнера, но и настройки проксирования.
А теперь браво решим проблему
Давайте попробуем все это развернуть и потестить. Заодно поиграемся с новым Яндекс.Облаком. Terraform брать не будем ради таких забав, возьмем обычный CLI.
Создадим две виртуалки с предустановленным docker, это что бы незапариваться с установкой самим.
> yc compute instance create-with-container \
--name traefik-vm1 \
--zone=ru-central1-a \
--ssh-key ~/.ssh/id_rsa.pub \
--public-ip --labels test=traefik \
--cores 2 --memory 2G \
--create-disk type=network-ssd,size=8 \
--container-image containous/whoami
> yc compute instance create-with-container \
--name traefik-vm2 \
--zone=ru-central1-b \
--ssh-key ~/.ssh/id_rsa.pub \
--public-ip --labels test=traefik \
--cores 2 --memory 2G \
--create-disk type=network-ssd,size=8 \
--container-image containous/whoami
На каждой машине мы хотим развернуть traefik в виде прокси сервера и тестовое приложение на sub-url /app
.
Для этого напишем простенький docker-compose.yml.
version: '3'
services:
traefik:
image: traefik:v2.2
command:
# Подключаем отслеживание docker и убираем автоматическое побликование контейнеров
- "--providers.docker=true"
- "--providers.docker.watch=true"
- "--providers.docker.exposedByDefault=false"
# Вешаем сервер на 443 порт
- "--entryPoints.web.address=:443"
# Добавил простенький healthcheck самого traefik
- "--ping=true"
# Всякая всячина
- "--api.insecure=true"
- "--metrics.prometheus=true"
restart: always
ports:
- 443:443
- 8080:8080 # Порт админки traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
# Регистрируем router с именем 'all' который будет перехватывать весь трафик
- traefik.http.routers.all.entrypoints=web
- traefik.http.routers.all.rule=HostRegexp(`{any:.+}`)
app:
image: "containous/whoami"
restart: always
hostname: traefik-${VM} # Пригодится для отладки
labels:
# Подключаем контейнер к traefik на sub-url '/app' с https
- traefik.enable=true
- traefik.http.routers.app.tls=true
- traefik.http.routers.app.rule=Path(`/app`)
Что бы запустить docker-compose, нам надо знать публичные IP аддреса виртуалок.
Для этого смотрим колонку 'EXTERNAL IP', в yc compute instance list
> yc compute instance list
+----------------------+-------------+---------------+---------+----------------+-------------+
| ID | NAME | ZONE ID | STATUS | EXTERNAL IP | INTERNAL IP |
+----------------------+-------------+---------------+---------+----------------+-------------+
| fhmg57b1c2sr5vo6sjuj | traefik-vm1 | ru-central1-a | RUNNING | 130.193.51.171 | 10.128.0.11 |
| epda5ji37b6vf7jgivqv | traefik-vm2 | ru-central1-b | RUNNING | 84.201.164.166 | 10.129.0.19 |
+----------------------+-------------+---------------+---------+----------------+-------------+
Каждый IP добавлюяем в known_hosts, иначе docker-compose будет падать.
> ssh-keyscan 130.193.51.171 >> ~/.ssh/known_hosts
> ssh-keyscan 84.201.164.166 >> ~/.ssh/known_hosts
Запускаем docker-compose для обоих виртуалок, дополнительно параметризуя вызов переменной окружения 'VM'.
> VM=vm1 docker-compose -H 'ssh://yc-user@130.193.51.171' up -d
> VM=vm2 docker-compose -H 'ssh://yc-user@84.201.164.166' up -d
Проверяем что у нас все удачно запустилось.
> curl --silent --insecure https://130.193.51.171/app | grep Hostname
Hostname: traefik-vm1
> curl --silent --insecure https://84.201.164.166/app | grep Hostname
Hostname: traefik-vm2
Теперь надо создать target-group с хостами на которые будет идти нагрузка. Для этого нам понадобятся подсети и ip адреса машинок. Мы их получим с помощью магии jq и формата вывода json.
> yc compute instance list --format json | jq '[.[] | {subnet: .network_interfaces[0].subnet_id, address: [.network_interfaces[].primary_v4_address.address]}]'
[
{
"subnet": "e2lqhsko5mdlsdsmr4p0",
"address": [
"10.129.0.19"
]
},
{
"subnet": "e9ba3jbnjvvqdhlu22j2",
"address": [
"10.128.0.11"
]
}
]
Подсталяем subnet и address в команду ниже.
> yc load-balancer target-group create \
--region-id ru-central1 \
--name traefik-tg \
--target subnet-id=e2lqhsko5mdlsdsmr4p0,address=10.129.0.19 \
--target subnet-id=e9ba3jbnjvvqdhlu22j2,address=10.128.0.11
id: b7rhkak24g2ddrk3in55
folder_id: b1g6ikhlpce8nn9mvl7r
created_at: "2020-06-27T21:42:33Z"
name: traefik-tg
region_id: ru-central1
targets:
- subnet_id: e2lqhsko5mdlsdsmr4p0
address: 10.129.0.19
- subnet_id: e9ba3jbnjvvqdhlu22j2
address: 10.128.0.11
Теперь нам надо создать балансировщик который будет слушать на https порту. target-group-id подставляем из предыдущей команды.
> yc load-balancer network-load-balancer create \
--region-id ru-central1 \
--name traefik-lb \
--type external \
--listener name=https,external-ip-version=ipv4,port=443 \
--target-group target-group-id=b7rhkak24g2ddrk3in55,healthcheck-name=http,healthcheck-interval=2s,healthcheck-timeout=1s,healthcheck-unhealthythreshold=2,healthcheck-healthythreshold=2,healthcheck-http-port=8080,healthcheck-http-path=/ping
Получаем IP аддрес нашего балансировщика.
> yc load-balancer network-load-balancer get traefik-lb --format json | jq '.listeners[0].address'
"84.201.129.67"
Проверяем что у нас все удачно запустилось.
> curl --silent --insecure https://84.201.129.67/app | grep Hostname
Hostname: traefik-vm2
> curl --silent --insecure https://84.201.129.67/app | grep Hostname
Hostname: traefik-vm1
Если подергать endpoint, то можно увидеть что в hostname значения меняются. Что нотифицирует нам о том, что мы все правильно настроили.
Scale it!
C виртуалками все просто. Стартуем ешë одну, запускаем docker-compose и добавляем в target-group. Но мы же предполагаем что у нас жирные виртуалки, и 2-3 таких нам за глаза. Их надо утилизировать, запуская несколько инстансов одного приложения внутри каждой виртуалки. И как не странно, но это очень просто сделать!
> VM=vm1 docker-compose -H 'ssh://yc-user@130.193.51.171' up -d --scale app=3
> VM=vm1 docker-compose -H 'ssh://yc-user@130.193.51.171' ps
Connected (version 2.0, client OpenSSH_7.2p2)
Authentication (publickey) successful!
Name Command State Ports
-------------------------------------------------------------------------------------------------------------------
traefik_app_1 /whoami Up 80/tcp
traefik_app_2 /whoami Up 80/tcp
traefik_app_3 /whoami Up 80/tcp
traefik_traefik_1 /entrypoint.sh --providers ... Up 0.0.0.0:443->443/tcp, 80/tcp, 0.0.0.0:8080->8080/tcp
Вся прелесть в том что traefik автоматически подхватывает новые инстансы и делает балансировку! Можно убедиться в этом дернув API и увидев 3 'servers'.
> curl -s 84.201.156.117:8080/api/http/services | jq '.[] | select(.type=="loadbalancer")'
{
"loadBalancer": {
"servers": [
{
"url": "http://172.18.0.6:80"
},
{
"url": "http://172.18.0.3:80"
},
{
"url": "http://172.18.0.4:80"
}
],
"passHostHeader": true
},
"status": "enabled",
"usedBy": [
"app@docker"
],
"serverStatus": {
"http://172.18.0.3:80": "UP",
"http://172.18.0.4:80": "UP",
"http://172.18.0.6:80": "UP"
},
"name": "app-traefik@docker",
"provider": "docker",
"type": "loadbalancer"
}
Let's encrypt
Вспомним что вся движуха началась с let's encrypt. Так что давайте прикрутим нормальный сертификат к нашему приложению. Воспользуемся DNS challenge, что бы не мучаться с прокидыванием дополнительных endpoint. У меня домен хоститься на AWS, поэтому я вольспользуюсь Route53. Другие провайдеры можно посмотреть в документации Поправим наш docker-compose.yml.
version: '3'
services:
traefik:
image: traefik:v2.2
command:
- "--providers.docker=true"
- "--providers.docker.watch=true"
- "--providers.docker.exposedByDefault=false"
- "--entryPoints.web.address=:443"
# Добавляем resolver с именем aws и настраеваем email, provider и storage
- "--certificatesresolvers.aws.acme.email=blog@isudo.ru"
- "--certificatesresolvers.aws.acme.dnschallenge.provider=route53"
- "--certificatesresolvers.aws.acme.storage=/etc/traefik/acme.json"
- "--ping=true"
- "--api.insecure=true"
- "--metrics.prometheus=true"
restart: always
ports:
- 443:443
- 8080:8080
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Выносим хранение сертификатов на хост машину
- /etc/traefik:/etc/traefik
labels:
- traefik.http.routers.all.entrypoints=web
- traefik.http.routers.all.rule=HostRegexp(`{any:.+}`)
app:
image: "containous/whoami"
restart: always
hostname: traefik-${VM} # Пригодится для отладки
labels:
- traefik.enable=true
# Добавляем совпадение по имени хоста и подключаем certresolver
- traefik.http.routers.app.entrypoints=web
- traefik.http.routers.app.tls=true
- traefik.http.routers.app.tls.certresolver=aws
- traefik.http.routers.app.rule=Host(`traefik.isudo.ru`) && Path(`/app`)
Далее идет хитрость за кадром, которая тут не описана. А имеено привязака IP балансировщика '84.201.129.67' к домену 'traefik.isudo.ru'.
Когда домен готов к использованию, можно обновить настройки контейнеров на виртуалках.
> export AWS_ACCESS_KEY_ID={{ your_aws_access_key_id }}
> export AWS_SECRET_ACCESS_KEY={{ your_aws_secret_access_key }}
> VM=vm1 docker-compose -H 'ssh://yc-user@130.193.51.171' up -d
> VM=vm2 docker-compose -H 'ssh://yc-user@84.201.164.166' up -d
Ждем немного пока traefik дождется подтверждения challenge и подставит сертификат. Проверить сертификат на каждой виртуалке можно через 'curl', добавив специальный флаг 'resolve'.
> curl --verbose --resolve 'traefik.isudo.ru:443:130.193.51.171' https://traefik.isudo.ru:443/app 2>&1 | grep 'Hostname\|CN='
* Hostname traefik.isudo.ru was found in DNS cache
* subject: CN=traefik.isudo.ru
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
Hostname: traefik-vm1
>
> curl --verbose --resolve 'traefik.isudo.ru:443:84.201.164.166' https://traefik.isudo.ru:443/app 2>&1 | grep 'Hostname\|CN='
* Hostname traefik.isudo.ru was found in DNS cache
* subject: CN=traefik.isudo.ru
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
Hostname: traefik-vm2
Ну и на последок можно дернуть запрос через балансировшик без флага --insecure
, потому что у нас теперь не самоподписанный сертификат.
> curl --silent https://traefik.isudo.ru/app | grep Hostname
Hostname: traefik-vm1
Работает!
Почистим за собой
Облака дело не дешевое, поэтому надо удалить все добро которое мы насоздавали.
> yc load-balancer network-load-balancer delete traefik-lb --async
> yc load-balancer target-group delete traefik-tg --async
> yc compute instance delete traefik-vm1 traefik-vm2 --async
Подведем итоги
Плюсы.
Отказоустойчивости добились, можно масштабировать как контейнеры так и ноды. Конфигурация приложений декларативная, на виртуалки лезть руками не надо. Сделали прозрачную и автоматическую конфигурацию сертификатов.
Есть конечно и минусы.
Например трафик в такой схеме между виртуалками ходить не будет. Надо еще докручивать health check, restart policy для контейнеров.
Тем не менее, в целом получилось недурно. Для небольшого приложения вполне годная структура для старта.