# Установка 3X-UI как под в K3s (Kubernetes)

3x-ui — это симпатичный веб-интерфейс для управления VPN-серверами, такими как WireGuard, Shadowsocks, Xray, V2Ray
и тому подобное. Он позволяет настраивать и мониторить VPN-соединения и клиентов через браузер. Мы будем запускать
его как контейнер (под) внутри K3s кластера на Orange Pi 5.

Мне нужен 3x-ui, для безопасного доступа к домашней сети из любой точки мира, а также для безопасного доступа
к интернету через домашний сервер.

### Создание namespace (не обязательно)

Для удобства организации рекомендую создать отдельное пространство имён (`namespace`) для 3x-ui. Пространство имен --
это способ организовать ресурсы в кластере. Оно работает как виртуальная "папка", которая помогает разделять 
(изолировать) и управлять объектами, такими как поды, сервисы, конфигурации и т.д. Объекты в одном _namespace_ не видят
объекты из другого namespace (если не настроено обратное), что помогает избежать путаницы. Несколько приложений
с одинаковыми именами могут без проблем существовать в разных пространствах имен. Кроме того, можно настроить
права доступа (RBAC) отдельно для каждого namespace. 

Выполним в терминале:
```bash
sudo kubectl create namespace x-ui
```

Проверим, что пространство имён создано:
```bash
kubectl get namespaces
```

Увидим x-ui в списке:
```text
NAME              STATUS   AGE
...               ...      ...
...               ...      ...
x-ui              Active   6s
```

## Простое развёртывание 3X-UI в поде

Cоздадим манифест развертывания пода (этого YAML-файл с инструкциями для K3s, что и как запустить). Мы будем
использовать SQLite как внутреннюю базу данных 3x-ui, и пока эта бызы будет храниться внутри пода. Позже сможем
переключиться на `Longhorn` (опционально).

Создадим `deployment.yaml` в каталоге `~/k3s/vpn/x-ui/` (см. [структуру каталогов для хранения конфигураций и манифестов](k3s-shadowsocks-client.md)
принятую в моем проекте):
```bash
mkdir -p ~/k3s/vpn/x-ui
nano ~/k3s/vpn/x-ui/deployment.yaml
```

Вставим в него следующий код:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: x-ui        # имя развертывания (имя пода)
  namespace: x-ui   # пространство имён, в котором будет создан под
spec:
  replicas: 1
  selector:
    matchLabels:
      app: x-ui
  template:
    metadata:
      labels:
        app: x-ui
    spec:
      hostNetwork: true   # использовать сетевой стек хоста
      containers:
      - name: x-ui        # имя контейнера
        image: ghcr.io/mhsanaei/3x-ui:latest
        # image: enwaiax/x-ui:latest    # альтернативный облегчённый: меньше способов шифрования и китайский интерфейс
```
В этом манифесте примечательно следующее:
- `hostNetwork: true` — позволяет контейнеру использовать сетевой стек хоста и значит работать
  с сетевыми интерфейсами и портами хоста напрямую. Это полезно для приложений, которые требуют прямого доступа
  к сети, например, VPN-серверы.
- `spec.replicas: 1` — количество реплик (экземпляров) пода, которые будут запущены. В данном случае -- оин под.
- `spec.selector` —  селектор, который используется для выбора подов, которые будут управляться этим
  развертыванием. Он определяет, какие поды будут обновлены или удалены при изменении конфигурации развертывания.
- `matchLabels` —  метки, которые должны совпадать с метками подов, чтобы они были выбраны селектором.
  В данном случае мы используем метку `app: x-ui`, чтобы выбрать поды, которые относятся к приложению x-ui.


Применим манифест:
```bash
sudo kubectl apply -f ~/k3s/vpn/x-ui/deployment.yaml
```

Проверим, что под запустился:
```bash
sudo k3s kubectl get pods -n x-ui -o wide
```

Увидим что-то вроде:
```text
NAME                   READY   STATUS    RESTARTS   AGE   IP           NODE         NOMINATED NODE   READINESS GATES
x-ui-bb97f6894-h7zj8   1/1     Running   0          11s   10.42.1.50   opi5plus-3   <none>           <none>

Видим, что нода на которой запустился 3x-ui это `opi5plus-3`, а имя пода `x-ui-bb97f6894-h7zj8`. Проверим логи пода,
используя его имя:
```bash
sudo kubectl logs -n x-ui x-ui-bb97f6894-h7zj8
```

Увидим что-то вроде:
```text
Server ready
(0x291e4e8,0x40001657b0)
2025/03/28 13:28:34 Starting x-ui 2.5.6
(0x291e4e8,0x40001658e0)
INFO - Web server running HTTP on [::]:2053
INFO - XRAY: infra/conf/serial: Reading config: &{Name:bin/config.json Format:json}
WARNING - XRAY: core: Xray 25.3.6 started
```

Теперь мы знаем порт, на котором работает 3x-ui (`2053`), и значит можем получить доступ к веб-интерфейсу через браузер
по адресу `http://opi5plus-3:2053` или `http://<IP_адрес_вашего_узла>:2053`.

<img src="../images/k3s--3x-ui-welcome.png" alt="3x-ui welcome page (RU)" width="70%" />

После первого логирования (по умолчанию логин и пароль `admin`/`admin`) можно настаивать VPN-подключения, создавать
пользователей, менять логин и пароль на вход и т.д. Веб-интерфейс 3x-ui интуитивно понятен, так что разбираться
не составит труда.

## Развертывание Kubernetes пода 3x-ui с постоянным хранилищем (PVC)

Есть, конечно, у 3x-ui под k3s минусы. В частности, внутри пода (`sudo kubectl exec -it -n x-ui x-ui-... -- /bin/sh`)
не будет работать командный интерфейс 3x-ui (`x-ui admin`). Поды k3s работают на **Alpine**, а там некоторые команды
отличаются (например, нет `bash`, а только `ash`). Но web-панель работает как положено, и всё управление удобнее
делать через веб-интерфейс, так что лезть в консоль подов не обязательно.

Но есть ещё другой минус, более критичный. При рестарте пода, все настройки будут сброшены, так как они хранятся
во внутреннем хранилище пода, а при остановке пода хранилище удаляется.

Чтобы этого избежать нужно использовать постоянное хранилище (Persistent Volume). Для его работы требуется установить
`Longhorn` (или другой менеджер хранилищ). K3s поддерживает `Longhorn` из коробки, так как в операционной системе на
Orange Pi 5 нет поддержки `iSCSI`, включение его потребует компиляции ядра (если вы этого еще не сделали, [смотрите
инструкцию](../raspberry-and-orange-pi/opi5plus-rebuilding-linux-kernel-for-iscsi.md).

Если `Longhorn` уже установлен, создадим не его базе постоянное хранилище для -- _PersistentVolumeClaim_ (**PVC**).
Манифест PVC создадим в каталоге `~/k3s/vpn/x-ui/`, рядом с `deployment.yaml`:
```bash
nano ~/k3s/vpn/x-ui/pvc-db.yaml
```

Вставим в него следующий код:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: x-ui-db-pvc
  namespace: x-ui
spec:
  storageClassName: longhorn  # Указываем Longhorn как класс хранилища
  accessModes:
    - ReadWriteOnce           # Доступ для чтения и записи одним подом
  resources:
    requests:
      storage: 512Mi         # Запрашиваемое хранилище, размер можно увеличить, если нужно
```

Обратите внимание:
- `metadata.name` и `metadata.namespace` — имя хранилища (и это имя мы должны использовать в манифесте
  развертывания пода, чтобы указать, какое хранилище использовать) и пространство имён, в котором оно будет создано.
- `spec.storageClassName` — класс хранилища, который будет использоваться для создания постоянного хранилища.
  В данном случае -- `longhorn`.
- `spec.accessModes` — режим доступа к хранилищу. `ReadWriteOnce` означает, что хранилище может быть смонтировано
  только одним подом для чтения и записи. У нас один под и база на SQLite, так что этого достаточно.
- `spec.resources.requests.storage` —  запрашиваемый размер хранилища. Мы запрашиваем 1 ГБ и не означает, что 
  хранилище будет занимать 1 ГБ на диске. Это предельный размер, который сможет занять хранилище.
  
Применим pvc-манифест:
```bash
sudo kubectl apply -f ~/k3s/vpn/x-ui/pvc-db.yaml
```

После этого Longhorn создаст том, который будет привязан к этому PVC.

Теперь нам нужно изменить манифест развертывания пода, и подключить к нему созданный PVC. Теперь наш
`~/k3s/vpn/x-ui/deployment.yaml` будет выглядеть так:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: x-ui
  namespace: x-ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: x-ui
  template:
    metadata:
      labels:
        app: x-ui
    spec:
      hostNetwork: true
      containers:
      - name: x-ui
        image: ghcr.io/mhsanaei/3x-ui:latest
        # image: enwaiax/x-ui:latest    # альтернативный облегчённый: меньше способов шифрования и китайский интерфейс
        volumeMounts:
        - name: db-storage      # Имя тома, в который будет смонтирован...
          mountPath: /etc/x-ui  # ...в путь к базе данных внутри контейнера
      volumes:
      - name: db-storage          # Имя тома, которое...
        persistentVolumeClaim:    # ...должно быть постоянным хранилищем
          claimName: x-ui-db-pvc  # ...и размещаться в PVC с именем 'x-ui-db-pvc'
```

Применим обновлённый манифест:
```bash
sudo kubectl apply -f ~/k3s/vpn/x-ui/deployment.yaml
```

Под перезапустится, и теперь база данных будет храниться в постоянном хранилище Longhorn. При перезапуске пода или его
"переезде" на другой узел, база данных останется доступной и не потеряется. Следует отметить, что при сбое узла 
процесс перемещения пода занимает некоторое время. В кластере на Orange Pi 5, где проверки связности не очень
агрессивные, это может занять до 5 минут. В общем, это нормально.

## Единая точка входа VPN-соединений через под 3x-ui

Под с 3x-ui может быть запущен k3s на произвольном узле, и может быть произвольно перемещён в кластере на другой узел.
Таким образом, если мы хотим предоставить доступ к VPN-соединениям из интернета, нам нужно настроить доступ через
единый IP-адрес. Это можно сделать несколькими способами.

### Доступ через VIP (виртуальный IP) c перенаправлял трафика через Keepalived на узел с подом с 3x-ui

При [развертывании k3s](../raspberry-and-orange-pi/k3s.md) на Orange Pi 5 Plus мы уже настраивали Keepalived. Теперь
надо настроить его так, чтобы узел с подом 3x-ui получал больший приоритет в Keepalived, и тогда виртуальный IP
будет получать трафик с этого узла.

**Лучшим решением будет динамическая настройка приоритета в Keepalived.**

Лучший способ — настроить так, чтобы приоритет Keepalived ноды автоматически повышался, если под 3x-ui запущен на ней.
Это можно сделать с помощью механизма `track_script`, который будет проверять наличие пода и динамически менять
приоритет. Такой подход сохранит текущую работу K3s API и [подов Shadowsocks](k3s-shadowsocks-client.md), добавив
поддержку 3x-ui.

Создадим проверочный скрипт (на каждом узле), который будет проверять наличие пода 3x-ui. Скрипт будет
расположен в `~/scripts/check_xui.sh`:
```bash
mkdir -p ~/scripts
nano ~/scripts/check_xui.sh
```

И вставим в него следующий код (на каждой ноде):
```bash
#!/usr/bin/bash
NODE_NAME=$(hostname)  # Получаем имя текущей ноды
POD_NAME=$(kubectl get pods -n x-ui -o jsonpath="{.items[?(@.spec.nodeName=='$NODE_NAME')].metadata.name}")
if [ -n "$POD_NAME" ]; then
    exit 0  # Под есть на этой ноде
else
    exit 1  # Пода нет
fi
```

Скрипт использует `kubectl`, чтобы проверить, есть ли под `3x-ui` в _namespace_ `x-ui` на текущей ноде. Использование 
`sudo` не требуется, так как скрипт будет запускаться `keepalived`, который работает от `root`.
Убедись, что kubectl доступен на всех нодах и настроен для работы с кластером (например, через kubeconfig).

Сделаем скрипт исполняемым (на каждой ноде):
```bash
sudo chmod +x ~/scripts/check_xui.sh
```

Обновим конфиг `reepalived`, добавив `vrrp_script` и привязку к нему через `track_script`. Теперь мы переведем все
ноды в **BACKUP** (чтобы избежать конфликтов), а приоритет будет динамически меняться в зависимости от наличия пода.

На перовой мастер-ноде:
```bash
sudo nano /etc/keepalived/keepalived.conf
```

И теперь там будет вот такой конфиг (не забудь указать правильное имя пользователя `<user>` в пути к скрипту):
```pycon
vrrp_script check_xui {
    script "/home/<user>/scripts/check_xui.sh"
    interval 2          # Проверять каждые 2 секунды
    weight 50           # Добавить 50 к приоритету, если под есть
}

vrrp_instance VI_1 {
    # state MASTER
    state BACKUP        # Все ноды стартуют как BACKUP
    interface enP4p65s0
    virtual_router_id 51
    priority 100        # Базовый приоритет
    advert_int 1
    unicast_src_ip 192.168.1.26
    unicast_peer {
        192.168.1.27
        192.168.1.28
    }
    virtual_ipaddress {
        192.168.1.200
    }
    track_script {
        check_xui       # Привязка к скрипту
    }
}
```

Перезапустим Keepalived:
```bash
sudo service keepalived restart
```

Аналогичным образом настроим конфиги на других узлах (добавить блок `vrrp_script` сверху, и добавить `track_script` в
`vrrp_instance`). Не забудь указать проверить `unicast_src_ip` для каждой ноды и перезапустить Keepalived на всех узлах.

Теперь на каждой ноде cкрипт `~/scripts/check_xui.sh` проверяет наличие пода `x-ui` каждые 2 секунды. Если под есть,
Keepalived добавляет 50 к базовому приоритету ноды (например, 100 → 150). Если пода нет, приоритет остаётся базовым
(100, 90 или 80). Нода с наивысшим приоритетом становится MASTER и получает виртуальный IP. Таким образом, VIP всегда
будет указывать на ноду с подом 3x-ui.

Теперь панель 3x-ui будет доступна с виртуального IP (192.168.1.200). Все VPN-соединения будут работать через него.
Так что если на домашнем роутере настроить перенаправление портов (для 2053-порта веб-панели 3x-ui, и портов которые
будем выбирать для VPN-соединений), то можно будет подключаться к 3x-ui и VPN-соединениям из любой точки мира.



### Доступ через Ingress Controller по имени домена (http).

Сейчас web-панель 3x-ui доступна через VIP по порту `2053` по http. _В принципе, так можно и оставить_. Но если мы хотим
иметь доступ по https, да еще чтобы это работало через доменное имя, и чтобы k3s автоматически получал и обновлял 
сертификаты, то можно использовать Ingress-контроллер. Он будет брать трафик с порта VIP, по порту `2055`, через
балансировщик svclb-traefik направлять его на Ingress-контроллер Traefik и перенаправлять его на под с 3x-ui (тоже
через VIP но уже по порту `2053`). Дополнительно, есть [заметка про настройку Traefik в качестве прокси](k3s-proxy.md).

#### Манифест для Ingress-контроллера Traefik

По умолчанию Ingress-контроллер Traefik в k3s слушает на портах 80 и 443 (HTTP и HTTPS) и перенаправляет трафик
на соответствующие поды. В моем случае порты 80 и 443 на моем роутере уже перенаправляются на другой хост.
В будущем я это, возможно, изменю, и сейчас я не могу перенаправить эти порты на VIP. Поэтому мне нужно настроить
Traefik так, чтобы он слушал http/https на другом порту (например, 2055, и порт, обратите внимание, стандартный
443-порт от продолжит слушать как и раньше) и перенаправлял трафик на под с 3x-ui (это только для http/https, то есть
для доступа в веб-интерфейсу 3x-ui, а не для VPN-соединений). Этот манифест задаёт глобальную конфигурацию Traefik
для всего кластера, а не только к 3x-ui, и потому лучше положить его в "общую" папку для Traefik, например:
`~/k3s/traefik/traefik-config.yaml`:

```bash
mkdir -p ~/k3s/traefik
nano ~/k3s/traefik/traefik-config.yaml
```

И вставим в него следующий код:
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    additionalArguments:
      - --entrypoints.web-custom.address=:2055    # Слушаем HTTP на 2055
      - --log.level=DEBUG
```
Что тут происходит: Для изменения настройки Traefik, создаётся HelmChartConfig (этот такой аналог пакетного менеджера
для Kubernetes, который позволяет управлять приложениями и сервисами в кластере). Этот манифест указывает Traefik,
в пространство имён `kube-system`, а аргумент `--entrypoints.web-custom.address=:2055` в конфигурацию -- инструкция:
_Слушай порт 2055 и назови эту точку входа **web-custom**_). После применения Traefik начнёт принимать запросы на порту
2055. Поскольку мой роутер пробрасывает 2055 на VIP-адрес (тоже 2055 порт), Traefik на ноде с VIP увидит этот трафик.

Применим манифест:
```bash
sudo kubectl apply -f ~/k3s/traefik/traefik-config.yaml
```

Теперь Traefik будет слушать http еще и на порту 2055.

#### Манифест для маршрутизации трафика на под с 3x-ui через Ingress-контроллер

Теперь нужно сказать Traefik, что запросы на домен `v.home.cube2.ru` через порт `2055` — это HTTP, и их надо
перенаправить на порт 2053, где работает 3x-ui. Для этого в каталоге с манифестами 3x-ui `~/k3s/vpn/x-ui/`
(ведь это касается подa с 3x-ui) создадим манифест IngressRoute:
```bash
nano ~/k3s/vpn/x-ui/ingressroute.yaml
```

И вставим в него следующий код (не забудь указать свой домен):
```yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: x-ui-ingress
  namespace: x-ui
spec:
  entryPoints:
    - web-custom  # ендпоинт, который "слушает" порт 2055
  routes:
    - match: Host("v.home.cube2.ru")
      kind: Rule
      services:
        - name: x-ui-external   # имя сервиса, на который будет перенаправлен трафик
          port: 2053            # порт, на который будет перенаправлен трафик
```

Что тут происходит? Мы создаём объект `IngressRoute`, который определяет маршрут для входящего трафика. Параметры:
- `kind` — тип объекта, который мы создаём. В данном случае это `IngressRoute`, который используется для
  маршрутизации трафика в Traefik.
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-ingress` и
  пространство имён `x-ui`, в котором будет создан объект (то же пространство, что и у пода с 3x-ui).
- `entryPoints` — точка входа, которая будет использоваться для маршрутизации трафика. В данном случае это `web-custom`,
  который мы настроили в предыдущем шаге.
- `routes` — определяет правила маршрутизации. В данном случае мы указываем, что если запрос приходит на домен
  `v.home.cube2.ru` (`match` — условие, которое должно быть выполнено для маршрутизации), то он будет перенаправлен
  на сервис `x-ui-external` (который мы создадим ниже) на порт `2053`.

Теперь создадим сервис `x-ui-external`, который будет использоваться для маршрутизации трафика на под с 3x-ui.

Создадим манифест сервиса в каталоге `~/k3s/vpn/x-ui/`:
```bash
nano ~/k3s/vpn/x-ui/x-ui-service.yaml
```

И вставим в него следующий код:
```yaml
# Service для 3x-ui с hostNetwork: true, использующего VIP 192.168.1.200
apiVersion: v1
kind: Service           # Тип объекта, который мы создаём. В данном случае это Service
metadata:
  name: x-ui-external   
  namespace: x-ui       
spec:
  ports:
    - port: 2053        
      targetPort: 2053  
      protocol: TCP
---
# Endpoints указывает на VIP, так как под не в сетевом пространстве Kubernetes
apiVersion: v1
kind: Endpoints         # Тип объекта, который мы создаём. В данном случае это Endpoints
metadata:
  name: x-ui-external   
  namespace: x-ui       
subsets:
  - addresses:
      - ip: 192.168.1.200   # IP-адрес (VIP), на который будет перенаправлен трафик
    ports:
      - port: 2053
        protocol: TCP
```

Что тут происходит? Мы создаём два объекта: `Service` и `Endpoints`. `Service` — это абстракция, которая предоставляет
единый IP-адрес и DNS-имя для доступа к группе подов. `Endpoints` — это объект, который указывает конечные точки
для перенаправления трафика. В нашем случае это VIP:2053, так как под 3x-ui использует `hostNetwork: true`
и недоступен через внутренние IP Kubernetes. Но обычно `Endpoints` указывают на имена подов, на которые отправляется
трафик. 

Для `Service` мы указываем:
- `kind` — тип объекта, который мы создаём. В данном случае это `Service`.
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-external` и
  пространство имён `x-ui`, в котором будет создан объект (то же пространство, что и у пода с 3x-ui).
- `spec` — спецификация объекта, которая определяет его поведение. Мы указываем, что сервис будет слушать внешний трафик
  на порту `2053` и перенаправлять на тот же порт внутри кластера.
- `ports` — определяет порты, на которых будет слушать сервис. Мы указываем, что сервис будет слушать
  на порту `2053` и перенаправлять трафик на тот же порт внутри кластера, и будем использоваться TCP.

Для `Endpoints` мы указываем:
- `kind` — тип объекта, который мы создаём. В данном случае это `Endpoints`.
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-external` (то же,
  что и у сервиса) и пространство имён `x-ui` (то же, что и у пода с 3x-ui).
- `subsets` — подмножество конечных точек, которые будут использоваться для маршрутизации трафика. Мы указываем, что
  в подмножестве есть одна конечная точка с IP 192.168.1.200 и портом 2053 (TCP). 

Применим манифесты:
```bash
sudo kubectl apply -f ~/k3s/vpn/x-ui/ingressroute.yaml
sudo kubectl apply -f ~/k3s/vpn/x-ui/x-ui-service.yaml
```

Перезагрузим Traefik, чтобы он увидел изменения:
```bash
kubectl rollout restart deployment traefik -n kube-system
```

Или для надёжности вовсе удалим поды с traefik и svclb-traefik, тогда они должны создастся заново, и гарантированно
примут новые настройки:
```bash
kubectl delete pod -n kube-system -l app.kubernetes.io/name=traefik
kubectl delete pod -n kube-system -l svccontroller.k3s.cattle.io/svcname=traefik
```

Проверим, что поды создались и запустились:
```bash
sudo kubectl get pods -n kube-system -o wide | grep traefik
```

Увидим что-то вроде (поды стартовали недавно):
```text
helm-install-traefik-c4vlp                      0/1     Completed   0             148m   10.42.0.84   opi5plus-2   <none>           <none>
svclb-traefik-4f8c2580-8pfdg                    4/4     Running     0             4m     10.42.2.62   opi5plus-1   <none>           <none>
svclb-traefik-4f8c2580-9tldj                    4/4     Running     0             4m     10.42.1.93   opi5plus-3   <none>           <none>
svclb-traefik-4f8c2580-pmbqj                    4/4     Running     0             4m     10.42.0.83   opi5plus-2   <none>           <none>
traefik-5db7d4fd45-45gj6                        1/1     Running     0             4m     10.42.0.82   opi5plus-2   <none>           <none>
```

Проверим, что сервисы создались и запустились:
```bash
sudo kubectl get svc -n kube-system -o wide | grep traefik
```

Увидим что-то вроде (есть обработка через порт 2055:ххххх):  
```text
traefik                LoadBalancer   10.43.164.48    192.168.1.26,192.168.1.27,192.168.1.28   80:31941/TCP,443:30329/TCP,9000:32185/TCP,2055:32627/TCP   53d   app.kubernetes.io/instance=traefik-kube-system,app.kubernetes.io/name=traefik
```

Проверим, что созданный сервис `x-ui-external` доступен:
```bash
sudo kubectl get svc -n x-ui -o wide
```

Увидим что-то вроде (сервис создан и слушает на порту 2053):
```text
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE   SELECTOR
x-ui-external   ClusterIP   10.43.73.106   <none>        2053/TCP   2h    <none>
```

Проверим, что созданный IngressRoute доступен:
```bash
sudo kubectl get ingressroutes -n x-ui -o wide
```

Увидим что-то вроде (IngressRoute создан):
```text
NAME           AGE
x-ui-ingress   14h
```

Проверим логи Traefik (не зря же мы включали отладку в манифесте)
```bash
kubectl get pods -n kube-system | grep traefik
sudo kubectl logs -n kube-system traefik-<hash> --since=5m
```
Ищем: `"web-custom": {"address": ":2055"}` и маршрут `x-ui-x-ui-ingress` с `Host("v.home.cube2.ru")`,

И наконец, проверим, что под с 3x-ui доступен по нашему доменному на порту 2055 через VIP-адрес (возможно, придется
сделать запись в `/etc/hosts`, если ваш роутер не может разрешить внешний домен внутрь домашней сети, и поставить
в соответствие домен и VIP):
```bash
curl -v http://v.home.cube2.ru:2055
```

**Все заработало**, мы видим, что запросы на домен `v.home.cube2.ru` через порт `2055` перенаправляются на под с 3x-ui

Если не получилось, то можно дополнительно проверить, что с сервисом `traefik` всё в порядке. Посмотрим его текущие
настройки:
```bash
sudo  kubectl get service -n kube-system traefik -o yaml
```

Мы должны увидеть в блоке `spec:ports` что-то типа:
```yaml
  - name: web-custom
    nodePort: тут-будет-номер-порта-внутри-балансировщика
    port: 2055
    protocol: TCP
    targetPort: 2055
```

Если блока нет, добавьте его через редактор (по умолчанию откроется `vim`, используйте `:wq` для сохранения и выхода):
```bash
sudo kubectl edit service -n kube-system traefik -o yaml
```

Найти в `spec:ports` блок:
```yaml
  - name: web                                                              
    nodePort: 31941                                                                       
    port: 80                          
    protocol: TCP        
    targetPort: web      
  - name: websecure      
    nodePort: 30329                                
    port: 443                                      
    protocol: TCP                                  
    targetPort: websecure  
```

И добавить под ним новый блок:
```yaml
  - name: web-custom
    port: 2055
    protocol: TCP
    targetPort: 2055
```

После сохранения изменений и выхода из редактора, сервис будет обновлён автоматически. Можно проверить, что ему присвоен
новый номер порта внутри балансировщика (см. выше) и возможно все заработает. Но скорее всего придется удалить манифесты
`ingressroute.yaml`, `x-ui-service.yaml` и все настраивать заново, проверять логи и т.д.









### Доступ через Ingress Controller c https и перенаправлением трафика на узел с подом с 3x-ui

Установим Cert-Manager для автоматического получения сертификатов Let's Encrypt. Это позволит нам использовать
HTTPS для доступа к 3x-ui (и другим подам). Cert-Manager автоматически обновляет сертификаты, когда они истекают.
```bash
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
```

В результате у нас появится три новых пода в пространстве имён `cert-manager`:
```bash
sudo k3s kubectl get pods -n cert-manager -o wide
```

Увидим что-то вроде:
```text
NAME                                       READY   STATUS    RESTARTS   AGE     IP           NODE         NOMINATED NODE   READINESS GATES
cert-manager-64478b89d5-p4msl              1/1     Running   0          8m36s   10.42.1.55   opi5plus-3   <none>           <none>
cert-manager-cainjector-65559df4ff-t7rj4   1/1     Running   0          8m36s   10.42.1.54   opi5plus-3   <none>           <none>
cert-manager-webhook-544c988c49-zxdxc      1/1     Running   0          8m36s   10.42.1.56   opi5plus-3   <none>           <none>
```

Cert-Manager состоит из трёх основных компонентов, каждый из которых запускается в своём поде:
* `cert-manager` -- основной контроллер. Он следит за ресурсами вроде Certificate и Issuer, запрашивает сертификаты
   у провайдеров (например, Let’s Encrypt) и обновляет их при необходимости.
* `cert-manager-cainjector` -- внедряет CA (Certificate Authority) в вебхуки и другие ресурсы Kubernetes, чтобы
  они могли доверять сертификатам, выданным Cert-Manager.
* `cert-manager-webhook` -- отвечает за валидацию и мутацию запросов на создание или обновление ресурсов, связанных
  с сертификатами. Он проверяет их на соответствие правилам.

#### Манифест для ClusterIssuer

Создадим манифест ClusterIssuer (эмитент кластера) для Cert-Manager и относится ко всему кластеру. В нем описываются
правила для получения сертификатов от внешнего поставщика (в нашем случае Let's Encrypt) и укажем твм адрес сервера
ACME, email для уведомлений, способы подтверждения владения доменом (например, через HTTP-01 или DNS-01).

```bash
mkdir ~/k3s/cert-manager
nano ~/k3s/cert-manager/clusterissuer.yaml
```

И вставим в него следующий код (не забудь указать свой email):
```yaml
piVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ваш@емейл.где-то
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: traefik  # Ingress-контроллер, например, traefik
```

Применим манифест, чтобы cert-manager принял конфигурацию:
```bash
sudo kubectl apply -f ~/k3s/cert-manager/clusterissuer.yaml 
```

Это будет работать для всего кластера и для всех подов (текущих и будущих). Cert-Manager будет автоматически
запрашивать и обновлять сертификаты. Let's Encrypt при проверке прав владения доменом посредством правила HTTP-01 
использует http (порт 80) и нужно настроить в роутере перенаправление трафика на кластер (лучше через VIP) для этого
порта.



--------
У меня развернут k3s-кластер из трех узлов (Orange Pi 5 Plus). Там есть под 3x-ui для внешнего доступа к VPN-соединениям.
Под настроен как hostNetwork: true и использует `Longhorn` для хранения базы данных. На всех узлах настроен keepalived так,
чтобы виртуальный IP-адрес (VIP) всегда указывал на узел с подом 3x-ui. Это позволяет поду ловить VPN-соединения из интернета
(благодаря пробросу портов на роутере). Веб-интерфейс 3x-ui по порту 2053 тоже доступен по VIP-адресу (192.168.1.200) благодаря
`hostNetwork: true` и виден из интернета по адресу `http://v.home.cube2.ru:2053`.

Я хочу настроить доступ к веб-интерфейсу 3x-ui через svclb-traefik по порту 2055 так, чтобы он стал доступен через VIP
и по 2055 порту и будущем можно было настроить доступ по https.


Кроме того у меня еще есть внешний хост с audiobookshelf по адресу http://192.168.1.15:8000  и я хотелбы настроить доступ
к нему через svclb-traefik по адресу http://zvuk.cube2.ru:2055.

Как это сделать? Какие конфиги тебе нужны чтобы ты смог мне помочь?