# 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- \ --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-`, где `` совпадает с полем `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 4. (если BasicAuth нужен) Создать Secret в namespace traefik └─ kubectl create secret generic traefik-auth- ... 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:/ └─ kubectl get ingressroute -n traefik └─ kubectl describe ingressroute route- -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 `, проверить 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**