Files
K3S/addons/ext-proxy/README.md
Sergey Antropoff aae7941416 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
2026-04-26 07:21:41 +03:00

23 KiB
Raw Blame History

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. Включи аддон и определи сервисы:

# 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. Разверни:

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.

Глобальные значения по умолчанию

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

Поля определения прокси

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:

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 для конкретного прокси:

ext_proxy_proxies:
  - name: router
    hosts: [router.home.ru]
    ips: [192.168.1.1]
    port: 8080
    tls:
      enabled: false    # отключить TLS только для этого прокси

TLS — автоматический выпуск через cert-manager

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):

htpasswd -nb admin 'мойсекретныйпароль'
# выводит: admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/
ext_proxy_defaults:
  auth:
    enabled: true
    credentials: "admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/"

Или выборочно по конкретным прокси:

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):

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. Отключи для конкретного прокси если не нужно:

- name: router
  ...
  websocket: false

Несколько хостов для одного сервиса

- name: plex
  hosts:
    - plex.home.ru
    - plex.internal
    - plex.lan
  ips: [192.168.1.50]
  port: 32400

Для каждого хоста создаётся отдельное правило Ingress, все ссылаются на один и тот же backend.

Несколько backend IP (round-robin / резервирование)

- name: homeassistant
  hosts: [ha.home.ru]
  ips:
    - 192.168.1.100   # основной
    - 192.168.1.101   # резервный
  port: 8123

K8s создаёт два адреса в объекте Endpoints. kube-proxy распределяет трафик между ними по round-robin. Для активно-пассивного резервирования необходим внешний механизм проверки работоспособности.

Маршрутизация по пути

- 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

- 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:
ext_proxy_proxies:
  # ... существующие записи ...
  - name: homeassistant
    hosts: [ha.home.ru]
    ips: [192.168.1.150]
    port: 8123
    websocket: true
  1. Запусти аддон повторно:
make addon-ext-proxy

Helm upgrade идемпотентен — существующие ресурсы обновляются, новые добавляются.

  1. Добавь 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:

# configmap coredns — добавить в Corefile
rewrite name plex.home.ru ingress-nginx.ingress-nginx.svc.cluster.local

Узнать VIP kube-vip

# EXTERNAL-IP сервиса ingress-nginx LoadBalancer — это и есть VIP:
kubectl -n ingress-nginx get svc ingress-nginx-controller

Проверка

1. Убедись что ресурсы созданы

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 заполнены

kubectl -n ext-proxy describe endpoints plex
# Должно показать "Addresses: 192.168.1.50" и "Ports: http 32400/TCP"

3. Проверь соединение изнутри кластера

kubectl run curl --rm -it --image=curlimages/curl -- \
  curl -v http://plex.ext-proxy.svc.cluster.local:32400

4. Проверь внешний доступ

# Замени IP на kube-vip VIP
curl -H "Host: plex.home.ru" http://192.168.1.100/
# Через DNS:
curl http://plex.home.ru/

5. Убедись что ingress-nginx применил правила

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

Пример сгенерированных манифестов

При такой конфигурации:

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:

apiVersion: v1
kind: Service
metadata:
  name: plex
  namespace: ext-proxy
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 32400
      targetPort: 32400
      protocol: TCP

Endpoints:

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:

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/. Развернуть напрямую:

# Из корня проекта:
helm upgrade --install ext-proxy addons/ext-proxy/role/chart \
  --namespace ext-proxy \
  --create-namespace \
  --values my-values.yaml

Сгенерировать манифесты без деплоя (для проверки):

helm template ext-proxy addons/ext-proxy/role/chart \
  --values my-values.yaml

Устранение неисправностей

502 Bad Gateway

Причина: ingress-nginx достигает Service, но Service не может достучаться до внешнего IP.

# 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.

# Проверь наличие 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 не совпало — ошибка в имени хоста или пути.

# 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 не резолвится

# Узнай куда резолвится хост:
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

# Проверь наличие 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

# Проверь с учётными данными:
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-соединения

# Проверь аннотацию 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

# Смотреть логи 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

Удаление

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

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 — отображается в сводке после установки