# Traefik: port-proxy режим для внутренних сервисов ## 1. Контекст и цель **Режим работы:** Traefik как HTTP-прокси на отдельном порту для каждого внутреннего сервиса. Вместо маршрутизации по доменному имени каждый сервис получает выделенный порт на worker-ноде. Клиент обращается `http://10.203.0.96:` и попадает напрямую на нужный сервис. **Что не используем:** 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-файл, а не часть роли — привязка порта к сервису это инфраструктурное решение, а не логика установки. ### Формат файла ```yaml # 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-ноде, диапазон `10000–10999` | | `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 (рекомендуется для первого раза):** ```bash # 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:** ```bash # Через openssl (доступен везде) openssl passwd -apr1 'yourpassword' # Собрать строку вручную: echo "admin:$(openssl passwd -apr1 'yourpassword')" ``` ### Обновление пароля ```bash # Пересоздать 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-формат поддерживает несколько строк: ```bash 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` ```yaml --- 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` ```yaml 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`: ```yaml 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` ```yaml 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` ```yaml --- - 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` ```yaml --- - 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 | | 10003–10999 | (резерв) | — | — | Будущие сервисы | --- ## 8. Порядок развёртывания ### Шаг 1: Развернуть Traefik через Ansible ```bash 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) ```bash # Longhorn — BasicAuth включён kubectl create secret generic traefik-auth-longhorn \ --from-literal=users="$(htpasswd -nb admin 'yourpassword')" \ -n traefik # Dashboard — Secret не нужен, сервис защищён собственной авторизацией по токену ``` После создания Secret Traefik подхватит его автоматически (без рестарта). ### Шаг 3: Проверить доступ ```bash # 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`: ```bash kubectl create secret generic traefik-auth- \ --from-literal=users="$(htpasswd -nb user 'pass')" \ -n traefik ``` 4. Обновить таблицу портов в разделе 7 этого документа --- ## 10. Интеграция с CI/CD ```yaml # .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 занят | Диапазон 10000–10999 специфический; проверить `ss -tlnp` перед деплоем | | DaemonSet пересоздаётся при добавлении нового порта в Helm values | Краткий downtime ~10с; все существующие соединения рвутся | | Dashboard Service имеет `port: 443` но трафик HTTP | `scheme: http` обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия |