k8s/roles/traefik/README.md
2026-07-15 11:14:21 +03:00

486 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 |
### Диапазон портов
```
1000010999 — зарезервировано для Traefik-сервисов
открыт на firewalld на всех worker-нодах
```
Порт `10000` — зарезервирован (не использовать).
Порты `1000110999` — свободны для сервисов, назначаются последовательно.
---
## 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**