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

495 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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-файл, а не часть роли — привязка порта к сервису это инфраструктурное решение, а не логика установки.
### Формат файла
```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-ноде, диапазон `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 (рекомендуется для первого раза):**
```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 |
| 1000310999 | (резерв) | — | — | Будущие сервисы |
---
## 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-<name> \
--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 занят | Диапазон 1000010999 специфический; проверить `ss -tlnp` перед деплоем |
| DaemonSet пересоздаётся при добавлении нового порта в Helm values | Краткий downtime ~10с; все существующие соединения рвутся |
| Dashboard Service имеет `port: 443` но трафик HTTP | `scheme: http` обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия |