18 KiB
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-ноде, диапазон 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 (рекомендуется для первого раза):
# 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:
- Открыть Dashboard → Namespace:
traefik→ Secrets → Create - Тип:
Opaque - Имя:
traefik-auth-dashboard(или как указано вsecret_name) - Ключ:
users - Значение: 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 |
| 10003–10999 | (резерв) | — | — | Будущие сервисы |
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)
- Добавить запись в
inventory/prod/group_vars/traefik.yml - Перезапустить
setup_traefik.yml - Создать Secret, если
basicauth.enabled: true:kubectl create secret generic traefik-auth-<name> \ --from-literal=users="$(htpasswd -nb user 'pass')" \ -n traefik - Обновить таблицу портов в разделе 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 занят | Диапазон 10000–10999 специфический; проверить ss -tlnp перед деплоем |
| DaemonSet пересоздаётся при добавлении нового порта в Helm values | Краткий downtime ~10с; все существующие соединения рвутся |
Dashboard Service имеет port: 443 но трафик HTTP |
scheme: http обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия |