486 lines
20 KiB
Markdown
486 lines
20 KiB
Markdown
# Traefik — стандарт публикации сервисов в кластере
|
||
|
||
Этот документ описывает стандарт подключения новых HTTP-сервисов к Traefik в нашем кластере.
|
||
Является эталоном для разработчиков и DevOps при добавлении любого нового сервиса.
|
||
|
||
---
|
||
|
||
## Содержание
|
||
|
||
1. [Архитектура: как Traefik работает в кластере](#1-архитектура-как-traefik-работает-в-кластере)
|
||
2. [Единственный способ добавить сервис: traefik_port_map](#2-единственный-способ-добавить-сервис-traefik_port_map)
|
||
3. [Пример 1: сервис без аутентификации](#3-пример-1-сервис-без-аутентификации)
|
||
4. [Пример 2: сервис с BasicAuth](#4-пример-2-сервис-с-basicauth)
|
||
5. [Пример 3: сервис в том же namespace что и Traefik](#5-пример-3-сервис-в-том-же-namespace-что-и-traefik)
|
||
6. [Пример 4: несколько маршрутов для одного приложения](#6-пример-4-несколько-маршрутов-для-одного-приложения)
|
||
7. [Создание BasicAuth-секрета](#7-создание-basicauth-секрета)
|
||
8. [Добавление нового сервиса: пошаговая инструкция](#8-добавление-нового-сервиса-пошаговая-инструкция)
|
||
9. [Ограничения и правила именования](#9-ограничения-и-правила-именования)
|
||
10. [Диагностика и типичные ошибки](#10-диагностика-и-типичные-ошибки)
|
||
11. [Таблица активных сервисов](#11-таблица-активных-сервисов)
|
||
|
||
---
|
||
|
||
## 1. Архитектура: как Traefik работает в кластере
|
||
|
||
### Схема
|
||
|
||
```
|
||
Пользователь
|
||
│
|
||
│ HTTP :10001, :10002, :10003, ... (порты 10000-10999)
|
||
▼
|
||
k8s-worker-01 (10.203.0.96)
|
||
│
|
||
│ hostPort (DaemonSet Pod слушает на хосте)
|
||
▼
|
||
Traefik Pod (namespace: traefik)
|
||
│
|
||
│ IngressRoute CRD → EntryPoint → Service
|
||
│
|
||
├──► kubernetes-dashboard.kubernetes-dashboard:443
|
||
├──► longhorn-frontend.longhorn-system:80
|
||
└──► my-service.my-namespace:8080
|
||
```
|
||
|
||
### Ключевые решения архитектуры
|
||
|
||
| Параметр | Значение | Причина |
|
||
|---|---|---|
|
||
| Режим деплоя | **DaemonSet** | Pod на каждой worker-ноде; при добавлении нод — автоматически |
|
||
| Тип доступа | **hostPort** | Прямое связывание порта Pod с портом хоста; не нужен NodePort или LoadBalancer |
|
||
| Маршрутизация | **IngressRoute CRD** | Не Ingress; только CRD Traefik — полный контроль над entryPoint |
|
||
| Namespace | `traefik` | Изолированный namespace; IngressRoute живут здесь, сервисы — в своих |
|
||
| TLS | **выключен** | Внутренняя сеть, доверенные клиенты; TLS добавить при необходимости |
|
||
| hostname routing | **нет** | Каждый сервис на своём порту; нет нужды в DNS-именах |
|
||
| `allowCrossNamespace` | **true** | IngressRoute в `traefik` ссылается на Service в других namespace |
|
||
|
||
### Диапазон портов
|
||
|
||
```
|
||
10000–10999 — зарезервировано для Traefik-сервисов
|
||
открыт на firewalld на всех worker-нодах
|
||
```
|
||
|
||
Порт `10000` — зарезервирован (не использовать).
|
||
Порты `10001–10999` — свободны для сервисов, назначаются последовательно.
|
||
|
||
---
|
||
|
||
## 2. Единственный способ добавить сервис: traefik_port_map
|
||
|
||
Вся топология Traefik определяется через одну переменную в `inventory/prod/group_vars/traefik.yml`.
|
||
**Не создавайте IngressRoute и Middleware вручную** — они будут перезаписаны при следующем запуске плейбука.
|
||
|
||
### Структура записи в traefik_port_map
|
||
|
||
```yaml
|
||
traefik_port_map:
|
||
- name: my-service # ≤15 символов! становится именем entryPoint и ресурсов
|
||
description: "Описание" # только для читаемости, не влияет на деплой
|
||
port: 10003 # уникальный порт из диапазона 10001-10999
|
||
backend:
|
||
namespace: my-namespace # namespace, где живёт целевой Service
|
||
service: my-service-svc # имя Service (не Pod, не Deployment)
|
||
port: 8080 # порт Service (не containerPort)
|
||
scheme: http # http или https (если backend использует TLS)
|
||
basicauth:
|
||
enabled: false # true — включить BasicAuth для этого маршрута
|
||
# secret_name: traefik-auth-my-service # нужно только если enabled: true
|
||
```
|
||
|
||
### Что происходит после добавления записи
|
||
|
||
При запуске `ansible-playbook playbooks/setup_traefik.yml`:
|
||
|
||
1. **Helm** перерендеривает `traefik-values.yml` — добавляет новый entryPoint `my-service` на порт 10003
|
||
2. **Middleware** (если `basicauth.enabled: true`) — создаёт объект `Middleware` в namespace `traefik`
|
||
3. **IngressRoute** — создаёт объект `IngressRoute` в namespace `traefik`, ссылающийся на backend Service
|
||
4. **Helm upgrade** применяет изменения; Traefik Pod перезапускается (DaemonSet rolling update)
|
||
|
||
---
|
||
|
||
## 3. Пример 1: сервис без аутентификации
|
||
|
||
**Сценарий:** развернули веб-приложение `my-app` в namespace `my-app`, хотим сделать его доступным на порту 10003.
|
||
|
||
### Шаг 1: убедиться что Service существует
|
||
|
||
```yaml
|
||
# Это должно быть в манифестах приложения (fleet-repo или kubectl apply)
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: my-app
|
||
namespace: my-app
|
||
spec:
|
||
selector:
|
||
app: my-app
|
||
ports:
|
||
- port: 80
|
||
targetPort: 8080
|
||
type: ClusterIP # ClusterIP достаточно — Traefik обращается по внутренней сети
|
||
```
|
||
|
||
### Шаг 2: добавить запись в traefik_port_map
|
||
|
||
```yaml
|
||
# inventory/prod/group_vars/traefik.yml
|
||
traefik_port_map:
|
||
# ... существующие записи ...
|
||
|
||
- name: my-app # ≤15 символов
|
||
description: "My Application"
|
||
port: 10003
|
||
backend:
|
||
namespace: my-app
|
||
service: my-app
|
||
port: 80
|
||
scheme: http
|
||
basicauth:
|
||
enabled: false
|
||
```
|
||
|
||
### Шаг 3: запустить плейбук
|
||
|
||
```bash
|
||
ansible-playbook -i inventory/prod playbooks/setup_traefik.yml
|
||
```
|
||
|
||
### Шаг 4: проверить доступность
|
||
|
||
```bash
|
||
# С любой машины в сети
|
||
curl http://10.203.0.96:10003/
|
||
|
||
# Проверить что IngressRoute создан
|
||
kubectl get ingressroute -n traefik
|
||
kubectl describe ingressroute route-my-app -n traefik
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Пример 2: сервис с BasicAuth
|
||
|
||
**Сценарий:** Grafana в namespace `monitoring`, доступ только с паролем.
|
||
|
||
### Шаг 1: создать Secret с учётными данными
|
||
|
||
```bash
|
||
# Сгенерировать хэш пароля и создать Secret вручную (один раз)
|
||
kubectl create secret generic traefik-auth-grafana \
|
||
--from-literal=users="$(openssl passwd -apr1 'SecurePassword' | xargs -I{} echo 'admin:{}')" \
|
||
-n traefik
|
||
```
|
||
|
||
> Secret создаётся **в namespace `traefik`**, а не в namespace приложения.
|
||
|
||
### Шаг 2: добавить запись
|
||
|
||
```yaml
|
||
# inventory/prod/group_vars/traefik.yml
|
||
traefik_port_map:
|
||
- name: grafana
|
||
description: "Grafana Monitoring Dashboard"
|
||
port: 10004
|
||
backend:
|
||
namespace: monitoring
|
||
service: grafana
|
||
port: 3000
|
||
scheme: http
|
||
basicauth:
|
||
enabled: true
|
||
secret_name: traefik-auth-grafana # имя Secret из шага 1
|
||
```
|
||
|
||
### Шаг 3: запустить плейбук
|
||
|
||
```bash
|
||
ansible-playbook -i inventory/prod playbooks/setup_traefik.yml
|
||
```
|
||
|
||
Плейбук создаст `Middleware` объект:
|
||
|
||
```yaml
|
||
# Автогенерируется из basicauth-middleware.yml.j2
|
||
apiVersion: traefik.io/v1alpha1
|
||
kind: Middleware
|
||
metadata:
|
||
name: basicauth-grafana
|
||
namespace: traefik
|
||
spec:
|
||
basicAuth:
|
||
secret: traefik-auth-grafana
|
||
removeHeader: true # не передавать Authorization header в backend
|
||
```
|
||
|
||
### Шаг 4: проверить
|
||
|
||
```bash
|
||
# Без пароля — должен вернуть 401
|
||
curl -v http://10.203.0.96:10004/
|
||
|
||
# С паролем — должен вернуть 200
|
||
curl -u admin:SecurePassword http://10.203.0.96:10004/
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Пример 3: сервис в том же namespace что и Traefik
|
||
|
||
**Сценарий:** какое-то приложение развёрнуто прямо в namespace `traefik` (нетипично, но возможно).
|
||
|
||
Для сервисов в том же namespace `traefik` параметр `allowCrossNamespace` не нужен — он уже есть.
|
||
Запись в `traefik_port_map` стандартная, просто `namespace: traefik`:
|
||
|
||
```yaml
|
||
- name: whoami
|
||
description: "Debug: whoami service"
|
||
port: 10010
|
||
backend:
|
||
namespace: traefik # тот же namespace
|
||
service: whoami
|
||
port: 80
|
||
scheme: http
|
||
basicauth:
|
||
enabled: false
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Пример 4: несколько маршрутов для одного приложения
|
||
|
||
**Сценарий:** у приложения есть отдельный API и UI, их нужно опубликовать на разных портах независимо (например, для разных уровней доступа).
|
||
|
||
```yaml
|
||
traefik_port_map:
|
||
# UI — с BasicAuth
|
||
- name: myapp-ui
|
||
description: "MyApp Frontend"
|
||
port: 10005
|
||
backend:
|
||
namespace: myapp
|
||
service: myapp-frontend
|
||
port: 80
|
||
scheme: http
|
||
basicauth:
|
||
enabled: true
|
||
secret_name: traefik-auth-myapp
|
||
|
||
# API — без BasicAuth (аутентификация на уровне приложения)
|
||
- name: myapp-api
|
||
description: "MyApp Backend API"
|
||
port: 10006
|
||
backend:
|
||
namespace: myapp
|
||
service: myapp-backend
|
||
port: 8080
|
||
scheme: http
|
||
basicauth:
|
||
enabled: false
|
||
```
|
||
|
||
> **Почему не один порт с path-routing?** Traefik в нашей конфигурации использует `PathPrefix(/)` для всех маршрутов — это намеренное упрощение. Каждый сервис получает свой порт. Path-routing возможен, но усложняет конфигурацию и не соответствует нашему стандарту порт-прокси.
|
||
|
||
---
|
||
|
||
## 7. Создание BasicAuth-секрета
|
||
|
||
### Формат
|
||
|
||
Secret должен содержать ключ `users` с htpasswd-строками (один пользователь на строку):
|
||
|
||
```
|
||
admin:$apr1$xyz$hashedpassword
|
||
```
|
||
|
||
### Один пользователь
|
||
|
||
```bash
|
||
kubectl create secret generic traefik-auth-<service-name> \
|
||
--from-literal=users="$(openssl passwd -apr1 'PASSWORD' | xargs -I{} echo 'USERNAME:{}')" \
|
||
-n traefik
|
||
```
|
||
|
||
### Несколько пользователей
|
||
|
||
```bash
|
||
# Сгенерировать файл htpasswd
|
||
htpasswd -c /tmp/auth admin # первый пользователь (создаёт файл)
|
||
htpasswd /tmp/auth developer # второй пользователь (добавляет)
|
||
|
||
# Создать Secret из файла
|
||
kubectl create secret generic traefik-auth-myapp \
|
||
--from-file=users=/tmp/auth \
|
||
-n traefik
|
||
|
||
rm /tmp/auth
|
||
```
|
||
|
||
### Обновить пароль существующего пользователя
|
||
|
||
```bash
|
||
# Удалить старый Secret и создать новый — Traefik подхватит без перезапуска
|
||
kubectl delete secret traefik-auth-myapp -n traefik
|
||
|
||
kubectl create secret generic traefik-auth-myapp \
|
||
--from-literal=users="$(openssl passwd -apr1 'NewPassword' | xargs -I{} echo 'admin:{}')" \
|
||
-n traefik
|
||
```
|
||
|
||
> Traefik читает Secret динамически. После замены Secret новые пароли начинают работать в течение 30-60 секунд без перезапуска Pod.
|
||
|
||
### Именование секретов
|
||
|
||
Формат: `traefik-auth-<service-name>`, где `<service-name>` совпадает с полем `name` в `traefik_port_map`.
|
||
Пример: `name: grafana` → `secret_name: traefik-auth-grafana`.
|
||
|
||
---
|
||
|
||
## 8. Добавление нового сервиса: пошаговая инструкция
|
||
|
||
```
|
||
1. Выбрать свободный порт из диапазона 10001-10999
|
||
└─ Проверить таблицу активных сервисов в этом README
|
||
|
||
2. Придумать name (≤15 символов, только строчные буквы, цифры, дефис)
|
||
└─ Пример: my-service, grafana, pgadmin, api-v2
|
||
|
||
3. Убедиться что Service существует в кластере
|
||
└─ kubectl get svc -n <namespace> <service-name>
|
||
|
||
4. (если BasicAuth нужен) Создать Secret в namespace traefik
|
||
└─ kubectl create secret generic traefik-auth-<name> ...
|
||
|
||
5. Добавить запись в inventory/prod/group_vars/traefik.yml
|
||
|
||
6. Обновить таблицу в этом README (раздел 11)
|
||
|
||
7. Запустить плейбук:
|
||
└─ ansible-playbook -i inventory/prod playbooks/setup_traefik.yml
|
||
|
||
8. Проверить:
|
||
└─ curl http://10.203.0.96:<port>/
|
||
└─ kubectl get ingressroute -n traefik
|
||
└─ kubectl describe ingressroute route-<name> -n traefik
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Ограничения и правила именования
|
||
|
||
### Критические ограничения
|
||
|
||
| Ограничение | Причина | Последствие нарушения |
|
||
|---|---|---|
|
||
| `name` ≤ 15 символов | Используется как имя Kubernetes container port | `helm upgrade` падает с ошибкой валидации |
|
||
| `name` только `[a-z0-9-]` | Kubernetes naming convention | Ошибка применения манифеста |
|
||
| Порты уникальны | hostPort — один порт = один Pod на ноде | Конфликт портов, Pod не стартует |
|
||
| Secret в namespace `traefik` | IngressRoute и Middleware в `traefik` | Middleware не найдёт Secret, маршрут недоступен (404) |
|
||
| Service тип ClusterIP | Достаточно для Traefik → Service связки | NodePort и LoadBalancer избыточны |
|
||
|
||
### Что происходит при отсутствии BasicAuth-секрета
|
||
|
||
Если Secret указан в `secret_name`, но не существует:
|
||
- Traefik **полностью отключает маршрут** (возвращает 404, не 401)
|
||
- Другие маршруты продолжают работать
|
||
- В логах Traefik: `middleware "traefik/basicauth-X" does not exist`
|
||
|
||
**Всегда создавайте Secret до запуска плейбука.**
|
||
|
||
### Rolling update DaemonSet: возможное зависание
|
||
|
||
При обновлении Traefik (новый Helm values) DaemonSet пересоздаёт Pod на каждой ноде.
|
||
Если старый Pod не успел завершиться, новый не может занять hostPort:
|
||
|
||
```bash
|
||
# Симптом: Pod застрял в Pending
|
||
kubectl get pods -n traefik -o wide
|
||
|
||
# Решение: принудительно удалить старые Pod
|
||
kubectl delete pod -n traefik -l app.kubernetes.io/name=traefik
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Диагностика и типичные ошибки
|
||
|
||
### Основные команды
|
||
|
||
```bash
|
||
# Список всех IngressRoute
|
||
kubectl get ingressroute -n traefik
|
||
|
||
# Детали конкретного маршрута
|
||
kubectl describe ingressroute route-my-service -n traefik
|
||
|
||
# Список Middleware
|
||
kubectl get middleware -n traefik
|
||
|
||
# Логи Traefik (последние 100 строк)
|
||
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --tail=100
|
||
|
||
# Access логи (каждый запрос)
|
||
kubectl logs -n traefik -l app.kubernetes.io/name=traefik --tail=50 | grep '"method"'
|
||
|
||
# Traefik Dashboard (если включён — у нас выключен по умолчанию)
|
||
# kubectl port-forward -n traefik svc/traefik 9000:9000
|
||
# open http://localhost:9000/dashboard/
|
||
|
||
# Проверить что порт слушается на хосте
|
||
# (выполнить через SSH на worker-ноде)
|
||
ss -tlnp | grep 10003
|
||
|
||
# Проверить firewalld
|
||
firewall-cmd --list-ports | grep 10000-10999
|
||
```
|
||
|
||
### Таблица ошибок
|
||
|
||
| Симптом | Причина | Решение |
|
||
|---|---|---|
|
||
| 404 на порту | IngressRoute не создан | Проверить `kubectl get ingressroute -n traefik` |
|
||
| 404 при включённом BasicAuth | Secret не существует | Создать Secret в namespace `traefik` |
|
||
| 401 без запроса пароля | BasicAuth не применился | Проверить `kubectl get middleware -n traefik` |
|
||
| Connection refused | Порт не открыт на firewall | `firewall-cmd --add-port=10003/tcp --permanent && firewall-cmd --reload` |
|
||
| Connection refused | Traefik Pod не запущен | `kubectl get pods -n traefik` |
|
||
| Bad Gateway (502) | Backend Service недоступен | `kubectl get svc -n <namespace>`, проверить Endpoints |
|
||
| `helm upgrade` ошибка | Имя `name` > 15 символов | Сократить имя в `traefik_port_map` |
|
||
| Pod застрял в Pending | hostPort конфликт | Удалить старый Pod вручную |
|
||
|
||
### Проверка backend: доступен ли Service из Pod Traefik
|
||
|
||
```bash
|
||
# Запустить временный Pod и обратиться к backend
|
||
kubectl run -it --rm debug --image=curlimages/curl --restart=Never \
|
||
-- curl -v http://my-service.my-namespace.svc.cluster.local:80/health
|
||
|
||
# Или через exec в Pod Traefik
|
||
kubectl exec -n traefik \
|
||
$(kubectl get pod -n traefik -l app.kubernetes.io/name=traefik -o name | head -1) \
|
||
-- wget -qO- http://my-service.my-namespace.svc.cluster.local:80/health
|
||
```
|
||
|
||
### Проверка конфигурации без применения
|
||
|
||
```bash
|
||
# Посмотреть что сгенерирует шаблон (dry-run Ansible)
|
||
ansible-playbook -i inventory/prod playbooks/setup_traefik.yml --check --diff
|
||
|
||
# Просмотреть текущий Helm values в кластере
|
||
helm get values traefik -n traefik
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Таблица активных сервисов
|
||
|
||
Обновлять при каждом добавлении или удалении сервиса.
|
||
|
||
| Порт | Name | Сервис | Namespace | BasicAuth | Secret |
|
||
|---|---|---|---|---|---|
|
||
| 10001 | `k8s-dashboard` | `kubernetes-dashboard:443` | `kubernetes-dashboard` | нет | — |
|
||
| 10002 | `longhorn-ui` | `longhorn-frontend:80` | `longhorn-system` | **да** | `traefik-auth-longhorn` |
|
||
|
||
**Следующий свободный порт: 10003**
|