# 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_ingress_proxypass: true ingress_proxypass_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-ingress-proxypass # с явным VIP в сводке: make addon-ingress-proxypass ARGS="-e ingress_proxypass_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 ingress_proxypass_namespace: "ingress-proxypass" # Kubernetes namespace ingress_proxypass_release_name: "ingress-proxypass" # имя Helm release ingress_proxypass_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 username: "" # логин (пароль хэшируется автоматически) password: "" # пароль в открытом виде — задавать через vault! credentials: "" # готовая htpasswd-строка (приоритет над username/password) 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 ingress_proxypass_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 username: admin # логин — хэш генерируется автоматически password: "{{ vault_mypass }}" # пароль из vault (рекомендуется) # ИЛИ готовая строка: credentials: "admin:$apr1$..." # ИЛИ существующий Secret: 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 ingress_proxypass_defaults: tls: enabled: true secretName: wildcard-tls # должен существовать в ingress_proxypass_namespace ingress_proxypass_proxies: - name: plex hosts: [plex.home.ru] ips: [192.168.1.50] port: 32400 ``` Переопределить TLS для конкретного прокси: ```yaml ingress_proxypass_proxies: - name: router hosts: [router.home.ru] ips: [192.168.1.1] port: 8080 tls: enabled: false # отключить TLS только для этого прокси ``` ### TLS — автоматический выпуск через cert-manager ```yaml ingress_proxypass_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 Просто укажи логин и пароль — хэш генерируется автоматически: ```yaml # group_vars/all/addons.yml ingress_proxypass_defaults: auth: enabled: true username: admin password: "{{ vault_ingress_proxypass_password }}" # пароль из vault ``` ```yaml # group_vars/all/vault.yml vault_ingress_proxypass_password: "мойсекретныйпароль" ``` Ansible автоматически вызовет `openssl passwd -apr1` и запишет хэш в Kubernetes Secret. Пароль в открытом виде **не попадает** в Helm values, логи или конфиги. Выборочно для конкретного прокси (остальные без auth): ```yaml ingress_proxypass_proxies: - name: router hosts: [router.home.ru] ips: [192.168.1.1] port: 8080 auth: enabled: true username: admin password: "{{ vault_router_password }}" - name: plex # без auth hosts: [plex.home.ru] ips: [192.168.1.50] port: 32400 ``` Разные пароли для разных прокси — каждый прокси создаёт свой отдельный Secret. **Если уже есть готовая htpasswd-строка** (поле `credentials` имеет приоритет над `username`/`password`): ```yaml auth: enabled: true credentials: "admin:$apr1$Rh0Ycxl9$rPTH7gRHfMBkS.7.Q1BxM/" # Сгенерировать вручную: htpasswd -nb admin 'пароль' ``` **Использование существующего Secret** (ключ должен называться `auth`, тип `Opaque`): ```yaml auth: enabled: true secretName: my-shared-htpasswd # создать вручную через kubectl ``` **Приоритет:** `secretName` > `credentials` > `username` + `password` > **Примечание:** `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. Добавь запись в `ingress_proxypass_proxies` в файле `group_vars/all/addons.yml`: ```yaml ingress_proxypass_proxies: # ... существующие записи ... - name: homeassistant hosts: [ha.home.ru] ips: [192.168.1.150] port: 8123 websocket: true ``` 2. Запусти аддон повторно: ```bash make addon-ingress-proxypass ``` 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 ingress-proxypass 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 ingress-proxypass 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.ingress-proxypass.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 ingress-proxypass 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 ingress_proxypass_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: ingress-proxypass spec: type: ClusterIP ports: - name: http port: 32400 targetPort: 32400 protocol: TCP ``` **Endpoints:** ```yaml apiVersion: v1 kind: Endpoints metadata: name: plex namespace: ingress-proxypass 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: ingress-proxypass 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/ingress-proxypass/role/chart/`. Развернуть напрямую: ```bash # Из корня проекта: helm upgrade --install ingress-proxypass addons/ingress-proxypass/role/chart \ --namespace ingress-proxypass \ --create-namespace \ --values my-values.yaml ``` Сгенерировать манифесты без деплоя (для проверки): ```bash helm template ingress-proxypass addons/ingress-proxypass/role/chart \ --values my-values.yaml ``` --- ## Устранение неисправностей ### 502 Bad Gateway **Причина:** ingress-nginx достигает Service, но Service не может достучаться до внешнего IP. ```bash # 1. Проверь Endpoints заполнены: kubectl -n ingress-proxypass 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 ingress-proxypass describe endpoints plex # Убедись что у service есть ClusterIP: kubectl -n ingress-proxypass 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 ingress-proxypass describe ingress plex # Раздел "Rules" — хост и путь должны совпадать точно. # 3. Проверь класс Ingress: kubectl -n ingress-proxypass 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 ingress-proxypass get secret wildcard-tls # Проверь что cert-manager выпустил сертификат: kubectl -n ingress-proxypass get certificate kubectl -n ingress-proxypass 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 ingress-proxypass get secret plex-auth -o jsonpath='{.data.auth}' | base64 -d # Должно вывести: admin:$apr1$... # Проверь аннотации в Ingress: kubectl -n ingress-proxypass get ingress plex -o yaml | grep auth ``` ### Обрывы WebSocket-соединения ```bash # Проверь аннотацию proxy-http-version: kubectl -n ingress-proxypass 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 ingress-proxypass uninstall ingress-proxypass kubectl delete namespace ingress-proxypass ``` Удалить только один прокси без полного сноса релиза: 1. Убери запись из `ingress_proxypass_proxies` в `group_vars/all/addons.yml` 2. Запусти `make addon-ingress-proxypass` — Helm upgrade удалит убранные ресурсы --- ## Интеграция с проектом | Зависимость | Примечание | |---|---| | `ingress-nginx` (`addon_ingress_nginx: true`) | Должен быть установлен первым | | `kube-vip` | Предоставляет VIP для LoadBalancer сервиса ingress-nginx | | `cert-manager` (опционально) | Нужен только при `tls.certManager.enabled: true` | ### Команды Makefile ```bash make addon-ingress-proxypass # развернуть / обновить make addon-ingress-proxypass ARGS="-e ingress_proxypass_vip=..." # с явным VIP в сводке ``` ### Переменные Ansible | Переменная | По умолчанию | Описание | |---|---|---| | `ingress_proxypass_namespace` | `ingress-proxypass` | Kubernetes namespace | | `ingress_proxypass_release_name` | `ingress-proxypass` | Имя Helm release | | `ingress_proxypass_proxies` | `[]` | Список определений внешних сервисов | | `ingress_proxypass_defaults.*` | см. defaults | Глобальные значения по умолчанию | | `ingress_proxypass_vip` | `""` | kube-vip VIP — отображается в сводке после установки | ## Официальные ресурсы - Официальный сайт: [https://kubernetes.io/docs/concepts/services-networking/ingress/](https://kubernetes.io/docs/concepts/services-networking/ingress/) - Официальная документация: [https://kubernetes.io/docs/concepts/services-networking/ingress/](https://kubernetes.io/docs/concepts/services-networking/ingress/) - Версии Helm chart / ПО: [https://kubernetes.github.io/ingress-nginx/deploy/](https://kubernetes.github.io/ingress-nginx/deploy/)