feat: добавить аддон ext-proxy — проксировать внешние сервисы через ingress-nginx

Helm chart (один чарт создаёт Service + Endpoints + Ingress на каждый прокси):
- _helpers.tpl: хелперы ext-proxy.resourceName, ext-proxy.labels
- service.yaml: ClusterIP без selector — имя совпадает с Endpoints
- endpoints.yaml: внешние IP(s) + порт; несколько IP → round-robin через kube-proxy
- ingress.yaml: слияние аннотаций (defaults → сгенерированные → уровень прокси);
  поддержка TLS, basic auth, WebSocket, несколько хостов, маршрутизация по пути
- secret-auth.yaml: htpasswd Secret создаётся только при auth.enabled=true + credentials
- NOTES.txt: список прокси + команды проверки после установки

Ansible роль:
- defaults/main.yml: ext_proxy_namespace, ext_proxy_defaults, ext_proxy_proxies
- tasks/main.yml: валидация → namespace → копировать chart → lint → helm upgrade --install --atomic
- templates/values.yaml.j2: преобразование Ansible-переменных в Helm values через to_yaml

Интеграция: Makefile addon-ext-proxy, флаг addons.yml, playbooks/addons.yml,
            docs/addons.md, README.md (счётчик 37 аддонов)

README.md на русском языке с полной документацией:
архитектура, настройка, функции, DNS, проверка, примеры манифестов, устранение неисправностей

Дополнительно: splitgw_deploy_mode изменён на k8s
This commit is contained in:
Sergey Antropoff
2026-04-26 07:21:41 +03:00
parent 07fdc9a994
commit aae7941416
19 changed files with 1408 additions and 3 deletions

View File

@@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \
addon-harbor addon-gitea addon-owncloud addon-nextcloud \ addon-harbor addon-gitea addon-owncloud addon-nextcloud \
addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \ addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \
addon-smtp-relay addon-vault addon-external-secrets \ addon-smtp-relay addon-vault addon-external-secrets \
addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw \ addon-jenkins addon-netbird addon-mediaserver addon-hysteria2-server addon-splitgw addon-ext-proxy \
add-node remove-node \ add-node remove-node \
add-etcd-node remove-etcd-node \ add-etcd-node remove-etcd-node \
etcd-backup etcd-restore etcd-list-snapshots \ etcd-backup etcd-restore etcd-list-snapshots \
@@ -420,6 +420,10 @@ addon-splitgw: _check_env _check_image ## Установить Split Gateway —
@printf "$(CYAN)Устанавливаю Split Gateway (sing-box + Hysteria2)...$(NC)\n" @printf "$(CYAN)Устанавливаю Split Gateway (sing-box + Hysteria2)...$(NC)\n"
$(DOCKER_RUN) addon splitgw $(ARGS) $(DOCKER_RUN) addon splitgw $(ARGS)
addon-ext-proxy: _check_env _check_image ## Проксировать внешние сервисы через ingress-nginx (ARGS="-e ext_proxy_vip=192.168.1.x")
@printf "$(CYAN)Устанавливаю External Services Ingress Proxy...$(NC)\n"
$(DOCKER_RUN) addon ext-proxy $(ARGS)
# Generic цель — любой аддон из addons/<name>/playbook.yml # Generic цель — любой аддон из addons/<name>/playbook.yml
addon-%: _check_env _check_image addon-%: _check_env _check_image
@if [ ! -f "addons/$*/playbook.yml" ]; then \ @if [ ! -f "addons/$*/playbook.yml" ]; then \

View File

@@ -38,7 +38,7 @@ HA-режим (embedded etcd): при отказе **любой одной** н
**CNI:** `flannel` (встроен) | `calico` (Network Policy, BGP) | `cilium` (eBPF, Hubble) **CNI:** `flannel` (встроен) | `calico` (Network Policy, BGP) | `cilium` (eBPF, Hubble)
## Аддоны (36) ## Аддоны (37)
| Категория | Аддоны | | Категория | Аддоны |
|---|---| |---|---|
@@ -52,6 +52,7 @@ HA-режим (embedded etcd): при отказе **любой одной** н
| **Файловые хранилища** | nextcloud, owncloud | | **Файловые хранилища** | nextcloud, owncloud |
| **Медиасервер** | mediaserver — Plex, Sonarr, Radarr, Lidarr, Bazarr, Prowlarr + Hysteria2, Overseerr, Transmission, Samba | | **Медиасервер** | mediaserver — Plex, Sonarr, Radarr, Lidarr, Bazarr, Prowlarr + Hysteria2, Overseerr, Transmission, Samba |
| **VPN / Прокси** | splitgw — прозрачный split-tunnel gateway (sing-box + Hysteria2 TPROXY, YouTube → прокси) | | **VPN / Прокси** | splitgw — прозрачный split-tunnel gateway (sing-box + Hysteria2 TPROXY, YouTube → прокси) |
| **Ingress Proxy** | ext-proxy — проксировать внешние сервисы (IP:PORT) через ingress-nginx по домену |
Все аддоны включаются флагами в `group_vars/all/addons.yml`. Установка: `make addon-<name>`. Все аддоны включаются флагами в `group_vars/all/addons.yml`. Установка: `make addon-<name>`.

702
addons/ext-proxy/README.md Normal file
View File

@@ -0,0 +1,702 @@
# External Services Ingress Proxy
Проксирует сервисы, работающие **вне Kubernetes**, через существующий стек `ingress-nginx` + `kube-vip` с одним общим VIP. Для каждого внешнего сервиса аддон автоматически создаёт:
- **Service** (ClusterIP, без selector) — стабильный адрес внутри кластера
- **Endpoints** — указывает на внешний IP(s) и порт
- **Ingress** — правила маршрутизации по хосту/пути через ingress-nginx
- **Secret** *(опционально)* — htpasswd-учётные данные для basic auth
Маршрут трафика:
```
Интернет ──► kube-vip VIP ──► ingress-nginx ──► Service ──► Endpoints ──► Внешний IP:PORT
```
Все прокси-домены резолвятся в один IP kube-vip. Дополнительный LoadBalancer не создаётся.
---
## Быстрый старт
**1. Включи аддон и определи сервисы:**
```yaml
# group_vars/all/addons.yml
addon_ext_proxy: true
ext_proxy_proxies:
- name: plex
hosts: [plex.home.ru]
ips: [192.168.1.50]
port: 32400
- name: router
hosts: [router.home.ru]
ips: [192.168.1.1]
port: 8080
```
**2. Разверни:**
```bash
make addon-ext-proxy
# с явным VIP в сводке:
make addon-ext-proxy ARGS="-e ext_proxy_vip=192.168.1.100"
```
**3. Направь DNS на kube-vip:**
```
plex.home.ru IN A 192.168.1.100 # kube-vip VIP
router.home.ru IN A 192.168.1.100
```
**4. Открой в браузере:** `http://plex.home.ru`
---
## Архитектура
```
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────────┐ │
│ │ Ingress │ │ Service │ │ Endpoints │ │
│ │ (nginx) │───►│ ClusterIP │───►│ 192.168.1.50:32400│ │
│ │ plex.home.ru│ │ без sel-ra │ └────────────────────┘ │
│ └─────────────┘ └─────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────┐ │
│ │ kube-vip │ VIP: 192.168.1.100 │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Клиент: curl http://plex.home.ru
```
**Почему Service + Endpoints, а не ExternalName?**
Сервисы типа `ExternalName` используют DNS CNAME, что обходит kube-proxy и ломает TLS SNI. `ClusterIP + Endpoints` маршрутизирует трафик через обычный service mesh и поддерживает все возможности ingress-nginx (auth, терминация TLS, rewrite и т.д.).
---
## Справочник по конфигурации
Все настройки задаются в `group_vars/all/addons.yml`.
### Глобальные значения по умолчанию
```yaml
ext_proxy_namespace: "ext-proxy" # Kubernetes namespace
ext_proxy_release_name: "ext-proxy" # имя Helm release
ext_proxy_defaults:
ingressClass: nginx # класс Ingress (должен совпадать с именем в ingress-nginx)
tls:
enabled: false # включить TLS-терминацию на Ingress
secretName: "" # имя существующего TLS Secret (wildcard cert и т.п.)
certManager:
enabled: false # автоматически выпускать/обновлять сертификат через cert-manager
issuer: "" # имя ClusterIssuer (например, letsencrypt-prod)
issuerKind: ClusterIssuer # ClusterIssuer | Issuer
auth:
enabled: false # включить nginx basic authentication
credentials: "" # строка htpasswd: htpasswd -nb admin 'пароль'
secretName: "" # использовать существующий Secret вместо генерации нового
websocket: true # включить WebSocket (заголовки HTTP/1.1 upgrade)
path: / # путь Ingress по умолчанию
pathType: Prefix # Prefix | Exact | ImplementationSpecific
proxyConnectTimeout: 60 # nginx proxy_connect_timeout (секунды)
proxyReadTimeout: 3600 # nginx proxy_read_timeout
proxySendTimeout: 3600 # nginx proxy_send_timeout
proxyBodySize: "1g" # nginx client_max_body_size (0 = без ограничений)
annotations: {} # дополнительные аннотации для каждого Ingress
```
### Поля определения прокси
```yaml
ext_proxy_proxies:
- name: myservice # (обязательно) уникальное имя → имя ресурса в K8s
hosts: # (обязательно) список хостов
- myservice.home.ru
ips: # (обязательно) внешние IP(s)
- 192.168.1.100
port: 8080 # (обязательно) внешний порт
# --- Все поля ниже опциональны (переопределяют глобальные defaults) ---
path: / # путь Ingress
pathType: Prefix # Prefix | Exact
websocket: true # поддержка WebSocket
ingressClass: nginx # переопределить класс Ingress для этого прокси
tls:
enabled: true
secretName: wildcard-cert
certManager:
enabled: false
auth:
enabled: true
credentials: "admin:$apr1$abc..." # строка htpasswd
# ИЛИ: secretName: my-existing-auth-secret
annotations: # аннотации уровня прокси (переопределяют всё)
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "7200"
```
---
## Руководство по функциям
### TLS — существующий Secret (wildcard-сертификат)
Если есть wildcard-сертификат, управляемый cert-manager и хранящийся в Secret:
```yaml
ext_proxy_defaults:
tls:
enabled: true
secretName: wildcard-tls # должен существовать в ext_proxy_namespace
ext_proxy_proxies:
- name: plex
hosts: [plex.home.ru]
ips: [192.168.1.50]
port: 32400
```
Переопределить TLS для конкретного прокси:
```yaml
ext_proxy_proxies:
- name: router
hosts: [router.home.ru]
ips: [192.168.1.1]
port: 8080
tls:
enabled: false # отключить TLS только для этого прокси
```
### TLS — автоматический выпуск через cert-manager
```yaml
ext_proxy_defaults:
tls:
enabled: true
certManager:
enabled: true
issuer: letsencrypt-prod # должен существовать как ClusterIssuer
issuerKind: ClusterIssuer
```
cert-manager автоматически выпустит сертификат для каждого хоста и сохранит его в Secret с именем `<имя-прокси>-tls`.
> **Важно:** ACME-челлендж cert-manager требует публичной доступности домена. Для локальных/LAN-доменов используй DNS-01 challenge или wildcard-сертификат.
### Basic Authentication
Сгенерируй учётные данные htpasswd (установи `apache2-utils` или `httpd-tools`):
```bash
htpasswd -nb admin 'мойсекретныйпароль'
# выводит: admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/
```
```yaml
ext_proxy_defaults:
auth:
enabled: true
credentials: "admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/"
```
Или выборочно по конкретным прокси:
```yaml
ext_proxy_proxies:
- name: router
hosts: [router.home.ru]
ips: [192.168.1.1]
port: 8080
auth:
enabled: true
credentials: "admin:$apr1$..."
- name: plex # без auth
hosts: [plex.home.ru]
ips: [192.168.1.50]
port: 32400
```
Использование существующего Secret (ключ должен называться `auth`, тип `Opaque`):
```yaml
auth:
enabled: true
secretName: my-shared-htpasswd # создать вручную через kubectl
```
> **Примечание:** `configuration-snippet` должен быть разрешён в ingress-nginx (`allow-snippet-annotations: true`), если добавляешь кастомные сниппеты. Стандартные аннотации работают без этого.
### WebSocket (Plex, Grafana, Home Assistant и т.д.)
WebSocket включён по умолчанию (`websocket: true`). Это устанавливает:
```
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
```
ingress-nginx автоматически проксирует заголовки `Upgrade` / `Connection` при использовании HTTP/1.1. Отключи для конкретного прокси если не нужно:
```yaml
- name: router
...
websocket: false
```
### Несколько хостов для одного сервиса
```yaml
- name: plex
hosts:
- plex.home.ru
- plex.internal
- plex.lan
ips: [192.168.1.50]
port: 32400
```
Для каждого хоста создаётся отдельное правило Ingress, все ссылаются на один и тот же backend.
### Несколько backend IP (round-robin / резервирование)
```yaml
- name: homeassistant
hosts: [ha.home.ru]
ips:
- 192.168.1.100 # основной
- 192.168.1.101 # резервный
port: 8123
```
K8s создаёт два адреса в объекте `Endpoints`. kube-proxy распределяет трафик между ними по round-robin. Для активно-пассивного резервирования необходим внешний механизм проверки работоспособности.
### Маршрутизация по пути
```yaml
- name: grafana
hosts: [tools.home.ru]
ips: [192.168.1.60]
port: 3000
path: /grafana
pathType: Prefix
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # убрать префикс /grafana
```
### Кастомные аннотации nginx
```yaml
- name: plex
hosts: [plex.home.ru]
ips: [192.168.1.50]
port: 32400
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0" # без ограничений на загрузку
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" # 1 час
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
```
Аннотации уровня прокси имеют наивысший приоритет и переопределяют все defaults и сгенерированные аннотации.
---
## Как добавить новый внешний сервис
1. Добавь запись в `ext_proxy_proxies` в файле `group_vars/all/addons.yml`:
```yaml
ext_proxy_proxies:
# ... существующие записи ...
- name: homeassistant
hosts: [ha.home.ru]
ips: [192.168.1.150]
port: 8123
websocket: true
```
2. Запусти аддон повторно:
```bash
make addon-ext-proxy
```
Helm upgrade идемпотентен — существующие ресурсы обновляются, новые добавляются.
3. Добавь DNS-запись для нового хоста (см. раздел DNS ниже).
---
## Настройка DNS
Все прокси-хосты должны резолвиться в **kube-vip VIP**.
### Вариант A — роутер / домашний DNS-сервер
В настройках DHCP/DNS роутера (Keenetic, pfSense, AdGuard Home, Pi-hole и т.п.):
```
plex.home.ru → 192.168.1.100 (kube-vip VIP)
router.home.ru → 192.168.1.100
grafana.home.local → 192.168.1.100
```
### Вариант B — /etc/hosts (на конкретной машине, для тестирования)
```
192.168.1.100 plex.home.ru router.home.ru grafana.home.local
```
### Вариант C — rewrite в CoreDNS (только внутри кластера)
Если прокси доступен только из кластера, добавь rewrite в CoreDNS:
```yaml
# configmap coredns — добавить в Corefile
rewrite name plex.home.ru ingress-nginx.ingress-nginx.svc.cluster.local
```
### Узнать VIP kube-vip
```bash
# EXTERNAL-IP сервиса ingress-nginx LoadBalancer — это и есть VIP:
kubectl -n ingress-nginx get svc ingress-nginx-controller
```
---
## Проверка
### 1. Убедись что ресурсы созданы
```bash
kubectl -n ext-proxy get all,ingress,endpoints
# Ожидаемый вывод:
# NAME TYPE CLUSTER-IP ...
# service/plex ClusterIP 10.43.x.x ...
# service/router ClusterIP 10.43.x.x ...
#
# NAME ENDPOINTS ...
# endpoints/plex 192.168.1.50:32400 ...
# endpoints/router 192.168.1.1:8080 ...
```
### 2. Проверь Endpoints заполнены
```bash
kubectl -n ext-proxy describe endpoints plex
# Должно показать "Addresses: 192.168.1.50" и "Ports: http 32400/TCP"
```
### 3. Проверь соединение изнутри кластера
```bash
kubectl run curl --rm -it --image=curlimages/curl -- \
curl -v http://plex.ext-proxy.svc.cluster.local:32400
```
### 4. Проверь внешний доступ
```bash
# Замени IP на kube-vip VIP
curl -H "Host: plex.home.ru" http://192.168.1.100/
# Через DNS:
curl http://plex.home.ru/
```
### 5. Убедись что ingress-nginx применил правила
```bash
kubectl -n ext-proxy describe ingress plex
# Должно показать Rules → host → plex.home.ru → plex:32400
# Проверь что конфиг nginx обновился:
kubectl -n ingress-nginx exec -it \
$(kubectl -n ingress-nginx get pod -l 'app.kubernetes.io/name=ingress-nginx' -o name | head -1) \
-- nginx -T | grep plex
```
---
## Пример сгенерированных манифестов
При такой конфигурации:
```yaml
ext_proxy_proxies:
- name: plex
hosts:
- plex.home.ru
ips:
- 192.168.1.50
port: 32400
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
```
Создаются следующие ресурсы Kubernetes:
**Service:**
```yaml
apiVersion: v1
kind: Service
metadata:
name: plex
namespace: ext-proxy
spec:
type: ClusterIP
ports:
- name: http
port: 32400
targetPort: 32400
protocol: TCP
```
**Endpoints:**
```yaml
apiVersion: v1
kind: Endpoints
metadata:
name: plex
namespace: ext-proxy
subsets:
- addresses:
- ip: "192.168.1.50"
ports:
- name: http
port: 32400
protocol: TCP
```
**Ingress:**
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plex
namespace: ext-proxy
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-body-size: "0" # переопределено на уровне прокси
nginx.ingress.kubernetes.io/proxy-http-version: "1.1" # websocket=true
spec:
ingressClassName: nginx
rules:
- host: plex.home.ru
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: plex
port:
number: 32400
```
---
## Helm Chart — самостоятельное использование без Ansible
Чарт находится в `addons/ext-proxy/role/chart/`. Развернуть напрямую:
```bash
# Из корня проекта:
helm upgrade --install ext-proxy addons/ext-proxy/role/chart \
--namespace ext-proxy \
--create-namespace \
--values my-values.yaml
```
Сгенерировать манифесты без деплоя (для проверки):
```bash
helm template ext-proxy addons/ext-proxy/role/chart \
--values my-values.yaml
```
---
## Устранение неисправностей
### 502 Bad Gateway
**Причина:** ingress-nginx достигает Service, но Service не может достучаться до внешнего IP.
```bash
# 1. Проверь Endpoints заполнены:
kubectl -n ext-proxy get endpoints plex
# "Addresses" не должен быть пустым. Если "<none>" — Endpoints отсутствует или некорректен.
# 2. Проверь доступность внешнего IP с ноды кластера:
ssh master01
curl -v http://192.168.1.50:32400
# 3. Проверь файрвол на внешнем хосте — он должен разрешать входящие из CIDR кластера.
```
### 503 Service Temporarily Unavailable
**Причина:** у ingress-nginx нет здоровых Endpoints.
```bash
# Проверь наличие endpoints:
kubectl -n ext-proxy describe endpoints plex
# Убедись что у service есть ClusterIP:
kubectl -n ext-proxy get svc plex
# Проверь логи ingress-nginx:
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --tail=100
```
### 404 Not Found
**Причина:** правило Ingress не совпало — ошибка в имени хоста или пути.
```bash
# 1. Проверь заголовок Host при запросе:
curl -v -H "Host: plex.home.ru" http://192.168.1.100/
# 2. Посмотри описание Ingress:
kubectl -n ext-proxy describe ingress plex
# Раздел "Rules" — хост и путь должны совпадать точно.
# 3. Проверь класс Ingress:
kubectl -n ext-proxy get ingress plex -o jsonpath='{.spec.ingressClassName}'
# Должен совпадать с классом ingress-nginx (обычно "nginx")
```
### DNS не резолвится
```bash
# Узнай куда резолвится хост:
nslookup plex.home.ru
dig plex.home.ru
# Должен вернуть kube-vip VIP, а не реальный IP сервера Plex.
# Проверь с явным IP:
curl -H "Host: plex.home.ru" http://<kube-vip-IP>/
```
### Проблемы с TLS / HTTPS
```bash
# Проверь наличие TLS Secret в нужном namespace:
kubectl -n ext-proxy get secret wildcard-tls
# Проверь что cert-manager выпустил сертификат:
kubectl -n ext-proxy get certificate
kubectl -n ext-proxy describe certificate plex
# Логи cert-manager:
kubectl -n cert-manager logs -l app=cert-manager --tail=50
```
### Basic Auth возвращает 401
```bash
# Проверь с учётными данными:
curl -u admin:мойпароль http://plex.home.ru/
# Убедись что в Secret есть ключ "auth":
kubectl -n ext-proxy get secret plex-auth -o jsonpath='{.data.auth}' | base64 -d
# Должно вывести: admin:$apr1$...
# Проверь аннотации в Ingress:
kubectl -n ext-proxy get ingress plex -o yaml | grep auth
```
### Обрывы WebSocket-соединения
```bash
# Проверь аннотацию proxy-http-version:
kubectl -n ext-proxy get ingress plex -o yaml | grep proxy-http
# Для сервисов где нужен длинный таймаут (стриминг):
# Добавь в annotations:
# nginx.ingress.kubernetes.io/proxy-read-timeout: "86400"
# nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
```
### Логи ingress-nginx
```bash
# Смотреть логи nginx в реальном времени:
kubectl -n ingress-nginx logs \
-l app.kubernetes.io/name=ingress-nginx \
--tail=100 -f
# Проверить nginx.conf применился:
kubectl -n ingress-nginx exec \
$(kubectl -n ingress-nginx get pod -l 'app.kubernetes.io/name=ingress-nginx' -o name | head -1) \
-- cat /etc/nginx/nginx.conf | grep -A5 plex
```
---
## Удаление
```bash
helm -n ext-proxy uninstall ext-proxy
kubectl delete namespace ext-proxy
```
Удалить только один прокси без полного сноса релиза:
1. Убери запись из `ext_proxy_proxies` в `group_vars/all/addons.yml`
2. Запусти `make addon-ext-proxy` — Helm upgrade удалит убранные ресурсы
---
## Интеграция с проектом
| Зависимость | Примечание |
|---|---|
| `ingress-nginx` (`addon_ingress_nginx: true`) | Должен быть установлен первым |
| `kube-vip` | Предоставляет VIP для LoadBalancer сервиса ingress-nginx |
| `cert-manager` (опционально) | Нужен только при `tls.certManager.enabled: true` |
### Команды Makefile
```bash
make addon-ext-proxy # развернуть / обновить
make addon-ext-proxy ARGS="-e ext_proxy_vip=..." # с явным VIP в сводке
```
### Переменные Ansible
| Переменная | По умолчанию | Описание |
|---|---|---|
| `ext_proxy_namespace` | `ext-proxy` | Kubernetes namespace |
| `ext_proxy_release_name` | `ext-proxy` | Имя Helm release |
| `ext_proxy_proxies` | `[]` | Список определений внешних сервисов |
| `ext_proxy_defaults.*` | см. defaults | Глобальные значения по умолчанию |
| `ext_proxy_vip` | `""` | kube-vip VIP — отображается в сводке после установки |

View File

@@ -0,0 +1,7 @@
---
- name: Install External Services Ingress Proxy
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/role"

View File

@@ -0,0 +1,19 @@
apiVersion: v2
name: ext-proxy
description: |
Proxies external services (outside Kubernetes) through ingress-nginx.
Creates Service + Endpoints + Ingress for each configured host.
Supports TLS, basic auth, WebSocket, multi-host, and multiple backend IPs.
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- ingress
- proxy
- external-services
- nginx
home: https://git.antropoff.ru/DevOpsTools/K3S
sources:
- https://git.antropoff.ru/DevOpsTools/K3S
maintainers:
- name: k3s-ansible

View File

@@ -0,0 +1,42 @@
╔══════════════════════════════════════════════════════════════╗
║ External Services Ingress Proxy — Installed ║
╚══════════════════════════════════════════════════════════════╝
Namespace : {{ .Release.Namespace }}
Release : {{ .Release.Name }}
Proxied services:
{{- range .Values.proxies }}
{{- $proxy := . }}
{{- $proxyName := include "ext-proxy.resourceName" $proxy.name }}
{{- $tlsEnabled := $proxy.tls | default dict | dig "enabled" ($.Values.defaults.tls.enabled | default false) }}
{{- $schema := "http" }}
{{- if $tlsEnabled }}{{ $schema = "https" }}{{ end }}
• {{ $proxyName }}
Hosts : {{ $proxy.hosts | default (list ($proxy.host | default "?")) | join ", " }}
Backend: {{ $proxy.ips | default (list $proxy.ip) | join ", " }}:{{ $proxy.port }}
URL : {{ $schema }}://{{ $proxy.hosts | default (list ($proxy.host | default "")) | first }}
{{- end }}
─── Verify ────────────────────────────────────────────────────
# List all Ingress resources:
kubectl -n {{ .Release.Namespace }} get ingress
# Check Endpoints are populated:
kubectl -n {{ .Release.Namespace }} get endpoints
# Describe a specific Ingress:
kubectl -n {{ .Release.Namespace }} describe ingress <name>
─── DNS ───────────────────────────────────────────────────────
Point all proxy hostnames to your kube-vip VIP:
<proxy-host> IN A <kube-vip-IP>
─── Troubleshooting ───────────────────────────────────────────
# ingress-nginx logs:
kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --tail=50 -f
# Check connectivity from a pod:
kubectl run curl --rm -it --image=curlimages/curl -- \
curl -v http://<service-name>.<namespace>:<port>

View File

@@ -0,0 +1,51 @@
{{/*
Normalize a proxy name to be safe as a Kubernetes resource name.
Lowercases, replaces underscores and dots with hyphens, trims to 63 chars.
Usage: {{ include "ext-proxy.resourceName" "my_service.name" }}
*/}}
{{- define "ext-proxy.resourceName" -}}
{{- . | lower | replace "_" "-" | replace "." "-" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Chart label string: name-version (used in helm.sh/chart label).
*/}}
{{- define "ext-proxy.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels applied to all resources.
*/}}
{{- define "ext-proxy.labels" -}}
helm.sh/chart: {{ include "ext-proxy.chart" . }}
app.kubernetes.io/name: {{ default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Resolve a per-proxy boolean setting with fallback to global default.
Usage: {{ include "ext-proxy.boolSetting" (dict "proxy" $proxy "defaults" $d "key" "websocket" "fallback" true) }}
*/}}
{{- define "ext-proxy.boolSetting" -}}
{{- $proxyVal := index .proxy .key }}
{{- $defaultVal := index .defaults .key }}
{{- if ne $proxyVal nil }}{{ $proxyVal }}
{{- else if ne $defaultVal nil }}{{ $defaultVal }}
{{- else }}{{ .fallback }}
{{- end }}
{{- end }}
{{/*
Resolve a per-proxy string setting with fallback to global default.
Usage: {{ include "ext-proxy.strSetting" (dict "proxy" $proxy "defaults" $d "key" "path" "fallback" "/") }}
*/}}
{{- define "ext-proxy.strSetting" -}}
{{- $proxyVal := index .proxy .key }}
{{- $defaultVal := index .defaults .key }}
{{- if and $proxyVal (ne $proxyVal "") }}{{ $proxyVal }}
{{- else if and $defaultVal (ne $defaultVal "") }}{{ $defaultVal }}
{{- else }}{{ .fallback }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,35 @@
{{/*
Creates one Endpoints object per proxy, pointing to the external IP(s) and port.
Rules:
- The Endpoints name MUST match the Service name exactly.
- Multiple IPs in .ips produce multiple addresses → K8s round-robins between them.
- Port name "http" MUST match the Service port name.
Traffic path:
ingress-nginx → ClusterIP Service → Endpoints → external IP:port
*/}}
{{- range .Values.proxies }}
{{- $proxy := . }}
{{- $proxyName := include "ext-proxy.resourceName" $proxy.name }}
{{- $ips := $proxy.ips | default (list $proxy.ip) }}
---
apiVersion: v1
kind: Endpoints
metadata:
name: {{ $proxyName }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "ext-proxy.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $proxyName }}
subsets:
- addresses:
{{- range $ips }}
# External IP — must be reachable from the cluster network
- ip: {{ . | quote }}
{{- end }}
ports:
- name: http
port: {{ $proxy.port }}
protocol: TCP
{{- end }}

View File

@@ -0,0 +1,131 @@
{{/*
Creates one Ingress per proxy entry.
Feature resolution order (highest → lowest priority):
1. Per-proxy annotations (.proxies[*].annotations) — override everything
2. Per-proxy feature flags (websocket, auth, tls…)
3. Global defaults (.defaults.*)
4. Built-in generated annotations (timeouts, body-size)
Annotation merge is done via successive dict mutations so that
per-proxy annotations always win, with no duplicate YAML keys.
*/}}
{{- range .Values.proxies }}
{{- $proxy := . }}
{{- $d := $.Values.defaults }}
{{- $proxyName := include "ext-proxy.resourceName" $proxy.name }}
{{/* ── Resolve per-proxy settings with fallback to defaults ────────────────── */}}
{{- $ingressClass := $proxy.ingressClass | default $d.ingressClass | default "nginx" }}
{{- $path := $proxy.path | default $d.path | default "/" }}
{{- $pathType := $proxy.pathType | default $d.pathType | default "Prefix" }}
{{- $connectTO := $proxy.proxyConnectTimeout | default $d.proxyConnectTimeout | default 60 }}
{{- $readTO := $proxy.proxyReadTimeout | default $d.proxyReadTimeout | default 3600 }}
{{- $sendTO := $proxy.proxySendTimeout | default $d.proxySendTimeout | default 3600 }}
{{- $bodySize := $proxy.proxyBodySize | default $d.proxyBodySize | default "1g" }}
{{/* websocket: check proxy first, then default, then true */}}
{{- $websocket := true }}
{{- if ne ($proxy.websocket | toString) "<nil>" }}
{{- $websocket = $proxy.websocket }}
{{- else if ne ($d.websocket | toString) "<nil>" }}
{{- $websocket = $d.websocket }}
{{- end }}
{{/* ── TLS: merge proxy-level overrides onto global defaults ───────────────── */}}
{{- $proxyTLS := $proxy.tls | default dict }}
{{- $defTLS := $d.tls | default dict }}
{{- $proxyCM := $proxyTLS.certManager | default dict }}
{{- $defCM := $defTLS.certManager | default dict }}
{{- $tlsEnabled := $proxyTLS.enabled | default $defTLS.enabled | default false }}
{{- $tlsSecret := $proxyTLS.secretName | default $defTLS.secretName | default "" }}
{{- $cmEnabled := $proxyCM.enabled | default $defCM.enabled | default false }}
{{- $cmIssuer := $proxyCM.issuer | default $defCM.issuer | default "" }}
{{- $cmKind := $proxyCM.issuerKind | default $defCM.issuerKind | default "ClusterIssuer" }}
{{/* ── Auth: merge proxy-level overrides onto global defaults ─────────────── */}}
{{- $proxyAuth := $proxy.auth | default dict }}
{{- $defAuth := $d.auth | default dict }}
{{- $authEnabled := $proxyAuth.enabled | default $defAuth.enabled | default false }}
{{- $authSecret := "" }}
{{- if $authEnabled }}
{{- $authSecret = $proxyAuth.secretName | default $defAuth.secretName | default (printf "%s-auth" $proxyName) }}
{{- end }}
{{/* ── Hosts: support .hosts list or single .host string ─────────────────── */}}
{{- $hosts := $proxy.hosts | default (list ($proxy.host | default "")) }}
{{/* ── Build annotation dict ───────────────────────────────────────────────── */}}
{{- $ann := dict }}
{{/* Step 1: global default annotations (lowest priority) */}}
{{- range $k, $v := ($d.annotations | default dict) }}
{{- $_ := set $ann $k ($v | toString) }}
{{- end }}
{{/* Step 2: generated feature annotations */}}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-connect-timeout" ($connectTO | toString) }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-read-timeout" ($readTO | toString) }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-send-timeout" ($sendTO | toString) }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-body-size" $bodySize }}
{{/* WebSocket: enable HTTP/1.1 keep-alive required for Upgrade handshake */}}
{{- if $websocket }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/proxy-http-version" "1.1" }}
{{- end }}
{{/* Basic auth */}}
{{- if $authEnabled }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-type" "basic" }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-secret" $authSecret }}
{{- $_ := set $ann "nginx.ingress.kubernetes.io/auth-realm" "Authentication Required" }}
{{- end }}
{{/* cert-manager: add ClusterIssuer/Issuer annotation */}}
{{- if $cmEnabled }}
{{- $cmAnnotationKey := printf "cert-manager.io/%s" ($cmKind | lower) }}
{{- $_ := set $ann $cmAnnotationKey $cmIssuer }}
{{- end }}
{{/* Step 3: per-proxy custom annotations override everything above */}}
{{- range $k, $v := ($proxy.annotations | default dict) }}
{{- $_ := set $ann $k ($v | toString) }}
{{- end }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $proxyName }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "ext-proxy.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $proxyName }}
annotations:
{{- toYaml $ann | nindent 4 }}
spec:
ingressClassName: {{ $ingressClass }}
{{- if $tlsEnabled }}
tls:
- hosts:
{{- range $hosts }}
- {{ . | quote }}
{{- end }}
{{- if $tlsSecret }}
secretName: {{ $tlsSecret | quote }}
{{- end }}
{{- end }}
rules:
{{- range $hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $path | quote }}
pathType: {{ $pathType }}
backend:
service:
name: {{ $proxyName }}
port:
number: {{ $proxy.port }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,40 @@
{{/*
Creates a basic-auth Secret for each proxy that has:
auth.enabled: true
auth.credentials: "<htpasswd string>" (and no auth.secretName — use existing instead)
The Secret key MUST be "auth" for nginx's auth-file type (default).
Reference: nginx.ingress.kubernetes.io/auth-secret-type: auth-file
Generate credentials with:
htpasswd -nb admin 'mypassword'
# outputs: admin:$apr1$...
*/}}
{{- range .Values.proxies }}
{{- $proxy := . }}
{{- $d := $.Values.defaults }}
{{- $proxyName := include "ext-proxy.resourceName" $proxy.name }}
{{- $proxyAuth := $proxy.auth | default dict }}
{{- $defAuth := $d.auth | default dict }}
{{- $authEnabled := $proxyAuth.enabled | default $defAuth.enabled | default false }}
{{- $existingSec := $proxyAuth.secretName | default $defAuth.secretName | default "" }}
{{- $credentials := $proxyAuth.credentials | default $defAuth.credentials | default "" }}
{{/* Only create a Secret when auth is on, no existing secret is referenced, and credentials are provided */}}
{{- if and $authEnabled (not $existingSec) $credentials }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $proxyName }}-auth
namespace: {{ $.Release.Namespace }}
labels:
{{- include "ext-proxy.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $proxyName }}
type: Opaque
data:
# nginx auth-file expects the key to be named "auth"
auth: {{ $credentials | b64enc | quote }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,27 @@
{{/*
Creates one ClusterIP Service per proxy entry.
No selector is set — traffic routing is handled by the paired Endpoints object.
The Service name MUST match the Endpoints name for K8s to associate them.
*/}}
{{- range .Values.proxies }}
{{- $proxy := . }}
{{- $proxyName := include "ext-proxy.resourceName" $proxy.name }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $proxyName }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "ext-proxy.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $proxyName }}
spec:
# ClusterIP with no selector: Kubernetes will not auto-manage endpoints.
# The companion Endpoints object (same name) provides the external addresses.
type: ClusterIP
ports:
- name: http
port: {{ $proxy.port }}
targetPort: {{ $proxy.port }}
protocol: TCP
{{- end }}

View File

@@ -0,0 +1,98 @@
# ─── Global defaults applied to all proxies unless overridden per-proxy ────────
defaults:
# ingress-nginx class name
ingressClass: nginx
# ── TLS ───────────────────────────────────────────────────────────────────────
tls:
enabled: false
# Name of an existing TLS Secret (e.g. wildcard cert managed by cert-manager)
secretName: ""
# cert-manager ClusterIssuer / Issuer integration
certManager:
enabled: false
issuer: "" # ClusterIssuer name (e.g. letsencrypt-prod)
issuerKind: ClusterIssuer # ClusterIssuer | Issuer
# ── Basic Auth (nginx auth_basic) ──────────────────────────────────────────
auth:
enabled: false
# Pre-generated htpasswd string. Generate with:
# htpasswd -nb admin 'mypassword'
credentials: ""
# OR reference an existing Secret (must contain key "auth" with htpasswd data)
secretName: ""
# Enable WebSocket upgrade headers (proxy-http-version 1.1)
websocket: true
# Default path and pathType for Ingress rules
path: /
pathType: Prefix
# Proxy timeout settings (seconds)
proxyConnectTimeout: 60
proxyReadTimeout: 3600
proxySendTimeout: 3600
# Max request body size (0 = unlimited, e.g. "10m", "1g")
proxyBodySize: "1g"
# Additional annotations added to every Ingress (per-proxy annotations override these)
annotations: {}
# ─── External service definitions ───────────────────────────────────────────────
# Each entry creates: Service + Endpoints + Ingress (+ optional auth Secret)
proxies:
- name: plex
# One or more hostnames served by this Ingress rule
hosts:
- plex.home.ru
# External IP(s) — multiple IPs get round-robin load balancing via Endpoints
ips:
- 192.168.1.50
# External service port
port: 32400
# Per-proxy overrides — any defaults.* key can be set here
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
- name: router
hosts:
- router.home.ru
ips:
- 192.168.1.1
port: 8080
websocket: false
- name: grafana
hosts:
- grafana.home.local
ips:
- 192.168.1.60
port: 3000
websocket: true
# Full example with all options:
# - name: myservice
# hosts:
# - myservice.home.ru
# - myservice.internal
# ips:
# - 192.168.1.100
# - 192.168.1.101 # failover / round-robin
# port: 8080
# path: /myservice
# pathType: Prefix
# websocket: true
# tls:
# enabled: true
# secretName: wildcard-cert
# certManager:
# enabled: false
# auth:
# enabled: true
# credentials: "admin:$apr1$xyz..." # htpasswd -nb admin password
# annotations:
# nginx.ingress.kubernetes.io/proxy-body-size: "0"
# nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"

View File

@@ -0,0 +1,68 @@
---
# ─── Helm release ─────────────────────────────────────────────────────────────
ext_proxy_namespace: "ext-proxy"
ext_proxy_release_name: "ext-proxy"
# ─── Global defaults (mirror of chart values.defaults) ────────────────────────
ext_proxy_defaults:
ingressClass: nginx
tls:
enabled: false
secretName: ""
certManager:
enabled: false
issuer: ""
issuerKind: ClusterIssuer
auth:
enabled: false
credentials: "" # htpasswd -nb user pass
secretName: ""
websocket: true
path: /
pathType: Prefix
proxyConnectTimeout: 60
proxyReadTimeout: 3600
proxySendTimeout: 3600
proxyBodySize: "1g"
annotations: {}
# ─── Proxy definitions ────────────────────────────────────────────────────────
# Each entry creates: Service + Endpoints + Ingress (+ optional auth Secret)
# All fields support per-entry overrides of ext_proxy_defaults.
#
# Minimal example:
# ext_proxy_proxies:
# - name: plex
# hosts: [plex.home.ru]
# ips: [192.168.1.50]
# port: 32400
#
# Full example:
# ext_proxy_proxies:
# - name: myapp
# hosts:
# - myapp.home.ru
# - myapp.lan
# ips:
# - 192.168.1.100
# - 192.168.1.101 # failover / round-robin
# port: 8080
# path: /myapp
# pathType: Prefix
# websocket: true
# tls:
# enabled: true
# secretName: wildcard-cert
# certManager:
# enabled: true
# issuer: letsencrypt-prod
# issuerKind: ClusterIssuer
# auth:
# enabled: true
# credentials: "admin:$apr1$..." # htpasswd -nb admin password
# annotations:
# nginx.ingress.kubernetes.io/proxy-body-size: "0"
ext_proxy_proxies: []
# kube-vip VIP — shown in post-install summary (informational only)
ext_proxy_vip: ""

View File

@@ -0,0 +1,128 @@
---
# ── Validate inputs ───────────────────────────────────────────────────────────
- name: Validate ext_proxy_proxies is defined and non-empty
ansible.builtin.assert:
that:
- ext_proxy_proxies is defined
- ext_proxy_proxies | length > 0
fail_msg: >
ext_proxy_proxies is empty. Define at least one proxy in
group_vars/all/addons.yml → ext_proxy_proxies.
success_msg: "ext_proxy_proxies: {{ ext_proxy_proxies | length }} service(s) defined"
# ── Create namespace ──────────────────────────────────────────────────────────
- name: Create ext-proxy namespace
ansible.builtin.command: >
k3s kubectl create namespace {{ ext_proxy_namespace }}
--dry-run=client -o yaml | k3s kubectl apply -f -
become: true
changed_when: false
# ── Copy Helm chart to master node ───────────────────────────────────────────
- name: Ensure chart temp directory is clean
ansible.builtin.file:
path: /tmp/ext-proxy-chart
state: absent
become: true
- name: Create chart temp directory
ansible.builtin.file:
path: /tmp/ext-proxy-chart
state: directory
mode: "0755"
become: true
- name: Copy Helm chart to master
ansible.builtin.copy:
src: "{{ role_path }}/chart/"
dest: /tmp/ext-proxy-chart/
mode: preserve
become: true
# ── Template Helm values ──────────────────────────────────────────────────────
- name: Template Helm values
ansible.builtin.template:
src: values.yaml.j2
dest: /tmp/ext-proxy-values.yaml
mode: "0640"
become: true
- name: Show generated Helm values
ansible.builtin.command: cat /tmp/ext-proxy-values.yaml
become: true
changed_when: false
register: _ext_proxy_values
- name: Debug generated values
ansible.builtin.debug:
var: _ext_proxy_values.stdout_lines
# ── Lint chart before deploying ───────────────────────────────────────────────
- name: Lint Helm chart
ansible.builtin.command: >
helm lint /tmp/ext-proxy-chart
--values /tmp/ext-proxy-values.yaml
become: true
changed_when: false
register: _helm_lint
failed_when: _helm_lint.rc != 0
# ── Deploy chart ──────────────────────────────────────────────────────────────
- name: Deploy ext-proxy via Helm
ansible.builtin.command: >
helm upgrade --install {{ ext_proxy_release_name }}
/tmp/ext-proxy-chart
--namespace {{ ext_proxy_namespace }}
--values /tmp/ext-proxy-values.yaml
--atomic
--wait
--timeout 60s
become: true
register: _helm_result
changed_when: true
# ── Verify deployment ─────────────────────────────────────────────────────────
- name: Get Ingress list
ansible.builtin.command: >
k3s kubectl -n {{ ext_proxy_namespace }} get ingress -o wide
become: true
changed_when: false
register: _ingress_list
- name: Get Endpoints list
ansible.builtin.command: >
k3s kubectl -n {{ ext_proxy_namespace }} get endpoints
become: true
changed_when: false
register: _endpoints_list
# ── Summary ───────────────────────────────────────────────────────────────────
- name: "=== External Services Ingress Proxy Ready ==="
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════════════════════════════╗"
- "║ External Services Ingress Proxy — Deployed ║"
- "╚══════════════════════════════════════════════════════════════╝"
- ""
- " Namespace : {{ ext_proxy_namespace }}"
- " Release : {{ ext_proxy_release_name }}"
- " Services : {{ ext_proxy_proxies | length }}"
- ""
- " Ingress resources:"
- "{{ _ingress_list.stdout_lines | to_yaml }}"
- ""
- " Endpoints:"
- "{{ _endpoints_list.stdout_lines | to_yaml }}"
- ""
- " kube-vip VIP: {{ ext_proxy_vip | default('<check kube-vip>') }}"
- " → Point all proxy hostnames to the VIP in DNS/hosts file"
- ""
- " Verify: kubectl -n {{ ext_proxy_namespace }} describe ingress"

View File

@@ -0,0 +1,8 @@
# Generated by Ansible — do not edit manually.
# Configure via: group_vars/all/addons.yml → ext_proxy_* variables.
defaults:
{{ ext_proxy_defaults | to_yaml | indent(2, True) }}
proxies:
{{ ext_proxy_proxies | to_yaml | indent(2, True) }}

View File

@@ -69,7 +69,7 @@ splitgw_ru_extra_keywords:
# ─── Режим деплоя ───────────────────────────────────────────────────────────── # ─── Режим деплоя ─────────────────────────────────────────────────────────────
# host — systemd сервис прямо на хосте (рекомендуется) # host — systemd сервис прямо на хосте (рекомендуется)
# k8s — DaemonSet в K3S кластере # k8s — DaemonSet в K3S кластере
splitgw_deploy_mode: "host" splitgw_deploy_mode: "k8s"
# ─── K8s режим (splitgw_deploy_mode: k8s) ──────────────────────────────────── # ─── K8s режим (splitgw_deploy_mode: k8s) ────────────────────────────────────
splitgw_k8s_namespace: "splitgw" splitgw_k8s_namespace: "splitgw"

View File

@@ -67,6 +67,7 @@ make addon-netbird
| mediaserver | `addon_mediaserver` | Plex, Sonarr, Radarr, Lidarr, Bazarr, Prowlarr + Hysteria2 sidecar, Overseerr, Transmission, Samba | [](../addons/mediaserver/README.md) | | mediaserver | `addon_mediaserver` | Plex, Sonarr, Radarr, Lidarr, Bazarr, Prowlarr + Hysteria2 sidecar, Overseerr, Transmission, Samba | [](../addons/mediaserver/README.md) |
| **Сеть / VPN** | | | | | **Сеть / VPN** | | | |
| splitgw | `addon_splitgw` | Прозрачный split-tunnel gateway: sing-box + Hysteria2 TPROXY, YouTube→прокси, RU→прямой | [](../addons/splitgw/README.md) | | splitgw | `addon_splitgw` | Прозрачный split-tunnel gateway: sing-box + Hysteria2 TPROXY, YouTube→прокси, RU→прямой | [](../addons/splitgw/README.md) |
| ext-proxy | `addon_ext_proxy` | Проксировать внешние сервисы (IP:PORT) через ingress-nginx по домену — Service + Endpoints + Ingress | [](../addons/ext-proxy/README.md) |
## Конфигурация addons.yml ## Конфигурация addons.yml
@@ -116,6 +117,9 @@ addon_mediaserver: false # Plex + *arr + Transmission + Prowlarr/Hyste
# ── Split Gateway ───────────────────────────────────────────────────────────── # ── Split Gateway ─────────────────────────────────────────────────────────────
addon_splitgw: false # sing-box + Hysteria2 TPROXY (host или k8s DaemonSet) addon_splitgw: false # sing-box + Hysteria2 TPROXY (host или k8s DaemonSet)
# ── External Services Ingress Proxy ───────────────────────────────────────────
addon_ext_proxy: false # проксировать внешние сервисы через ingress-nginx
``` ```
## Зависимости между аддонами ## Зависимости между аддонами
@@ -136,6 +140,7 @@ addon_splitgw: false # sing-box + Hysteria2 TPROXY (host или k8
| `crowdsec` | `ingress-nginx` | Bouncer интеграция при addon_crowdsec | | `crowdsec` | `ingress-nginx` | Bouncer интеграция при addon_crowdsec |
| `mediaserver` | `csi-nfs` (рекомендуется) | Shared PVC требует RWX StorageClass | | `mediaserver` | `csi-nfs` (рекомендуется) | Shared PVC требует RWX StorageClass |
| `splitgw` | Hysteria2 сервер (vault_hysteria2_url) | URL из Shadowrocket / NekoBox | | `splitgw` | Hysteria2 сервер (vault_hysteria2_url) | URL из Shadowrocket / NekoBox |
| `ext-proxy` | `ingress-nginx` | Требует работающий Ingress controller |
## MediaServer ## MediaServer
@@ -176,6 +181,36 @@ Samba получает IP от kube-vip (`LoadBalancer`) — подключен
Подробнее: [addons/mediaserver/README.md](../addons/mediaserver/README.md) Подробнее: [addons/mediaserver/README.md](../addons/mediaserver/README.md)
## External Services Ingress Proxy
Проксирует внешние сервисы (вне кластера) через ingress-nginx по доменному имени. Для каждого сервиса автоматически создаёт `Service (ClusterIP, no selector)` + `Endpoints` + `Ingress`. Поддерживает TLS, basic auth, WebSocket, несколько хостов и несколько backend IP.
```bash
make addon-ext-proxy
```
Конфигурация в `group_vars/all/addons.yml`:
```yaml
ext_proxy_proxies:
- name: plex
hosts: [plex.home.ru]
ips: [192.168.1.50]
port: 32400
- name: grafana
hosts: [grafana.home.ru]
ips: [192.168.1.60]
port: 3000
tls:
enabled: true
secretName: wildcard-cert
```
Подробнее: [addons/ext-proxy/README.md](../addons/ext-proxy/README.md)
---
## Split Gateway ## Split Gateway
Прозрачный split-tunnel proxy на базе sing-box с Hysteria2 как outbound. Перехватывает трафик с TV/устройств через TPROXY и маршрутизирует по правилам: YouTube → Hysteria2, RU-сервисы и частные сети → прямой маршрут. Прозрачный split-tunnel proxy на базе sing-box с Hysteria2 как outbound. Перехватывает трафик с TV/устройств через TPROXY и маршрутизирует по правилам: YouTube → Hysteria2, RU-сервисы и частные сети → прямой маршрут.

View File

@@ -41,6 +41,7 @@ addon_netbird: false # NetBird VPN (управляющий сер
addon_mediaserver: false # MediaServer — Plex, *arr, Transmission, Prowlarr+Hysteria2, Samba addon_mediaserver: false # MediaServer — Plex, *arr, Transmission, Prowlarr+Hysteria2, Samba
addon_hysteria2_server: false # Hysteria2 VPN сервер на удалённый VPS (группа [hysteria2_server] в inventory) addon_hysteria2_server: false # Hysteria2 VPN сервер на удалённый VPS (группа [hysteria2_server] в inventory)
addon_splitgw: false # Split Gateway — прозрачный прокси sing-box+Hysteria2 (группа [splitgw] в inventory) addon_splitgw: false # Split Gateway — прозрачный прокси sing-box+Hysteria2 (группа [splitgw] в inventory)
addon_ext_proxy: false # External Services Ingress Proxy — проксировать внешние сервисы через ingress-nginx
# ─── NFS Server ─────────────────────────────────────────────────────────────── # ─── NFS Server ───────────────────────────────────────────────────────────────
nfs_exports: nfs_exports:

View File

@@ -295,3 +295,11 @@
when: addon_splitgw | default(false) | bool when: addon_splitgw | default(false) | bool
roles: roles:
- role: "{{ playbook_dir }}/../addons/splitgw/role" - role: "{{ playbook_dir }}/../addons/splitgw/role"
- name: Install External Services Ingress Proxy
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_ext_proxy | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/ext-proxy/role"