diff --git a/Makefile b/Makefile index 7745f62..8b83ea4 100644 --- a/Makefile +++ b/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//playbook.yml addon-%: _check_env _check_image @if [ ! -f "addons/$*/playbook.yml" ]; then \ diff --git a/README.md b/README.md index 2c88cd6..729b7d5 100644 --- a/README.md +++ b/README.md @@ -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-`. diff --git a/addons/ext-proxy/README.md b/addons/ext-proxy/README.md new file mode 100644 index 0000000..5fa812a --- /dev/null +++ b/addons/ext-proxy/README.md @@ -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" не должен быть пустым. Если "" — 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:/// +``` + +### Проблемы с 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 — отображается в сводке после установки | diff --git a/addons/ext-proxy/playbook.yml b/addons/ext-proxy/playbook.yml new file mode 100644 index 0000000..0873250 --- /dev/null +++ b/addons/ext-proxy/playbook.yml @@ -0,0 +1,7 @@ +--- +- name: Install External Services Ingress Proxy + hosts: k3s_master[0] + gather_facts: false + become: true + roles: + - role: "{{ playbook_dir }}/role" diff --git a/addons/ext-proxy/role/chart/Chart.yaml b/addons/ext-proxy/role/chart/Chart.yaml new file mode 100644 index 0000000..53a939c --- /dev/null +++ b/addons/ext-proxy/role/chart/Chart.yaml @@ -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 diff --git a/addons/ext-proxy/role/chart/templates/NOTES.txt b/addons/ext-proxy/role/chart/templates/NOTES.txt new file mode 100644 index 0000000..69a6040 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/NOTES.txt @@ -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 + +─── DNS ─────────────────────────────────────────────────────── +Point all proxy hostnames to your kube-vip VIP: + + IN A + +─── 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://.: diff --git a/addons/ext-proxy/role/chart/templates/_helpers.tpl b/addons/ext-proxy/role/chart/templates/_helpers.tpl new file mode 100644 index 0000000..bc6d234 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/_helpers.tpl @@ -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 }} diff --git a/addons/ext-proxy/role/chart/templates/endpoints.yaml b/addons/ext-proxy/role/chart/templates/endpoints.yaml new file mode 100644 index 0000000..6440b39 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/endpoints.yaml @@ -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 }} diff --git a/addons/ext-proxy/role/chart/templates/ingress.yaml b/addons/ext-proxy/role/chart/templates/ingress.yaml new file mode 100644 index 0000000..f103ca4 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/ingress.yaml @@ -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) "" }} +{{- $websocket = $proxy.websocket }} +{{- else if ne ($d.websocket | toString) "" }} +{{- $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 }} diff --git a/addons/ext-proxy/role/chart/templates/secret-auth.yaml b/addons/ext-proxy/role/chart/templates/secret-auth.yaml new file mode 100644 index 0000000..c92d5c5 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/secret-auth.yaml @@ -0,0 +1,40 @@ +{{/* +Creates a basic-auth Secret for each proxy that has: + auth.enabled: true + auth.credentials: "" (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 }} diff --git a/addons/ext-proxy/role/chart/templates/service.yaml b/addons/ext-proxy/role/chart/templates/service.yaml new file mode 100644 index 0000000..46c2547 --- /dev/null +++ b/addons/ext-proxy/role/chart/templates/service.yaml @@ -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 }} diff --git a/addons/ext-proxy/role/chart/values.yaml b/addons/ext-proxy/role/chart/values.yaml new file mode 100644 index 0000000..01ecdba --- /dev/null +++ b/addons/ext-proxy/role/chart/values.yaml @@ -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" diff --git a/addons/ext-proxy/role/defaults/main.yml b/addons/ext-proxy/role/defaults/main.yml new file mode 100644 index 0000000..8107bf8 --- /dev/null +++ b/addons/ext-proxy/role/defaults/main.yml @@ -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: "" diff --git a/addons/ext-proxy/role/tasks/main.yml b/addons/ext-proxy/role/tasks/main.yml new file mode 100644 index 0000000..456c37c --- /dev/null +++ b/addons/ext-proxy/role/tasks/main.yml @@ -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('') }}" + - " → Point all proxy hostnames to the VIP in DNS/hosts file" + - "" + - " Verify: kubectl -n {{ ext_proxy_namespace }} describe ingress" diff --git a/addons/ext-proxy/role/templates/values.yaml.j2 b/addons/ext-proxy/role/templates/values.yaml.j2 new file mode 100644 index 0000000..88ca903 --- /dev/null +++ b/addons/ext-proxy/role/templates/values.yaml.j2 @@ -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) }} diff --git a/addons/splitgw/role/defaults/main.yml b/addons/splitgw/role/defaults/main.yml index abeed95..8e20be4 100644 --- a/addons/splitgw/role/defaults/main.yml +++ b/addons/splitgw/role/defaults/main.yml @@ -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" diff --git a/docs/addons.md b/docs/addons.md index 4d909aa..eacefb6 100644 --- a/docs/addons.md +++ b/docs/addons.md @@ -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-сервисы и частные сети → прямой маршрут. diff --git a/group_vars/all/addons.yml b/group_vars/all/addons.yml index 81d164c..7492403 100644 --- a/group_vars/all/addons.yml +++ b/group_vars/all/addons.yml @@ -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: diff --git a/playbooks/addons.yml b/playbooks/addons.yml index 40af1d6..9bbc25f 100644 --- a/playbooks/addons.yml +++ b/playbooks/addons.yml @@ -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"