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:
6
Makefile
6
Makefile
@@ -58,7 +58,7 @@ DOCKER_RUN := docker run --rm -it \
|
||||
addon-harbor addon-gitea addon-owncloud addon-nextcloud \
|
||||
addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \
|
||||
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-etcd-node remove-etcd-node \
|
||||
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"
|
||||
$(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
|
||||
addon-%: _check_env _check_image
|
||||
@if [ ! -f "addons/$*/playbook.yml" ]; then \
|
||||
|
||||
@@ -38,7 +38,7 @@ HA-режим (embedded etcd): при отказе **любой одной** н
|
||||
|
||||
**CNI:** `flannel` (встроен) | `calico` (Network Policy, BGP) | `cilium` (eBPF, Hubble)
|
||||
|
||||
## Аддоны (36)
|
||||
## Аддоны (37)
|
||||
|
||||
| Категория | Аддоны |
|
||||
|---|---|
|
||||
@@ -52,6 +52,7 @@ HA-режим (embedded etcd): при отказе **любой одной** н
|
||||
| **Файловые хранилища** | nextcloud, owncloud |
|
||||
| **Медиасервер** | mediaserver — Plex, Sonarr, Radarr, Lidarr, Bazarr, Prowlarr + Hysteria2, Overseerr, Transmission, Samba |
|
||||
| **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>`.
|
||||
|
||||
|
||||
702
addons/ext-proxy/README.md
Normal file
702
addons/ext-proxy/README.md
Normal 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 — отображается в сводке после установки |
|
||||
7
addons/ext-proxy/playbook.yml
Normal file
7
addons/ext-proxy/playbook.yml
Normal 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"
|
||||
19
addons/ext-proxy/role/chart/Chart.yaml
Normal file
19
addons/ext-proxy/role/chart/Chart.yaml
Normal 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
|
||||
42
addons/ext-proxy/role/chart/templates/NOTES.txt
Normal file
42
addons/ext-proxy/role/chart/templates/NOTES.txt
Normal 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>
|
||||
51
addons/ext-proxy/role/chart/templates/_helpers.tpl
Normal file
51
addons/ext-proxy/role/chart/templates/_helpers.tpl
Normal 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 }}
|
||||
35
addons/ext-proxy/role/chart/templates/endpoints.yaml
Normal file
35
addons/ext-proxy/role/chart/templates/endpoints.yaml
Normal 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 }}
|
||||
131
addons/ext-proxy/role/chart/templates/ingress.yaml
Normal file
131
addons/ext-proxy/role/chart/templates/ingress.yaml
Normal 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 }}
|
||||
40
addons/ext-proxy/role/chart/templates/secret-auth.yaml
Normal file
40
addons/ext-proxy/role/chart/templates/secret-auth.yaml
Normal 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 }}
|
||||
27
addons/ext-proxy/role/chart/templates/service.yaml
Normal file
27
addons/ext-proxy/role/chart/templates/service.yaml
Normal 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 }}
|
||||
98
addons/ext-proxy/role/chart/values.yaml
Normal file
98
addons/ext-proxy/role/chart/values.yaml
Normal 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"
|
||||
68
addons/ext-proxy/role/defaults/main.yml
Normal file
68
addons/ext-proxy/role/defaults/main.yml
Normal 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: ""
|
||||
128
addons/ext-proxy/role/tasks/main.yml
Normal file
128
addons/ext-proxy/role/tasks/main.yml
Normal 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"
|
||||
8
addons/ext-proxy/role/templates/values.yaml.j2
Normal file
8
addons/ext-proxy/role/templates/values.yaml.j2
Normal 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) }}
|
||||
@@ -69,7 +69,7 @@ splitgw_ru_extra_keywords:
|
||||
# ─── Режим деплоя ─────────────────────────────────────────────────────────────
|
||||
# host — systemd сервис прямо на хосте (рекомендуется)
|
||||
# k8s — DaemonSet в K3S кластере
|
||||
splitgw_deploy_mode: "host"
|
||||
splitgw_deploy_mode: "k8s"
|
||||
|
||||
# ─── K8s режим (splitgw_deploy_mode: k8s) ────────────────────────────────────
|
||||
splitgw_k8s_namespace: "splitgw"
|
||||
|
||||
@@ -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) |
|
||||
| **Сеть / VPN** | | | |
|
||||
| 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
|
||||
|
||||
@@ -116,6 +117,9 @@ addon_mediaserver: false # Plex + *arr + Transmission + Prowlarr/Hyste
|
||||
|
||||
# ── Split Gateway ─────────────────────────────────────────────────────────────
|
||||
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 |
|
||||
| `mediaserver` | `csi-nfs` (рекомендуется) | Shared PVC требует RWX StorageClass |
|
||||
| `splitgw` | Hysteria2 сервер (vault_hysteria2_url) | URL из Shadowrocket / NekoBox |
|
||||
| `ext-proxy` | `ingress-nginx` | Требует работающий Ingress controller |
|
||||
|
||||
## MediaServer
|
||||
|
||||
@@ -176,6 +181,36 @@ Samba получает IP от kube-vip (`LoadBalancer`) — подключен
|
||||
|
||||
Подробнее: [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-tunnel proxy на базе sing-box с Hysteria2 как outbound. Перехватывает трафик с TV/устройств через TPROXY и маршрутизирует по правилам: YouTube → Hysteria2, RU-сервисы и частные сети → прямой маршрут.
|
||||
|
||||
@@ -41,6 +41,7 @@ addon_netbird: false # NetBird VPN (управляющий сер
|
||||
addon_mediaserver: false # MediaServer — Plex, *arr, Transmission, Prowlarr+Hysteria2, Samba
|
||||
addon_hysteria2_server: false # Hysteria2 VPN сервер на удалённый VPS (группа [hysteria2_server] в inventory)
|
||||
addon_splitgw: false # Split Gateway — прозрачный прокси sing-box+Hysteria2 (группа [splitgw] в inventory)
|
||||
addon_ext_proxy: false # External Services Ingress Proxy — проксировать внешние сервисы через ingress-nginx
|
||||
|
||||
# ─── NFS Server ───────────────────────────────────────────────────────────────
|
||||
nfs_exports:
|
||||
|
||||
@@ -295,3 +295,11 @@
|
||||
when: addon_splitgw | default(false) | bool
|
||||
roles:
|
||||
- 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"
|
||||
|
||||
Reference in New Issue
Block a user