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

18 KiB
Raw Blame History

Traefik: port-proxy режим для внутренних сервисов

1. Контекст и цель

Режим работы: Traefik как HTTP-прокси на отдельном порту для каждого внутреннего сервиса.

Вместо маршрутизации по доменному имени каждый сервис получает выделенный порт на worker-ноде. Клиент обращается http://10.203.0.96:<PORT> и попадает напрямую на нужный сервис.

Что не используем: HTTPS, Let's Encrypt, hostname-роутинг, Ingress.

Что используем: EntryPoint per service, HTTP Router с catch-all правилом, BasicAuth Middleware для сервисов без авторизации.


2. Концепция Port Map

Port Map — единственный файл, описывающий всю топологию проксирования. Из него генерируется вся конфигурация Traefik: entrypoints, routers, services, middleware CRD.

Расположение: inventory/prod/group_vars/traefik.yml

Это inventory-файл, а не часть роли — привязка порта к сервису это инфраструктурное решение, а не логика установки.

Формат файла

# inventory/prod/group_vars/traefik.yml
---

traefik_port_map:

  - name: kubernetes-dashboard
    description: "Kubernetes Dashboard"
    port: 10001
    backend:
      namespace: kubernetes-dashboard
      service: kubernetes-dashboard
      port: 443        # порт Service; K8s перенаправит на pod:9090 (insecure HTTP)
      scheme: http     # dashboard работает без TLS (--insecure-port=9090 --port=0)
    basicauth:
      enabled: false   # Dashboard имеет собственную авторизацию по токену — BasicAuth не нужен

  - name: longhorn-ui
    description: "Longhorn Storage UI"
    port: 10002
    backend:
      namespace: longhorn-system
      service: longhorn-frontend
      port: 80
      scheme: http
    basicauth:
      enabled: true
      secret_name: traefik-auth-longhorn    # Longhorn UI без авторизации — защищаем BasicAuth

  # Шаблон для будущих сервисов:
  # - name: my-service
  #   description: "Human-readable описание"
  #   port: 10003              # уникальный порт в диапазоне 10000-10999
  #   backend:
  #     namespace: my-namespace
  #     service: my-service-name
  #     port: 8080
  #     scheme: http
  #   basicauth:
  #     enabled: false          # если сервис имеет собственную авторизацию

Поля Port Map

Поле Обязательное Описание
name да Уникальный идентификатор (используется в именах K8s-ресурсов)
description нет Человекочитаемое описание
port да Порт на Traefik-ноде, диапазон 1000010999
backend.namespace да Kubernetes namespace сервиса
backend.service да Имя Kubernetes Service
backend.port да Порт Service (не pod-а)
backend.scheme да http или https (протокол, которым Traefik достучится до Service)
basicauth.enabled нет true — прикрепить BasicAuth; дефолт false
basicauth.secret_name если enabled Имя Secret в namespace traefik; создаётся вручную

3. Управление credentials: только вручную, не через Ansible

Принцип разделения ответственности:

  • Ansible управляет инфраструктурой: Middleware CRD, Helm-чарт, firewall, IngressRoute.
  • Оператор управляет секретами: Kubernetes Secret с htpasswd-строками создаётся вручную через Dashboard или kubectl.

Ansible не знает о содержимом секрета — только о его имени (secret_name в port_map). Если Secret не существует, Traefik вернёт 401 для всех запросов к этому сервису (безопасный дефолт).

Создание Secret вручную

Через kubectl (рекомендуется для первого раза):

# 1. Сгенерировать htpasswd-строку (требует htpasswd или openssl)
htpasswd -nb admin 'yourpassword'
# Вывод: admin:$apr1$xyz...

# 2. Создать Secret в namespace traefik
kubectl create secret generic traefik-auth-dashboard \
  --from-literal=users='admin:$apr1$xyz...' \
  -n traefik

# Для второго сервиса — отдельный Secret:
kubectl create secret generic traefik-auth-longhorn \
  --from-literal=users='admin:$apr1$abc...' \
  -n traefik

Через Kubernetes Dashboard:

  1. Открыть Dashboard → Namespace: traefik → Secrets → Create
  2. Тип: Opaque
  3. Имя: traefik-auth-dashboard (или как указано в secret_name)
  4. Ключ: users
  5. Значение: htpasswd-строка, например admin:$apr1$xyz...

Генерация htpasswd без утилиты htpasswd:

# Через openssl (доступен везде)
openssl passwd -apr1 'yourpassword'

# Собрать строку вручную:
echo "admin:$(openssl passwd -apr1 'yourpassword')"

Обновление пароля

# Пересоздать Secret с новым паролем
kubectl create secret generic traefik-auth-dashboard \
  --from-literal=users='admin:$apr1$newvalue...' \
  -n traefik \
  --dry-run=client -o yaml | kubectl apply -f -

Разные пользователи для разных сервисов

Каждый Secret независим. Для одного сервиса можно дать доступ нескольким пользователям — htpasswd-формат поддерживает несколько строк:

kubectl create secret generic traefik-auth-longhorn \
  --from-literal=users='admin:$apr1$...'$'\n''devops:$apr1$...' \
  -n traefply

4. Архитектура

4.1. Схема потока

Клиент
        │  http://10.203.0.96:10001  →  kubernetes-dashboard  (BasicAuth: traefik-auth-dashboard)
        │  http://10.203.0.96:10002  →  longhorn-ui           (BasicAuth: traefik-auth-longhorn)
        │  http://10.203.0.96:10003  →  future-service        (без BasicAuth)
        ▼
┌────────────────────────────────────┐
│           k8s-worker-01            │
│           10.203.0.96              │
│                                    │
│  ┌────────────────────────────┐    │
│  │       Traefik DaemonSet    │    │
│  │                            │    │
│  │  EP :10001 → Middleware ──────► svc kubernetes-dashboard:443 (http→pod:9090)
│  │            basicauth-dash  │    │
│  │                            │    │
│  │  EP :10002 → Middleware ──────► svc longhorn-frontend:80
│  │            basicauth-lhorn │    │
│  │                            │    │
│  │  EP :10003 ───────────────────► svc future:8080 (без auth)
│  └────────────────────────────┘    │
└────────────────────────────────────┘
                │  pod network (flannel 10.244.0.0/16)
    ┌───────────┼───────────┐
    ▼           ▼           ▼
  dashboard   longhorn   future
  pod:9090    pod:80     pod:8080

4.2. Соответствие port_map → Traefik-ресурсы

Каждая запись в traefik_port_map генерирует:

"kubernetes-dashboard" (port: 10001, basicauth.enabled: true)
  │
  ├── EntryPoint  "kubernetes-dashboard"         port 10001, hostPort 10001
  ├── Middleware  "basicauth-kubernetes-dashboard"
  │       └── basicAuth.secret: traefik-auth-dashboard   (Secret создан вручную)
  ├── HTTP Router "router-kubernetes-dashboard"
  │       entryPoints: [kubernetes-dashboard]
  │       rule: PathPrefix(`/`)
  │       middlewares: [basicauth-kubernetes-dashboard]
  └── HTTP Service "svc-kubernetes-dashboard"
          url: http://kubernetes-dashboard.kubernetes-dashboard.svc:443

4.3. Почему Dashboard: scheme: http, port: 443

В текущей конфигурации Dashboard пропатчен с --insecure-port=9090 --port=0:

Service spec: port 443 → targetPort 9090
Pod: слушает на :9090 по HTTP (без TLS)

Traefik подключается к ClusterIP:443 по протоколу HTTP. Kubernetes направляет трафик на pod:9090. TLS нет на всём пути.


5. Структура роли

roles/traefik/
  defaults/main.yml          — chart version, namespace, kubeconfig path
  tasks/main.yml             — include_tasks по шагам
  tasks/helm.yml             — helm upgrade --install
  tasks/middleware.yml       — Middleware CRD (один на сервис, если basicauth.enabled)
  tasks/routes.yml           — IngressRoute CRD (один на сервис)
  tasks/firewall.yml         — открыть диапазон 10000-10999 на workers (delegate_to)
  templates/
    traefik-values.yml.j2        — Helm values, генерируется из traefik_port_map
    basicauth-middleware.yml.j2  — Middleware CRD (один экземпляр, рендерится в цикле)
    ingressroute.yml.j2          — IngressRoute CRD (один экземпляр, рендерится в цикле)

playbooks/
  setup_traefik.yml

6. Ключевые конфигурационные артефакты

6.1. roles/traefik/defaults/main.yml

---
traefik_chart_version: "32.1.0"
traefik_namespace: traefik
traefik_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
traefik_port_map: []   # переопределяется в inventory/prod/group_vars/traefik.yml

6.2. roles/traefik/templates/traefik-values.yml.j2

deployment:
  kind: DaemonSet

nodeSelector:
  kubernetes.io/os: linux

ports:
{% for svc in traefik_port_map %}
  {{ svc.name }}:
    port: {{ svc.port }}
    hostPort: {{ svc.port }}
    expose:
      default: true
    exposedPort: {{ svc.port }}
    protocol: TCP
{% endfor %}
  web:
    expose:
      default: false
  websecure:
    expose:
      default: false

ingressRoute:
  dashboard:
    enabled: false

providers:
  kubernetesCRD:
    enabled: true
  kubernetesIngress:
    enabled: false

persistence:
  enabled: false

logs:
  general:
    level: INFO
  access:
    enabled: true

6.3. roles/traefik/templates/basicauth-middleware.yml.j2

Рендерится в цикле для каждого сервиса с basicauth.enabled: true:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: basicauth-{{ item.name }}
  namespace: {{ traefik_namespace }}
spec:
  basicAuth:
    secret: {{ item.basicauth.secret_name }}
    removeHeader: true

6.4. roles/traefik/templates/ingressroute.yml.j2

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: route-{{ item.name }}
  namespace: {{ traefik_namespace }}
spec:
  entryPoints:
    - {{ item.name }}
  routes:
    - match: PathPrefix(`/`)
      kind: Rule
      services:
        - name: {{ item.backend.service }}
          namespace: {{ item.backend.namespace }}
          port: {{ item.backend.port }}
          scheme: {{ item.backend.scheme }}
{% if item.basicauth.enabled | default(false) %}
      middlewares:
        - name: basicauth-{{ item.name }}
          namespace: {{ traefik_namespace }}
{% endif %}

6.5. roles/traefik/tasks/middleware.yml

---
- name: Middleware | Render BasicAuth CRD manifests
  ansible.builtin.template:
    src: basicauth-middleware.yml.j2
    dest: "/tmp/traefik-middleware-{{ item.name }}.yml"
    mode: "0600"
  loop: "{{ traefik_port_map | selectattr('basicauth.enabled', 'defined') | selectattr('basicauth.enabled') | list }}"
  loop_control:
    label: "{{ item.name }}"

- name: Middleware | Apply BasicAuth CRD manifests
  ansible.builtin.command: >
    kubectl apply -f /tmp/traefik-middleware-{{ item.name }}.yml    
  environment:
    KUBECONFIG: "{{ traefik_kubeconfig }}"
    PATH: "/usr/local/bin:/usr/bin:/bin"
  loop: "{{ traefik_port_map | selectattr('basicauth.enabled', 'defined') | selectattr('basicauth.enabled') | list }}"
  loop_control:
    label: "{{ item.name }}"
  register: _mw_apply
  changed_when: "'configured' in _mw_apply.stdout or 'created' in _mw_apply.stdout"

6.6. roles/traefik/tasks/firewall.yml

---
- name: Firewall | Open Traefik service port range on workers
  ansible.posix.firewalld:
    port: "10000-10999/tcp"
    permanent: true
    state: enabled
    immediate: true
  delegate_to: "{{ item }}"
  loop: "{{ groups['workers'] }}"

- name: Firewall | Trust CNI interfaces on workers
  ansible.posix.firewalld:
    zone: trusted
    interface: "{{ iface }}"
    permanent: true
    state: enabled
    immediate: true
  loop: "{{ groups['workers'] }}"
  loop_control:
    loop_var: worker_host
  vars:
    _cni_ifaces:
      - flannel.1
      - cni0
  delegate_to: "{{ worker_host }}"
  with_nested:
    - "{{ groups['workers'] }}"
    - [flannel.1, cni0]

7. Сводная таблица портов

Поддерживать синхронизированной с inventory/prod/group_vars/traefik.yml:

Порт Сервис BasicAuth Secret Описание
10001 kubernetes-dashboard нет K8s Dashboard (своя авторизация по токену)
10002 longhorn-frontend да traefik-auth-longhorn Longhorn UI
1000310999 (резерв) Будущие сервисы

8. Порядок развёртывания

Шаг 1: Развернуть Traefik через Ansible

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

Ansible создаёт: DaemonSet, Middleware CRD (ссылается на несуществующие пока Secret), IngressRoute, firewall-правила.

На данном этапе Traefik вернёт 500 для сервисов с BasicAuth — Secret ещё не создан.

Шаг 2: Создать Secrets вручную (только для сервисов с basicauth.enabled: true)

# Longhorn — BasicAuth включён
kubectl create secret generic traefik-auth-longhorn \
  --from-literal=users="$(htpasswd -nb admin 'yourpassword')" \
  -n traefik

# Dashboard — Secret не нужен, сервис защищён собственной авторизацией по токену

После создания Secret Traefik подхватит его автоматически (без рестарта).

Шаг 3: Проверить доступ

# Dashboard — открывается без BasicAuth, но потребует токен внутри UI
curl http://10.203.0.96:10001/

# Longhorn — потребует BasicAuth
curl -u admin:yourpassword http://10.203.0.96:10002/

9. Добавление нового сервиса (runbook)

  1. Добавить запись в inventory/prod/group_vars/traefik.yml
  2. Перезапустить setup_traefik.yml
  3. Создать Secret, если basicauth.enabled: true:
    kubectl create secret generic traefik-auth-<name> \
      --from-literal=users="$(htpasswd -nb user 'pass')" \
      -n traefik
    
  4. Обновить таблицу портов в разделе 7 этого документа

10. Интеграция с CI/CD

# .gitlab-ci.yml

# Добавить в syntax-check:
- ansible-playbook --syntax-check -i $INVENTORY playbooks/setup_traefik.yml

# Новый job:
setup:traefik:
  stage: setup
  script:
    - ansible-playbook -i $INVENTORY playbooks/setup_traefik.yml
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  resource_group: production

Secrets с паролями не передаются через GitLab CI/CD variables — они создаются вручную напрямую в кластере.


11. Риски и gotchas

Риск Митигация
Secret не создан — Traefik возвращает 500 Ожидаемое поведение; создать Secret и Traefik подхватит без рестарта
flannel.1/cni0 не в trusted zone Явная задача в tasks/firewall.yml
hostPort занят Диапазон 1000010999 специфический; проверить ss -tlnp перед деплоем
DaemonSet пересоздаётся при добавлении нового порта в Helm values Краткий downtime ~10с; все существующие соединения рвутся
Dashboard Service имеет port: 443 но трафик HTTP scheme: http обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия