495 lines
18 KiB
Markdown
495 lines
18 KiB
Markdown
# 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-ноде, диапазон `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-<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 занят | Диапазон 10000–10999 специфический; проверить `ss -tlnp` перед деплоем |
|
||
| DaemonSet пересоздаётся при добавлении нового порта в Helm values | Краткий downtime ~10с; все существующие соединения рвутся |
|
||
| Dashboard Service имеет `port: 443` но трафик HTTP | `scheme: http` обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия |
|