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

20 KiB
Raw Blame History

Traefik — стандарт публикации сервисов в кластере

Этот документ описывает стандарт подключения новых HTTP-сервисов к Traefik в нашем кластере.
Является эталоном для разработчиков и DevOps при добавлении любого нового сервиса.


Содержание

  1. Архитектура: как Traefik работает в кластере
  2. Единственный способ добавить сервис: traefik_port_map
  3. Пример 1: сервис без аутентификации
  4. Пример 2: сервис с BasicAuth
  5. Пример 3: сервис в том же namespace что и Traefik
  6. Пример 4: несколько маршрутов для одного приложения
  7. Создание BasicAuth-секрета
  8. Добавление нового сервиса: пошаговая инструкция
  9. Ограничения и правила именования
  10. Диагностика и типичные ошибки
  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

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 существует

# Это должно быть в манифестах приложения (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

# 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: запустить плейбук

ansible-playbook -i inventory/prod playbooks/setup_traefik.yml

Шаг 4: проверить доступность

# С любой машины в сети
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 с учётными данными

# Сгенерировать хэш пароля и создать 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: добавить запись

# 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: запустить плейбук

ansible-playbook -i inventory/prod playbooks/setup_traefik.yml

Плейбук создаст Middleware объект:

# Автогенерируется из 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: проверить

# Без пароля — должен вернуть 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:

  - 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, их нужно опубликовать на разных портах независимо (например, для разных уровней доступа).

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

Один пользователь

kubectl create secret generic traefik-auth-<service-name> \
  --from-literal=users="$(openssl passwd -apr1 'PASSWORD' | xargs -I{} echo 'USERNAME:{}')" \
  -n traefik

Несколько пользователей

# Сгенерировать файл 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

Обновить пароль существующего пользователя

# Удалить старый 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: grafanasecret_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:

# Симптом: Pod застрял в Pending
kubectl get pods -n traefik -o wide

# Решение: принудительно удалить старые Pod
kubectl delete pod -n traefik -l app.kubernetes.io/name=traefik

10. Диагностика и типичные ошибки

Основные команды

# Список всех 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

# Запустить временный 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

Проверка конфигурации без применения

# Посмотреть что сгенерирует шаблон (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