Compare commits

..

89 Commits

Author SHA1 Message Date
f2583a89cc mod: Перенос контейнера Docker в k3s...... 2025-07-29 23:09:35 +03:00
948dbe28b0 mod: Развертывание Metabase в K3s .. 2025-07-08 19:38:08 +03:00
be40e3764d mod: минор 2025-07-07 15:48:10 +03:00
cc4a4add8c add: Развертывание Metabase в K3s . 2025-07-07 15:46:22 +03:00
0af69580e7 add: Под с ChartDB (графический редактор схем баз данных) . 2025-07-04 11:15:59 +03:00
fa0eb3188f add: Под с ChartDB (графический редактор схем баз данных) 2025-07-04 00:50:03 +03:00
adbc6c4072 add: Под с PostgeSQL ..... 2025-05-26 22:40:59 +03:00
c70eb60c8a add: Под с PostgeSQL .... 2025-05-26 22:07:16 +03:00
f7203aabdc add: Под с PostgeSQL .. 2025-05-26 22:04:17 +03:00
026a03de3d add: Под с PostgeSQL . 2025-05-26 22:01:35 +03:00
54b743e2ad add: Под с PostgeSQL 2025-05-26 21:31:35 +03:00
60e8e24ed7 add: Под с SmokePing .. 2025-05-25 12:08:20 +03:00
dad496edcb add: Под с SmokePing . 2025-05-23 20:58:47 +03:00
010d85a0ce add: Под с SmokePing 2025-05-23 20:33:56 +03:00
d6c747f7c6 add: Защита хоста с помощью CrowdSec ................ 2025-05-19 23:54:46 +03:00
e32b702d7c add: Защита хоста с помощью CrowdSec ............... 2025-05-19 23:45:11 +03:00
08042c99b2 add: Защита хоста с помощью CrowdSec .............. 2025-05-19 23:43:32 +03:00
39a731f369 add: Защита хоста с помощью CrowdSec ............. 2025-05-19 23:38:22 +03:00
af3da4aca3 add: Защита хоста с помощью CrowdSec ............ 2025-05-19 23:37:36 +03:00
356a6d72bb add: Защита хоста с помощью CrowdSec ........... 2025-05-19 11:00:18 +03:00
da9a92f9c6 add: Защита хоста с помощью CrowdSec .......... 2025-05-18 23:10:28 +03:00
f94b1bd679 add: Защита хоста с помощью CrowdSec ......... 2025-05-18 21:14:43 +03:00
b594271e03 add: Защита хоста с помощью CrowdSec ........ 2025-05-18 15:06:55 +03:00
f04d593f53 add: Защита хоста с помощью CrowdSec ....... 2025-05-18 14:50:21 +03:00
75b24bbdab add: Защита хоста с помощью CrowdSec ...... 2025-05-18 14:15:49 +03:00
f94d7e42ed add: Защита хоста с помощью CrowdSec ..... 2025-05-18 14:11:33 +03:00
fc924e196b add: Защита хоста с помощью CrowdSec .... 2025-05-18 14:08:36 +03:00
af40c9c716 add: Защита хоста с помощью CrowdSec ... 2025-05-18 00:31:30 +03:00
ce7cea9b0f add: Защита хоста с помощью CrowdSec .. 2025-05-17 22:54:00 +03:00
c80f4fa469 add: Защита хоста с помощью CrowdSec . 2025-05-17 21:15:55 +03:00
88b7e81b84 add: Перенос контейнера Docker в k3s Защита кластера с помощью CrowdSec 2025-05-16 18:34:26 +03:00
8d5b6d1306 mod: Перенос контейнера Docker в k3s ......... 2025-05-16 18:03:29 +03:00
ffb24ad816 ---: Подключение менеджера сертификатов (cert-manager) Let's Encrypt .... 2025-05-16 18:00:58 +03:00
e62371bb52 mod: Настройка доступа к панелям управления Lobghorn и Traefik... 2025-05-03 19:05:57 +03:00
76cf200847 mod: Настройка доступа к панелям управления Lobghorn и Traefik.. 2025-05-03 16:23:24 +03:00
625b6c0b0a mod: Настройка доступа к панелям управления Lobghorn и Traefik. 2025-05-03 15:59:23 +03:00
19ba8e07e4 add: Настройка доступа к панелям управления Lobghorn и Traefik 2025-05-03 15:58:21 +03:00
920cf30bae mod: backup k3s ...... 2025-04-30 00:01:52 +03:00
18d396991e mod: backup k3s ..... 2025-04-29 23:02:21 +03:00
92ad28bc58 mod: backup k3s .... 2025-04-29 21:34:18 +03:00
1abeffd72c mod: backup k3s ... 2025-04-29 21:17:12 +03:00
c3d4600662 mod: backup k3s .. 2025-04-29 16:22:26 +03:00
9629b85011 add: backup k3s . 2025-04-28 20:42:08 +03:00
475a294d61 mod: Перенос контейнера Docker в k3s ........ 2025-04-28 13:44:51 +03:00
7bfc9001e0 mod: Перенос контейнера Docker в k3s ....... 2025-04-28 13:39:50 +03:00
ceb94eb151 mod: Перенос контейнера Docker в k3s ...... 2025-04-28 13:33:58 +03:00
df7777655c mod: Перенос контейнера Docker в k3s..../ 2025-04-28 10:57:34 +03:00
1abefba930 mod: Перенос контейнера Docker в k3s.... 2025-04-28 10:54:10 +03:00
9f713ba93a mod: развертывание k3s (iSCSI). 2025-04-28 09:15:07 +03:00
60650b69d2 mod: развертывание k3s (iSCSI) 2025-04-28 09:13:26 +03:00
2fb12824e2 mod: развертывание k3s (права на .kube/config, избавление от sudo) 2025-04-27 22:05:14 +03:00
93548f786e add: одключение менеджера сертификатов (cert-manager) Let's Encrypt ... 2025-04-27 22:01:23 +03:00
e12e8a5b80 add: одключение менеджера сертификатов (cert-manager) Let's Encrypt .. 2025-04-27 22:00:35 +03:00
819c122a2b add: одключение менеджера сертификатов (cert-manager) Let's Encrypt . 2025-04-27 21:55:52 +03:00
bf15c4fb7b add: одключение менеджера сертификатов (cert-manager) Let's Encrypt 2025-04-27 21:45:03 +03:00
effe549f75 Fix Git hooks permissions 2025-04-27 13:48:03 +03:00
205e8b9c53 mod: Перенос контейнера Docker в k3s..... 2025-04-27 11:57:28 +03:00
c18a5ffe43 mod: Перенос контейнера Docker в k3s.... 2025-04-27 11:50:00 +03:00
9e1a414f45 mod: Перенос контейнера Docker в k3s... 2025-04-25 00:20:24 +03:00
138bb728fc mod: Перенос контейнера Docker в k3s.. 2025-04-24 17:55:15 +03:00
dc04658e70 mod: проксирование через traefik..... 2025-04-22 15:44:14 +03:00
5560ed2d8e mod: проксирование через traefik.... 2025-04-22 15:43:31 +03:00
3b64bc44ac mod: проксирование через traefik... 2025-04-22 15:39:56 +03:00
0b4bc455bf mod: проксирование через traefik.. 2025-04-22 15:38:13 +03:00
e2c18fd572 mod: проксирование через traefik 2025-04-22 15:26:28 +03:00
3bc414ca98 add: Перенос контейнера Docker в k3s. 2025-04-19 19:53:48 +03:00
b6b54e136c add: под с 3X-UI ......... 2025-04-19 19:40:36 +03:00
f50b509440 add: проксирование через traefik 2025-04-16 17:42:20 +03:00
3a71c76f89 add: под с 3X-UI ........ 2025-04-16 15:43:03 +03:00
20fa88fa19 add: под с 3X-UI ....... 2025-04-16 13:50:53 +03:00
c54ab2ebc1 add: под с 3X-UI ...... 2025-03-31 18:09:01 +03:00
f3a296f9e4 add: под с 3X-UI ..... 2025-03-29 17:44:39 +03:00
09d32c8cb1 add: под с 3X-UI .... 2025-03-29 17:43:53 +03:00
3a1ce1370b add: под с 3X-UI ... 2025-03-29 13:58:59 +03:00
558653d7e9 add: под с 3X-UI .. 2025-03-29 00:11:00 +03:00
433a3b102f add: под с 3X-UI . 2025-03-28 23:51:58 +03:00
1571e7118d add: под с 3X-UI . 2025-03-28 16:19:03 +03:00
ed9159335e add: под с 3X-UI 2025-03-28 16:17:48 +03:00
c7a5241d33 add: minor. 2025-03-25 16:04:39 +03:00
0a90d4a444 add: minor 2025-03-25 11:24:04 +03:00
81fbf2ffa6 add: сборка ядра Linux для opi5plus (fin_2) 2025-03-25 11:19:39 +03:00
a49d432419 add: сборка ядра Linux для opi5plus (fin+note) 2025-03-24 18:44:03 +03:00
84a6ba4c92 add: сборка ядра Linux для opi5plus (fin) 2025-03-24 18:25:01 +03:00
b470dcdd5d add: сборка ядра Linux для opi5plus (.4) 2025-03-23 22:29:37 +03:00
86c51d33ce add: сборка ядра Linux для opi5plus (.3) 2025-03-23 22:27:23 +03:00
cbf3ae56bb add: сборка ядра Linux для opi5plus (.2) 2025-03-23 21:35:05 +03:00
7b0853da9c add: сборка ядра Linux для opi5plus (.1) 2025-03-23 15:08:41 +03:00
0a6c1591ab add: k3s (ss-client fin) 2025-03-21 14:35:42 +03:00
1437724e55 add: k3s (ss-client fin) 2025-03-21 14:34:11 +03:00
25 changed files with 6654 additions and 83 deletions

View File

@@ -11,21 +11,32 @@
## Kubernetes (k3s/k8s)
* [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md)
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) (k3s)
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) в k3s
* [Подключение менеджера сертификатов (cert-manager) Let's Encrypt](kubernetes/k3s-lets-encrypt-cert-manager.md) к k3s
* [Под с 3X-UI](kubernetes/k3s-3xui-pod.md) в k3s
* [Проксирование внешнего хоста через Traefik (Ingress-контроллер)](kubernetes/k3s-proxy.md)
* [Перенос контейнера Docker в k3s](kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md) (на примере Gitea)
* [Резервное копирование k3s](kubernetes/k3s-backup.md)
* [Настройка доступа к панелям управления](kubernetes/k3s-setting-up-web-access-to-dashboard.md) Longhorn и Traefik
* [Под с SmokePing](kubernetes/k3s_smokeping.md) для мониторинга доступности хостов
* [PostgeSQL в K3s](kubernetes/k3s-postresql.md)
* [ChartDB в K3s](kubernetes/k3s-chartdb.md) — графический редактор схем баз данных
* [Развертывание Metabase в K3s](kubernetes/k3s-metabase.md) — BI-платформа для анализа данных
## Python
* [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
* [Python-скрипт как служба Linux](python/python_as_service.md)
## Orange Pi / Raspberry Pi
## Linux (возможно в специфике Orange Pi / Raspberry Pi)
* [Установка (перенос) системы на NVMe или eMMC (для Orange Pi 5 Plus)](raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md)
* [Измерение производительности накопителей](raspberry-and-orange-pi/measuring-performance-storage-devices.md)
* [Установка Docker и Docker Compose](raspberry-and-orange-pi/install-docker-compose.md)
* [Резервное копирование и восстановление](raspberry-and-orange-pi/backup-restore.md)
* [k8s (кubernetes) на Orange Pi (драфт...)](raspberry-and-orange-pi/k8s.md)
* [k3s (кubernetes) на Orange Pi](raspberry-and-orange-pi/k3s.md)
* [Перекомпиляция ядра Linux (включение пподдержки iSCSI в Orange Pi 5 Plus](raspberry-and-orange-pi/opi5plus-rebuilding-linux-kernel-for-iscsi.md)
* [Защита хоста с помощью CrowdSec](raspberry-and-orange-pi/host-protection-with-crowdsec.md), включая GeoIP блокировки
*
## Nginx / Apache
* [Ограничение доступа по User-Agent (на примере GPTBot)](nginx/nginx-ban-user-agent.md)
* [Настройка nginx как прямого прокси](nginx/nginx_as_direct_proxy.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
images/k3s--chartdb-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/k3s--longhorn-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

687
kubernetes/k3s-3xui-pod.md Normal file
View File

@@ -0,0 +1,687 @@
# Установка 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, запрашивает сертификаты
у провайдеров (например, Lets 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.
Как это сделать? Какие конфиги тебе нужны чтобы ты смог мне помочь?

198
kubernetes/k3s-affinate.md Normal file
View File

@@ -0,0 +1,198 @@
# Аффинность узлов в K3s
Аффинность узлов (node affinity) в K3s позволяет управлять размещением подов на определённых узлах кластера.
Это полезно, когда нужно обеспечить запуск приложения на узле с определёнными характеристиками или метками.
?Привязка узлов может быть и жесткой и не жесткой.
## Под с предпочтительной привязкой к узлу
У меня есть [VPN-под на 3x-ui](k3s-3xui-pod.md), и предпочтительно, чтобы он при старте k3s запускался на узле
c наивысшем приоритетом в keepalive (у меня это хост `opi5plus-1`), но если нода "упадет" и под 3x-ui "переедет"
на другой узел, то keepalive повысит приоритет т ноды При этом возвращать под на исходный узел после его восстановления не обязательно.
(например, opi5plus-1), но мог переезжать на другие узлы (например, opi5plus-2 или opi5plus-3), если что-то пойдёт не так с основным узлом. При этом возвращать под на исходный узел после его восстановления не обязательно.
Ответ: Да, это возможно! В Kubernetes для этого используется механизм node affinity (аффинность к узлам). Он позволяет указать "мягкое" предпочтение для запуска пода на определённом узле, но не запрещает поду перемещаться, если нужного узла нет.
Как настроить node affinity
Допустим, ты хочешь, чтобы под запускался на opi5plus-1 по умолчанию, но мог переехать на другие узлы при необходимости.
Шаги:
Проверь или добавь метки узлам: Убедись, что твои узлы имеют метки (labels). Например:
bash
Collapse
Wrap
Copy
kubectl label nodes opi5plus-1 preferred=true
kubectl label nodes opi5plus-2 preferred=false
kubectl label nodes opi5plus-3 preferred=false
Здесь preferred=true — это метка, которая указывает предпочтительный узел.
Обнови манифест деплоя 3x-ui: Добавь секцию affinity в спецификацию пода. Пример:
yaml
Collapse
Wrap
Copy
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:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: preferred
operator: In
values:
- "true"
containers:
- name: x-ui
image: ghcr.io/mhsanaei/3x-ui:latest
# Остальные настройки контейнера...
Что здесь происходит:
preferredDuringSchedulingIgnoredDuringExecution — это "мягкое" правило. Kubernetes постарается запустить под на узле с меткой preferred=true, но если это невозможно (например, узел недоступен), под запустится на любом другом доступном узле.
weight: 1 — вес предпочтения. Если у тебя будет несколько правил, можно задавать приоритеты, но для одного правила это не так важно.
Как это работает:
Если узел opi5plus-1 доступен, под запустится там.
Если opi5plus-1 упадёт, под автоматически переедет на другой узел (например, opi5plus-2).
После восстановления opi5plus-1 под останется на текущем узле и не вернётся автоматически, что соответствует твоему запросу.
Как это сочетается с Keepalived?
Твой скрипт проверки в Keepalived гарантирует, что виртуальный IP будет следовать за подом 3x-ui. Если ты задашь opi5plus-1 как предпочтительный узел через affinity и дашь ему высокий приоритет в Keepalived (например, 100), то в нормальной ситуации под и IP будут на одном узле. А если под переедет из-за сбоя, Keepalived переключит IP на новый узел благодаря скрипту.
Итог
Скрипт проверки: sudo не нужен, так как Keepalived запускает скрипт от root. Если будут проблемы с доступом, добавь --kubeconfig с путём к файлу конфигурации.
Приоритет узла для пода: Используй node affinity с "мягким" предпочтением (preferredDuringSchedulingIgnoredDuringExecution). Это позволит поду запускаться на нужном узле, но свободно перемещаться при сбоях без обязательного возвращения.
Твоя идея с приоритетами и "кочеванием" пода отлично реализуема, и связка Keepalived + affinity обеспечит гибкость и надёжность. Удачи с настройкой, если что — пиши! 😊
Отлично. Ты мне отлично помог и 3x-ui заработал на VIP. Все сработало как ты сказал. Объясни пожалуйста про метки:
```
kubectl label nodes opi5plus-1 preferred=true
kubectl label nodes opi5plus-2 preferred=false
kubectl label nodes opi5plus-3 preferred=false
```
это означает, что все поды будут предпочитать opi5plus-1 или это касается только подов с nodeAffinity?
3 / 3
Completed
42s
Thinking
Исследую использование меток
Thinking
Исследую использование меток
Пользователь интересуется метками в K3s и их применением для привязки узлов. Они пометили один узел как "preferred=true", а другие как "preferred=false".
Метки в K3s — это пары ключ-значение, прикрепленные к объектам, таким как узлы, поды, сервисы. Они помогают организовать и выбрать подмножества объектов.
Привязка узлов позволяет указать, на каких узлах должен размещаться под. Есть два типа: обязательная и предпочтительная.
Метка "preferred=true" влияет только на поды с привязкой узлов, ищущими эту метку. Поды без таких правил размещаются без учета этой метки.
Все три узла имеют метку "preferred", но с разными значениями: opi5plus-1 — true, opi5plus-2 и opi5plus-3 — false.
Если под не имеет привязки узлов, планировщик размещает его на любом узле с нужными ресурсами, игнорируя метку "preferred".
Под без nodeSelector или nodeAffinity размещается на любом узле с необходимыми ресурсами. Метка "preferred" важна только для подов, ссылающихся на нее.
В K8s, метка "preferred=false" добавляет метку "preferred" со значением "false" на узел. Если метки нет, узел ее не имеет.
Под с предпочтительной привязкой сначала пытается разместиться на opi5plus-1, где preferred=true. Если не получится, может быть размещен на opi5plus-2 или opi5plus-3.
Метка "preferred=true" влияет только на поды с правилами, ищущими эту метку. Другие поды размещаются по другим критериям.
В стандартном K3s нет поведения, которое по умолчанию использует метки, такие как "preferred", для влияния на планирование. Метка важна только для подов, явно ссылающихся на нее в правилах привязки.
Другие поды, без ссылок на эту метку, будут размещены без учета "preferred", основываясь на доступности ресурсов.
Пример: под без привязки узлов размещается на любом узле, игнорируя метку "preferred". Это подтверждает, что метка влияет только на поды с соответствующими правилами.
Если под указывает nodeSelector с preferred="false", он размещается только на узлах, где preferred=false, например, opi5plus-2 или opi5plus-3.
Метка "preferred" влияет только на поды с правилами привязки или селектора, ссылающимися на эту метку. Поды без таких правил не затрагиваются.
Метка "preferred" важна только для подов, таких как 3x-ui, с установленной предпочтительной привязкой. Другие поды в кластере размещаются по своим спецификациям, если нет похожих правил.
Метки на узлах важны только тогда, когда правила планирования пода ссылаются на них. Например, под может исключать узлы, где preferred=true, если указано соответствующее правило.
Прямой ответ
Метки, такие как preferred=true или preferred=false, влияют только на поды с указанной аффинностью узлов, которые ссылаются на эту метку.
Другие поды, без аффинности, размещаются без учёта этих меток, основываясь на доступных ресурсах.
Влияние на поды
Метки на узлах, вроде preferred, не имеют значения для подов, если те не имеют правил аффинности, ссылающихся на эту метку. Например, под с аффинностью, предпочитающей preferred=true, будет стараться запускаться на opi5plus-1, но может переместиться на другие узлы, если opi5plus-1 недоступен.
Неожиданный нюанс
Интересно, что метки могут использоваться в других правилах, например, для исключения узлов (NotIn), но это требует явного указания в спецификации пода, что выходит за рамки стандартного поведения.
Отчёт
Введение
Метки (labels) в K3s — это ключевые пары, прикрепляемые к объектам, таким как узлы, поды и сервисы, для организации и выбора подмножества объектов. В контексте планирования подов метки на узлах используются для влияния на их размещение через механизмы, такие как аффинность узлов (node affinity). Пользователь задал вопрос о влиянии меток preferred=true и preferred=false, установленных на узлы, на все поды или только на те, у которых указана аффинность узлов.
Анализ влияния меток
Метки на узлах, такие как preferred=true или preferred=false, добавляются через команды вроде kubectl label nodes opi5plus-1 preferred=true. Эти метки сами по себе не имеют встроенного значения для планировщика K3s, если поды не ссылаются на них в своих правилах размещения.
В K3s, основанном на Kubernetes, планировщик подов (scheduler) размещает поды на узлах, учитывая доступные ресурсы (CPU, память) и любые ограничения, указанные в спецификации пода. Метки становятся значимыми только тогда, когда под использует их в правилах nodeSelector, nodeAffinity или nodeAntiAffinity.
Пользователь настроил метки следующим образом:
opi5plus-1: preferred=true
opi5plus-2: preferred=false
opi5plus-3: preferred=false
И в манифесте пода 3x-ui указал аффинность:
yaml
Collapse
Wrap
Copy
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: preferred
operator: In
values:
- "true"
Это означает, что под предпочтительно будет размещён на узле с меткой preferred=true, то есть на opi5plus-1. Если такой узел недоступен, под может быть размещён на opi5plus-2 или opi5plus-3, где preferred=false.
Влияние на поды без аффинности
Поды, у которых не указаны nodeSelector, nodeAffinity или nodeAntiAffinity, размещаются планировщиком K3s без учёта метки preferred. Планировщик выбирает узел на основе:
Доступных ресурсов.
Отсутствия конфликтов (например, taints).
Других факторов, таких как анти-аффинность подов или балансировка нагрузки.
Таким образом, метки preferred=true и preferred=false не влияют на поды, у которых нет правил, ссылающихся на эту метку. Например, поды Shadowsocks, если они не имеют аффинности, будут размещаться на любом узле без учёта preferred.
Пример с таблицей
Вот таблица, которая иллюстрирует влияние меток на поды:
Под Имеет аффинность к preferred? Узел предпочтения Возможные узлы при сбое
3x-ui Да, preferred=true opi5plus-1 opi5plus-2, opi5plus-3
Shadowsocks (без аффинности) Нет Любой доступный узел Любой доступный узел
Заключение
Метки preferred влияют только на поды с аффинностью, ссылающейся на эту метку, и не затрагивают остальные поды. Это позволяет гибко управлять размещением, сохраняя свободу для других приложений.
Ключевые цитаты
Kubernetes Node Affinity and Anti-Affinity https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
K3s Documentation on Node Labels https://grok.com/chat/6a083b91-ea05-4246-9407-b9b25af4b48f#:~:text=K3s%20Documentation%20on%20Node%20Labels

281
kubernetes/k3s-backup.md Normal file
View File

@@ -0,0 +1,281 @@
# Резервное копирование и восстановление K3s
У меня все манифесты хранятся в домашнем каталоге в папке `~/k3s`, но сохранение манифестов не обеспечит резервного
копирования (хотя и будет хорошим подспорьем). Но в k3s есть еще настройки развертывания, маршруты, секреты,
данные etcd (базы данных, в котрой хранится и синхронизируется вся информация k3s) и тома блочного хранилища
PersistentVolumeClaims (PVC). Хочется сделать резервную копию всего этого, на случай сбоя и фактора "кривых рук".
```bash
mkdir -p ~/script
nano ~/script/backup-k3s.sh
```
И вставить туда вот такой скрипт (не забудьте заменить `<secret-password>`, `<NAS-IP>` и `<FOLDER>` на свои значения):
```bash
#!/usr/bin/bash
# Скрипт для резервного копирования компонентов K3s (снапшоты etcd, манифесты, секреты)
# на сетевой ресурс SAMBA.
# --- Конфигурация ---
# Локальная точка монтирования для SAMBA
MOUNT_POINT="/media/backup"
# Сетевой ресурс SAMBA
SAMBA_USER="<USER>"
SAMBA_PASSWORD="<secret-password>" # Лучше использовать файл credentials: credentials=/путь/к/.smbcreds
SAMBA_SHARE="//<NAS-IP>/<FOLDER>"
# Каталог для резервных копий на SAMBA
BACKUP_DIR="${MOUNT_POINT}/k3s-backup"
# Каталог с манифестами K3s
MANIFESTS_DIR="/home/opi/k3s"
# Каталог со снапшотами etcd K3s
ETCD_SNAPSHOT_DIR="/var/lib/rancher/k3s/server/db/snapshots"
# Домашний каталог пользователя (используется для cd)
USER_HOME="/home/opi"
# Сколько дней хранить старые резервные копии
RETENTION_DAYS=14
# Формат даты для имен файлов и записей в журнале
DATE_FORMAT='%F--%H-%M-%S'
# Файл журнала на SAMBA
LOG_FILE="${BACKUP_DIR}/-backup---"$(date +${DATE_FORMAT})".log"
# --- Вспомогательные функции ---
# Функция для записи сообщения в журнал и на консоль
log_message() {
local message="$1"
local timestamp
timestamp=$(date +'%F %R:%S')
# Выводим на консоль и дописываем в файл журнала (если он уже доступен)
# Добавляем проверку, существует ли каталог для лога, на случай ошибки монтирования
if [ -d "$(dirname "${LOG_FILE}")" ]; then
echo -e "${timestamp} - ${message}" | tee -a "${LOG_FILE}"
else
# Если каталог недоступен (например, до монтирования или после размонтирования), пишем только в консоль
echo -e "${timestamp} - ${message}"
fi
}
# Функция для вывода разделителя в журнал и на консоль
log_separator() {
local timestamp
timestamp=$(date +'%F %R:%S')
if [ -d "$(dirname "${LOG_FILE}")" ]; then
echo -e "${timestamp} - =========================" | tee -a "${LOG_FILE}"
else
echo -e "${timestamp} - ========================="
fi
}
# Функция для завершения скрипта и размонтирования SAMBA
cleanup_and_exit() {
local exit_code=$? # Захватываем код завершения последней команды
local timestamp
timestamp=$(date +'%F %R:%S')
# Логируем код завершения *до* попытки размонтирования, пока лог-файл (возможно) доступен
log_message "Скрипт завершился с кодом ${exit_code}."
# Пытаемся размонтировать SAMBA, если она примонтирована
if mountpoint -q "${MOUNT_POINT}"; then
log_message "Размонтирование SAMBA ресурса '${MOUNT_POINT}'..." # Это сообщение еще (возможно) попадет в лог
log_separator # И это тоже
if umount "${MOUNT_POINT}"; then
# <<< РЕСУРС УСПЕШНО РАЗМОНТИРОВАН >>>
# Выводим сообщение только в консоль, так как лог-файл уже недоступен
echo "${timestamp} - SAMBA ресурс успешно размонтирован."
else
# Ошибка размонтирования. Лог-файл может быть еще доступен, а может и нет.
# Надежнее вывести ошибку в консоль.
echo "${timestamp} - ОШИБКА: Не удалось размонтировать SAMBA ресурс '${MOUNT_POINT}'."
fi
else
# Ресурс не был примонтирован, лог-файл на нем недоступен
echo "${timestamp} - SAMBA ресурс '${MOUNT_POINT}' не примонтирован или уже размонтирован."
fi
exit "${exit_code}"
}
# Перехватываем сигнал EXIT для запуска функции очистки
trap cleanup_and_exit EXIT
# --- Основной скрипт ---
echo "Запуск скрипта резервного копирования K3s..." # Это сообщение только в консоль
# Проверяем, что скрипт запущен от имени root (нужно для mount, доступа к /var/lib/rancher)
if [[ $EUID -ne 0 ]]; then
echo "ОШИБКА: Этот скрипт должен быть запущен от имени root (используй sudo)."
exit 1
fi
# 1. Подготовка точки монтирования
echo "Проверка и создание локальной точки монтирования '${MOUNT_POINT}'..." # Только консоль
if [ ! -d "${MOUNT_POINT}" ]; then
if mkdir -p "${MOUNT_POINT}"; then
echo "Точка монтирования '${MOUNT_POINT}' создана." # Только консоль
else
echo "ОШИБКА: Не удалось создать точку монтирования '${MOUNT_POINT}'."
exit 1
fi
fi
echo "=========================" # Только консоль
# 2. Монтирование SAMBA ресурса
echo "Монтирование SAMBA ресурса '${SAMBA_SHARE}' в '${MOUNT_POINT}'..." # Только консоль
# Для безопасности лучше использовать файл credentials: -o credentials=/путь/к/.smbcreds,uid=1000,gid=1000 и т.д.
if ! mount -t cifs -o username="${SAMBA_USER}",password="${SAMBA_PASSWORD}" "${SAMBA_SHARE}" "${MOUNT_POINT}"; then
echo "ОШИБКА: Не удалось примонтировать SAMBA ресурс."
exit 1
fi
log_message "SAMBA ресурс успешно примонтирован."
log_separator
# 3. Подготовка каталога для резервных копий на SAMBA
# Теперь можно использовать log_message, т.к. каталог BACKUP_DIR должен быть доступен
log_message "Проверка и создание каталога для резервных копий '${BACKUP_DIR}' на SAMBA..."
if [ ! -d "${BACKUP_DIR}" ]; then
if mkdir -p "${BACKUP_DIR}"; then
log_message "Каталог для резервных копий '${BACKUP_DIR}' создан."
else
log_message "ОШИБКА: Не удалось создать каталог '${BACKUP_DIR}' на SAMBA ресурсе."
exit 1 # Выходим, так как некуда сохранять резервные копии
fi
fi
log_separator
# Начинаем полноценное логирование в файл на примонтированном ресурсе
log_message "Начало процесса резервного копирования (лог: ${LOG_FILE})..."
log_separator
# Переходим в домашний каталог пользователя (если нужно для относительных путей, хотя сейчас используются абсолютные)
cd "${USER_HOME}" || { log_message "ОШИБКА: Не удалось перейти в каталог ${USER_HOME}"; exit 1; }
# 4. Резервное копирование снапшотов etcd
log_message "Резервное копирование снапшотов etcd из '${ETCD_SNAPSHOT_DIR}'..."
etcd_backup_file="${BACKUP_DIR}/etcd-------$(date +"${DATE_FORMAT}").zip"
if /usr/bin/zip -r "${etcd_backup_file}" "${ETCD_SNAPSHOT_DIR}"; then
log_message "Снапшоты etcd сохранены в ${etcd_backup_file}."
else
log_message "ОШИБКА: Не удалось создать резервную копию снапшотов etcd."
# Решите, является ли это критической ошибкой или скрипт может продолжаться
fi
log_separator
# 5. Резервное копирование манифестов
log_message "Резервное копирование манифестов из '${MANIFESTS_DIR}'..."
manifests_backup_file="${BACKUP_DIR}/manifests--$(date +"${DATE_FORMAT}").zip"
if /usr/bin/zip -r "${manifests_backup_file}" "${MANIFESTS_DIR}"; then
log_message "Манифесты сохранены в ${manifests_backup_file}."
else
log_message "ОШИБКА: Не удалось создать резервную копию манифестов."
fi
log_separator
# 6. Резервное копирование секретов Kubernetes
log_message "Резервное копирование секретов Kubernetes..."
secrets_backup_file="${BACKUP_DIR}/secrets----$(date +"${DATE_FORMAT}").zip"
# Безопасно создаем временный каталог
tmp_secrets_dir=$(mktemp -d -t k8s-secrets-backup-XXXXXX)
if [[ -z "$tmp_secrets_dir" || ! -d "$tmp_secrets_dir" ]]; then
log_message "ОШИБКА: Не удалось создать временный каталог для резервной копии секретов."
else
log_message "Создан временный каталог для секретов: ${tmp_secrets_dir}"
secrets_exported=false
# Получаем все пространства имен, исключая некоторые системные (при необходимости)
namespaces=$(kubectl get ns -o jsonpath='{.items[*].metadata.name}')
# Если нужно, настройте исключаемые из резервного копирования пространства имен
# namespaces=$(kubectl get ns -o jsonpath='{.items[*].metadata.name}' --field-selector metadata.name!=kube-system,metadata.name!=kube-public,metadata.name!=kube-node-lease,metadata.name!=default,metadata.name!=longhorn-system,metadata.name!=cert-manager)
for ns in $namespaces; do
log_message "Экспорт секретов из пространства имен: ${ns}"
# Определяем путь к файлу вывода во временном каталоге
secret_file="${tmp_secrets_dir}/secrets-${ns}.yaml"
# Экспортируем секреты во временный файл
if kubectl get secret -n "${ns}" -o yaml > "${secret_file}"; then
# Проверяем, не пустой ли файл (если в namespace нет секретов)
if [[ -s "${secret_file}" ]]; then
log_message "Успешно экспортированы секреты для пространства имен ${ns} в ${secret_file}"
secrets_exported=true
else
log_message "В пространстве имен ${ns} нет секретов, пропускаем."
rm "${secret_file}" # Удаляем пустой файл
fi
else
log_message "ПРЕДУПРЕЖДЕНИЕ: Не удалось экспортировать секреты для пространства имен ${ns}. Возможно, оно пустое или недоступно."
# Удаляем файл, если он был создан, но команда завершилась с ошибкой
[ -f "${secret_file}" ] && rm "${secret_file}"
fi
done
# Архивируем собранные секреты из временного каталога
if [ "$secrets_exported" = true ]; then
# Используем флаг -j, чтобы не сохранять структуру временного каталога в архиве
if /usr/bin/zip -j "${secrets_backup_file}" "${tmp_secrets_dir}"/*; then
log_message "Секреты сохранены в ${secrets_backup_file}."
else
log_message "ОШИБКА: Не удалось заархивировать экспортированные секреты."
fi
else
log_message "Секреты для экспорта не найдены, создание архива пропущено."
fi
# Очищаем временный каталог
log_message "Удаление временного каталога секретов: ${tmp_secrets_dir}"
rm -rf "${tmp_secrets_dir}"
fi
log_separator
# 7. Резервное копирование PVC (Заглушка - Требуется отдельная стратегия, например, Velero или бэкап Longhorn)
log_message "Секция резервного копирования PVC - Заглушка."
log_message "Примечание: Резервное копирование данных PVC требует специальной стратегии, такой как Velero (velero.io) или встроенные функции резервного копирования Longhorn."
# Пример использования бэкапа Longhorn (концептуально - требует настройки Longhorn):
# longhorn backup create my-pvc-backup --dest s3://my-backup-bucket/longhorn/
log_separator
# 8. Очистка старых резервных копий
log_message "Очистка старых резервных копий старше ${RETENTION_DAYS} дней в '${BACKUP_DIR}'..."
# Ищем и удаляем старые zip-файлы и файлы журналов, соответствующие шаблонам
# Используем -maxdepth 1, чтобы случайно не удалить файлы во вложенных каталогах
# Обновляем шаблон для лог-файлов: backup-*.log
deleted_files=$(/usr/bin/find "${BACKUP_DIR}" -maxdepth 1 -type f \( -name "etcd-------*.zip" -o -name "manifests--*.zip" -o -name "secrets----*.zip" -o -name "-backup---*.log" \) -mtime +"${RETENTION_DAYS}" -print -delete)
if [[ -n "$deleted_files" ]]; then
log_message "Удалены старые файлы резервных копий:"
echo "$deleted_files" | while IFS= read -r file; do log_message " - $file"; done
else
log_message "Старые файлы резервных копий для удаления не найдены."
fi
log_separator
# 9. Список текущих резервных копий
log_message "Текущие резервные копии в '${BACKUP_DIR}':"
ls -alhcrt "${BACKUP_DIR}" >> "${LOG_FILE}" # Записываем подробный список в журнал
ls -alhcrt "${BACKUP_DIR}" # Показываем список на консоли
log_separator
# 10. Размонтирование и выход (Обрабатывается через trap)
log_message "Процесс резервного копирования завершен. Размонтирование произойдет при выходе."
log_separator
# Явный выход с кодом 0, если мы дошли до сюда без ошибок
exit 0
```
Добавим скрипт в системный cron (root):
```bash
sudo crontab -e
```
Например, добавим в cron запуск скрипта каждый день в 2:10:
```text
# Резервное копирование K3S
10 2 * * * /usr/bin/bash /home/opi/script/backup-k3s.sh
```
## Настройка резервного копирования томов блочного хранилища Longhorn
Наиболее удобный способ резервного копирования томов блочного хранилища Longhorn - встроенная в него панель управления.
О том как настроить доступ к ней из браузера, читайте в заметке [Настройка доступа к панели управления Longhorn](k3s-setting-up-web-access-to-dashboard.md).

127
kubernetes/k3s-chartdb.md Normal file
View File

@@ -0,0 +1,127 @@
# Под с ChartDB
ChatBD ([chartdb.io](https://chartdb.io/)) - это визуальный редактор структуры базы данных и инструмент проектирования
схем (ER-диаграмм), мини-конструктор для работы с локальной псевдо-БД. Все это доступно в веб-интерфейсе. Пока ChatBD
не позволяет подключаться к реальным базам данных, но в будущем планируется поддержка. Зато в нее можно импортировать
схемы рабочих баз данных из PostgreSQL, MySQL, MariaDB, SQLite, MSSQL, ClickHouse и CockroachDB. Так же поддерживается
экспорт из JSON и DBML. Импорт готовых схем возможен в любую из поддерживаемых СУБД (плюс JSON и DBML).
Можно работать с таблицами, колонками, индексами, ключами... но в документации прямо заявлено:
> «Самостоятельная версия не поддерживает удаленные подключения к базе данных или аутентификацию.
> Для полной функциональности используйте chartdb.io».
Но самое печальное, в нем нет никаких инструментов для создания запросов.
_Из интересных фишек ChatDB -- к нему можно подключить LLM (через API OpenAI или локальный Ollama) и тогда он,
похоже, сможет генерировать SQL-запросы по текстовым описаниям. Но это я пока пока не проверил (ждите обновления этой
инструкции)._
Манифест для развертывания пода с ChartDB в k3s, который предоставляет веб-интерфейс по адресу `http://chartdb.local`:
```yaml
# ~/k3s/chartdb/chartdb.yaml
# Все манифесты для ChartDB
# 1. Манифест создания пространства имён `chartdb`. Если оно уже есть — kubectl apply ничего не изменит
apiVersion: v1
kind: Namespace
metadata:
name: chartdb
---
# 2. Манифест PVC (Longhorn) -- том в блочном хранилище, в котором будут храниться данные ChartDB.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: chartdb-data
namespace: chartdb
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 320Mi # Более чем достаточно для хранения схем и данных ChartDB, даже 150Mi хватит
---
# 3. Deployment: развёртывание ChartDB
apiVersion: apps/v1
kind: Deployment
metadata:
name: chartdb
namespace: chartdb
spec:
replicas: 1
selector:
matchLabels:
app: chartdb
template:
metadata:
labels:
app: chartdb
spec:
containers:
- name: chartdb
image: ghcr.io/chartdb/chartdb:latest
ports:
- containerPort: 80
env:
- name: TZ # Часовой пояс, который будет в поде
value: Europe/Moscow
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts: # Монтируем том:
- name: chartdb-storage # ... имя PVC-тома
mountPath: /data # ...путь внутри контейнера, куда будет смонтирован PVC-том
volumes: # Используемые том:
- name: chartdb-storage # ... c именем
persistentVolumeClaim: # ... PVC (Longhorn)
claimName: chartdb-data
---
# 4. Service: внутренний доступ к контейнеру ChartDB
apiVersion: v1
kind: Service
metadata:
name: chartdb
namespace: chartdb
spec:
selector:
app: chartdb
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
# 5. IngressRoute для Traefik (под твою конфигурацию)
# Это публикует ChartDB по адресу http://chartdb.local (заменить на свой домен)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: chartdb
namespace: chartdb
spec:
entryPoints:
- web # это должен быть один из entrypoints в Traefik (обычно "web" = порт 80)
routes:
- match: Host("chartdb.local") # доменное имя, по которому будет доступен сервис
kind: Rule
services:
- name: chartdb
port: 80
```
Применим манифесты командой:
```shell
kubectl apply -f ~/k3s/chartdb/chartdb.yaml
```
После этого ChartDB будет доступен по адресу `http://chartdb.local`. и в нем можно будет создавать и редактировать
схемы, например:
![k3s--chartdb-ui.png](../images/k3s--chartdb-ui.png)

View File

@@ -0,0 +1,255 @@
# Подключение менеджера сертификатов (cert-manager) Let's Encrypt к k3s
№ Установка cert-manager
Установим `cert-manager` из официального манифеста GitHub. Это автоматически добавит все необходимые компоненты,
включая CRD (Custom Resource Definitions) и контроллеры:
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
```
Это установит cert-manager версии 1.15.3 (предварительно надо проверь актуальную версию на GitHub cert-manager).
Компоненты развернутся в namespace cert-manager.
Проверим установку:
```bash
kubectl get pods --namespace cert-manager
```
Увидим, что все поды в статусе `Running`:
```text
NAME READY STATUS RESTARTS AGE
cert-manager-778cb8f45-gxtpf 1/1 Running 0 22m
cert-manager-cainjector-6d5854cdd8-w4d8x 1/1 Running 0 22m
cert-manager-webhook-78b57fb6bb-jljdx 1/1 Running 0 22m
```
Что это за поды:
- `cert-manager` — основной контроллер cert-manager, который управляет созданием и обновлением сертификатов.
- `cert-manager-cainjector` — контроллер, который автоматически добавляет CA-сертификаты в секреты и ресурсы Kubernetes.
- `cert-manager-webhook` — контроллер, который обрабатывает запросы на валидацию и мутацию ресурсов cert-manager.
Проверим, что Custom Resource Definitions (CRDs) для cert-manager созданы:
```bash
kubectl get crd | grep cert-manager
```
Должны быть `certificates.cert-manager.io`, `issuers.cert-manager.io`, `clusterissuers.cert-manager.io и другие:
```text
certificaterequests.cert-manager.io 2025-04-27T16:29:56Z
certificates.cert-manager.io 2025-04-27T16:29:56Z
challenges.acme.cert-manager.io 2025-04-27T16:29:56Z
clusterissuers.cert-manager.io 2025-04-27T16:29:56Z
issuers.cert-manager.io 2025-04-27T16:29:56Z
orders.acme.cert-manager.io 2025-04-27T16:29:57Z
```
# Создание ClusterIssuer (Эмитент сертификатов для всего кластера)
Создадим `ClusterIssuer` для Let's Encrypt. Это позволит cert-manager автоматически запрашивать и обновлять сертификаты.
Разместим его в отдельном файле `~/k3s/cert-manager/letsencrypt-clusterissuer.yaml`:
```bash
mkdir -p ~/k3s/cert-manager
nano ~/k3s/cert-manager/letsencrypt-clusterissuer.yaml
```
И вставим в него следующий код (не забудьте заменить `<my@email.com>` на свой email):
```yaml
apiVersion: cert-manager.io/v1 # Версия API для cert-manager, v1 — текущая стабильная.
kind: ClusterIssuer # Тип ресурса. ClusterIssuer — глобальный объект для выдачи сертификатов
# всему кластеру (в отличие от Issuer, который ограничен namespace).
metadata:
name: letsencrypt-prod # Имя `ClusterIssuer`, на него будем ссылаться в манифестах (например, в Certificate).
spec:
acme: # Используется ACME-протокол (Automated Certificate Management Environment) Let's Encrypt.
server: https://acme-v02.api.letsencrypt.org/directory # URL сервера Let's Encrypt (боевой).
email: <my@email.com> # Логин-клиент Let's Encrypt (раньше так же использовался для уведомлений).
privateKeySecretRef:
name: letsencrypt-prod # Имя секрета, где будет храниться приватные ключи для ACME-аккаунта.
solvers: # Список способов "решения" ACME-задач для подтверждения владения доменом.
- http01: # Используется HTTP-01 challenge (подтверждение через HTTP-запросы).
ingress:
class: traefik # Указывает, что ingress-контроллер Traefik будет обрабатывать HTTP-01 challenge
# (создает временные маршруты для `/.well-known/acme-challenge`) для подтверждения
# владения доменом.
```
Применим манифест:
```bash
kubectl apply -f ~/k3s/cert-manager/letsencrypt-clusterissuer.yaml
```
Проверим, что `ClusterIssuer` создан:
```bash
kubectl get clusterissuer
```
Увидим что-то вроде:
```text
NAME READY AGE
letsencrypt-prod True 27s
```
Проверим статус:
```bash
kubectl describe clusterissuer letsencrypt-prod
```
Увидим что-то вроде:
```text
...
...
Status:
Acme:
Last Private Key Hash: тут-хранится-хэш-приватного-ключа
Last Registered Email: <my@email.com>
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/2365771147
Conditions:
Last Transition Time: 2025-04-27T17:00:56Z
Message: The ACME account was registered with the ACME server
Observed Generation: 1
Reason: ACMEAccountRegistered
Status: True
Type: Ready
...
```
Должны быть `Status: True` и `Reason: ACMEAccountRegistered`, что означает, что `ClusterIssuer` успешно зарегистрирован
и готов к использованию.
# Создание сертификата
Эта общая часть, объясняющая как создавать манифесты для выпуска сертификатов.
В своем манифесте, например, в манифесте пода, создается блок (данный кусочек взят из манифеста пода
[Gitea](k3s-migrating-container-from-docker-to-kubernetes.md) и он находится в пространстве имен `gitea`):
```yaml
---
# Манифест для получения сертификата от Let's Encrypt
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: gitea-tls # Имя сертификата `gitea-tls`
namespace: gitea # В пространстве имен `gitea`
spec:
secretName: gitea-tls # Имя секрета для сертификата
dnsNames: # Доменные имена для сертификата...
- git.cube2.ru
issuerRef: # Эмитент сертификата (Issuer)
name: letsencrypt-prod # Имя ClusterIssuer, который отвечает за получение сертификата: `letsencrypt-prod`
kind: ClusterIssuer # Тип эмитента
```
После применения манифеста, можно проверить статус сертификата:
```bash
kubectl get certificate -A
```
Увидим что-то вроде:
```text
NAMESPACE NAME READY SECRET AGE
ab-shelf audiobookshelf-tls False audiobookshelf-tls 90m
gitea gitea-tls True gitea-tls 66s
```
Тут можно заметить, что сертификат `audiobookshelf-tls` не готов (False), а `gitea-tls` готов (True).
Как проверить статус сертификата и причину его неготовности (на примере `audiobookshelf-tls`):
```bash
kubectl describe certificate audiobookshelf-tls -n ab-shelf
```
Увидим что-то вроде:
```text
Name: audiobookshelf-tls
Namespace: ab-shelf
...
...
...
Spec:
Dns Names:
<тут-будет-доменное-имя-для-которого-выдается-сертификат>
Issuer Ref:
Kind: ClusterIssuer
Name: letsencrypt-prod
Secret Name: audiobookshelf-tls
Status:
Conditions:
Last Transition Time: 2025-04-27T17:13:32Z
Message: The certificate request has failed to complete and will be retried: Failed to wait for order resource "audiobookshelf-tls-1-722111375" to become ready: order is in "errored" state: Failed to create Order: 429 urn:ietf:params:acme:error:rateLimited: too many certificates (5) already issued for this exact set of domains in the last 168h0m0s, retry after 2025-04-28 19:30:09 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-hostnames
Observed Generation: 1
Reason: Failed
Status: False
Type: Issuing
Last Transition Time: 2025-04-27T17:13:29Z
Message: Issuing certificate as Secret does not exist
Observed Generation: 1
Reason: DoesNotExist
Status: False
Type: Ready
...
...
...
```
Как видно, причина неготовности сертификата `audiobookshelf-tls` в том, что уже превышен лимит на количество сертификатов
(пять запросов) для данного доменного имени за последние 168 часов (семь дней). Указано время после которого можно
повторить запрос.
По идее запрос на повторный сертификат будет отправлен автоматически, но это может произойти спустя несколько часов
(три-четыре или более) после разрешенного времени. Кстати, время указывается в UTC, делайте поправку на свой часовой
пояс.
Чтобы ускорить процесс, можно удалить сертификат и создать его заново (на примере `audiobookshelf-tls`):
```bash
kubectl delete certificate audiobookshelf-tls -n ab-shelf
```
А затем повторно принять манифест, в котором у вас находится `kind: Certificate`.
| Заметка |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Пока Let's Encrypt не выдал сертификат, Traefik будет работать по SSL (https) на самоподписанном сертификате. Можно открыть анонимное окно браузера, согласится с предупреждениями безопасности и пользоваться сайтом. |
| |
Когда же все хорошо (на примере `gitea-tls` для домены `git.cube2.ru`, сайт которого вы сейчас читаете):
```bash
kubectl describe certificate gitea-tls -n gitea
```
Увидим что-то вроде:
```text
Name: gitea-tls
Namespace: gitea
...
...
...
Spec:
Dns Names:
git.cube2.ru
Issuer Ref:
Kind: ClusterIssuer
Name: letsencrypt-prod
Secret Name: gitea-tls
Status:
Conditions:
Last Transition Time: 2025-04-27T18:43:02Z
Message: Certificate is up to date and has not expired
Observed Generation: 1
Reason: Ready
Status: True
Type: Ready
Not After: 2025-07-26T17:44:29Z
Not Before: 2025-04-27T17:44:30Z
Renewal Time: 2025-06-26T17:44:29Z
...
...
...
```
Видим что `Status: True`, `Reason: Ready`, а также время дату/время с которого сертификат действителен
(время `Not Before`) и до которого он действителен (время `Not After`), а также дату/время, когда сертификат
будет автоматически обновлен (время `Renewal Time`).
**ВАЖНО**: ремя указывается в UTC, делайте поправку на свой часовой пояс_.

178
kubernetes/k3s-metabase.md Normal file
View File

@@ -0,0 +1,178 @@
# Развертывание Metabase в K3s
Metabase ([metabase.com](https://www.metabase.com/)) — это мощный и дружелюбный инструмент BI (Business Intelligence).
Он работает через веб-интерфейс, позволяет подключаться к реальным БД, делать запросы (SQL и визуальный конструктор),
строить графики, дашборды, отчёты (и отправлять эти отчеты по email-рассылки) и многое другое.
Metabase немного похож на [Power BI](https://www.microsoft.com/en-us/power-platform/products/power-bi/) или
[Tableau](https://www.tableau.com/), но Open-Source.
Поддерживаются основные СУБД: PostgreSQL, MySQL/MariaDB, SQLite, ClickHouse, MongoDB, Microsoft SQL Server, BigQuery
и многие другие.
Metabase — монолит (Java JAR), и работает на сервере (или любом компьютере) как один процесс Java Virtual Machine.
Для хранения своей конфигурации использует встроенную базу данных H2, но его можно подключить и внешней СУБД
(например, PostgreSQL или MariaDB). И главное, для меня, он поддерживает ARM64? (а значит заработает [на моем k3s
на базе Orange Pi 5](../raspberry-and-orange-pi/k3s.md)).
## Подготовка базы данных
В deployment-манифесте Metabase будут указаны параметры подключения к PostgreSQL. Он у меня тоже развернут как под k3s
(см.: [развертывание PostgeSQL в K3s](k3s-postresql.md).
Создадим пользователя PostgreSQL, базу данных и права пользователя для Metabase. Это можно сделать через `psql` или
любой другой клиент PostgreSQL. Нужно выполнить следующие SQL-команды (не забудьте заменить пароль на свой):
```sql
CREATE USER metabase_user WITH ENCRYPTED PASSWORD 'очень-секретный-пароль-123!!';
CREATE DATABASE metabase OWNER metabase_user;
GRANT ALL PRIVILEGES ON DATABASE metabase TO metabase_user;
GRANT ALL ON SCHEMA public TO metabase_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO metabase_user;
```
Что здесь происходит:
- Создаем пользователя `metabase_user` с паролем `очень-секретный-пароль-123!!`.
- Создаем базу данных `metabase` и назначаем владельцем этого пользователя.
- Предоставляем все привилегии на базу данных `metabase` этому пользователю (можно не делать, т.к. мы уже указали владельца).
- Схема public создаётся автоматически в базе, но чтобы metabase_user мог работать с таблицами предоставляем все права
на схему `public` базы данных (это стандартная схема PostgreSQL).:
- Чтобы пользователь мог создавать таблицы, функции и прочее в схеме...
# Манифесты для развертывания Metabase в K3s
У меня Metabase будет доступен по адресу `http://mb.local` (через VIP-адрес Keeepalive). Замените доменное имя на свое,
и не забудьте настроить DNS или файл `/etc/hosts`.
Для развертывания Metabase в k3s нам понадобятся следующие манифесты Kubernetes (не забудьте поменять пароль
пользователя PostgreSQL на свой):
```yaml
# ~/k3s/metabase/metabase.yaml
# Все манифесты для Metabase в k3s
# 1. Namespace: создаём пространство имен `metabase`
apiVersion: v1
kind: Namespace
metadata:
name: metabase
---
# 2. Secret: храним пароль к PostgreSQL в Kubernetes-секрете (это безопаснее, чем указывать его прямо в Deployment)
apiVersion: v1
kind: Secret
metadata:
name: metabase-db-secret
namespace: metabase
type: Opaque
stringData:
MB_DB_PASS: 'очень-секретный-пароль-123!!' # Пароль. Не закодирован, но kubectl хранит его в base64.
---
# 3. PVC: том для временных данных Metabase
# Metabase хранит всё важное в PostgreSQL, но PVC-том нужен для кеша, логов и временных файлов
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: metabase-data
namespace: metabase
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 512Mi # Достаточно для большинства задач
---
# 4. Deployment: контейнер с Metabase (+указываем переменные окружения для подключения к PostgreSQL)
apiVersion: apps/v1
kind: Deployment
metadata:
name: metabase
namespace: metabase
spec:
replicas: 1
selector:
matchLabels:
app: metabase
template:
metadata:
labels:
app: metabase
spec:
containers:
- name: metabase
image: metabase/metabase:latest # ARM64-совместимый образ
ports:
- containerPort: 3000 # Стандартный порт Metabase внутри контейнера
env:
- name: MB_DB_TYPE
value: postgres
- name: MB_DB_DBNAME
value: metabase
- name: MB_DB_PORT
value: "5432" # В кавычках: безопаснее в YAML
- name: MB_DB_USER
value: metabase_user
- name: MB_DB_HOST
value: postgres.postgresql.svc.cluster.local # Берем из Service-монифеста PostgreSQL (или хост `pg.local`, доступный внутри сети)
- name: MB_DB_PASS
valueFrom:
secretKeyRef:
name: metabase-db-secret # Секрет, созданный выше
key: MB_DB_PASS
- name: JAVA_TIMEZONE
value: Europe/Moscow
volumeMounts:
- name: metabase-storage
mountPath: /metabase-data # Временные файлы, кеши, логи
volumes:
- name: metabase-storage
persistentVolumeClaim:
claimName: metabase-data
---
# 5. Сервис: внутренняя точка доступа (не публикуется наружу)
apiVersion: v1
kind: Service
metadata:
name: metabase
namespace: metabase
spec:
selector:
app: metabase
ports:
- port: 80
targetPort: 3000 # Проксируем внешний порт 80 → контейнерный 3000
type: ClusterIP
---
# 6. IngressRoute: Traefik CRD для публикации Metabase по адресу http://mb.local
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: metabase
namespace: metabase
spec:
entryPoints:
- web # HTTP-порт Traefik (обычно 80)
routes:
- match: Host("mb.local")
kind: Rule
services:
- name: metabase
port: 80
```
Применим манифесты командой:
```bash
kubectl apply -f ~/k3s/metabase/metabase.yaml
```
Все должно заработать, и Metabase будет доступен по адресу `http://mb.local` (или по тому доменному имени, которое
вы указали). Если что-то не так, то скорее всего проблемы с подключением к PostgreSQL. Удалите базу, пользователя,
k3s-деплоймент, и создайте заново. Диагностировать причину неполадок можно посмотрев логи пода:
```bash
kubectl -n metabase logs deploy/metabase
```

File diff suppressed because it is too large Load Diff

1251
kubernetes/k3s-postresql.md Normal file

File diff suppressed because it is too large Load Diff

358
kubernetes/k3s-proxy.md Normal file
View File

@@ -0,0 +1,358 @@
# Проксирование внешнего хоста через Traefik (Ingress-контроллер)
У меня в домашней сети есть хост с web-сервисом (audiobookshelf), а стандартные web-порты (80 для HTTP и 443 для HTTPS)
на домашнем роутере перенаправлены в кластер k3S (через keepalived). Таким образом, если прокинуть http-трафик этого
хоста через Traefik, то можно будет получить доступ к этому сервису через доменное имя и SSL-сертификат от
Lets Encrypt.
Для удобства я поместил все манифесты в один файл (вы можете оформить из и как отдельные файлы). Так как хотелось бы
описать как делаются универсальные манифесты, которые можно использовать для проксирования любого сервиса, то
я заменил в нем конкретные значения на "заглушки. Можно взять этот манифест и просто заменить в нем значения на
свои:
* `<PROXIED-HOST>` -- IP-адрес хоста, где работает сервис, который надо проксировать.
* `<PROXIED-PORT>` -- порт, с которого отвечает сервис.
* `<YOU-DOMAIN-NAME>` -- доменное имя, на которое будет проксировать сервис.
* `<NAME-SPACE>` -- пространство имен кластера, в котором будет создан сервис, маршруты, секреты и все необходимое
для проксирования. Пространство имен -- это логическая группа ресурсов в кластере Kubernetes, которая позволяет
организовать и изолировать ресурсы.
* `<SERVICE-NAME>` -- имя сервиса, который будет проксироваться. Это имя, для простоты, будем использоваться
* и в маршрутах, и сертификатах, и в секрете...
## Пространство имен
Чтобы все было аккуратно и сервисы и поды не путались, создадим пространство имен для проксирования конкретного хоста.
Мо хост относится к <SERVICE-NAME>, поэтому назову пространство имен `<NAME-SPACE>`
Например, `<NAME-SPACE>`.
```bash
sudo ubectl create namespace <NAME-SPACE>
```
Проверяем, что пространство создано:
```bash
sudo kubectl get namespace <NAME-SPACE>
```
Увидим, что пространство создано и активно:
```text
NAME STATUS AGE
<NAME-SPACE> Active 54s
```
## Конфигурация всего
Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифест:
```bash
sudo nano ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
```
Он состоит из следующих частей:
* `Endpoints` -- указывает на конечную точку, в которую будет проксироваться трафик. В данном случае это
IP-адрес и порт, на который будет проксироваться запрос.
* `Service` -- создает сервис, который будет использоваться для проксирования запросов к `Endpoints`.
* `Certificate` -- создает сертификат для домена `<YOU-DOMAIN-NAME>`, который будет использоваться для
шифрования трафика. Сертификат запрашивается через `cert-manager`, который автоматически обновляет его по мере
необходимости.
* `Middleware` -- создает промежуточное ПО, которое будет использоваться для обработки запросов. В данном случае
это редирект с HTTP на HTTPS и исключение редиректа для ACME challenge (механизма внешней проверки владения доменом
со стороны Lets Encrypt).
* `IngressRoute` -- создает маршрут, который будет использоваться для проксирования запросов к сервису.
В данном случае это маршруты для HTTP и HTTPS, которые будут обрабатывать запросы на домен `<YOU-DOMAIN-NAME>`.
Также создается маршрут для ACME challenge, который позволяет cert-manager пройти проверку через порт 80.
Вставляем в манифест следующее содержимое (не забудьте заменить `<PROXIED-HOST>`, `<PROXIED-PORT>`, `<YOU-DOMAIN-NAME>`,
`<NAME-SPACE>` и `<SERVICE-NAME>` на свои значения):
```yaml
# Endpoints для внешнего хоста <SERVICE-NAME>
# Задаёт IP и порт внешнего сервера, так как <SERVICE-NAME> внешний хост для k3s
apiVersion: v1
kind: Endpoints
metadata:
name: <SERVICE-NAME>
namespace: <NAME-SPACE> # Namespace для <SERVICE-NAME>
subsets: # Прямо в корне, без spec
- addresses:
- ip: <PROXIED-HOST> # IP Synology, где работает <SERVICE-NAME>
ports:
- port: <PROXIED-PORT> # Порт Synology (HTTP)
protocol: TCP
---
# Service для маршрутизации трафика от Traefik к внешнему хосту
# Связывает IngressRoute с Endpoints
apiVersion: v1
kind: Service
metadata:
name: <SERVICE-NAME>
namespace: <NAME-SPACE>
spec:
ports:
- port: <PROXIED-PORT> # Порт сервиса, на который Traefik отправляет трафик
targetPort: <PROXIED-PORT> # Порт на Synology
protocol: TCP
---
# Middleware для редиректа HTTP → HTTPS
# Применяется к HTTP-запросам для перенаправления на HTTPS
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: https-redirect
namespace: <NAME-SPACE>
spec:
redirectScheme:
scheme: https # Перенаправлять на HTTPS
permanent: true # Код 301 (постоянный редирект)
---
# Middleware для исключения редиректа на ACME challenge
# Позволяет Lets Encrypt проверять /.well-known/acme-challenge без HTTPS
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: no-https-redirect
namespace: <NAME-SPACE>
spec:
stripPrefix:
prefixes:
- /.well-known/acme-challenge # Убрать префикс для ACME
---
# IngressRoute для HTTP (порт 80) с редиректом на HTTPS
# Обрабатывает HTTP-запросы и перенаправляет их
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: <SERVICE-NAME>-http
namespace: <NAME-SPACE>
spec:
entryPoints:
- web # Порт 80 (стандартный HTTP)
routes:
- match: Host("<YOU-DOMAIN-NAME>") # Запросы к <YOU-DOMAIN-NAME>
kind: Rule
services:
- name: <SERVICE-NAME> # Сервис <SERVICE-NAME>
port: <PROXIED-PORT> # Порт сервиса
middlewares:
- name: https-redirect # Редирект на HTTPS
---
# IngressRoute для HTTPS (порт 443)
# Обрабатывает HTTPS-запросы с TLS
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: <SERVICE-NAME>-https
namespace: <NAME-SPACE>
spec:
entryPoints:
- websecure # Порт 443 (HTTPS)
routes:
- match: Host("<YOU-DOMAIN-NAME>")
kind: Rule
services:
- name: <SERVICE-NAME>
port: <PROXIED-PORT>
tls:
secretName: <SERVICE-NAME>-tls # Сертификат от cert-manager
---
# IngressRoute для HTTP-01 challenge (Lets Encrypt)
# Позволяет cert-manager пройти проверку через порт 80
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
# apiVersion: traefik.containo.us/v1alpha1 # старая версия Traefik
kind: IngressRoute
metadata:
name: <SERVICE-NAME>-acme
namespace: <NAME-SPACE>
spec:
entryPoints:
- web # Порт 80
routes:
- match: Host("<YOU-DOMAIN-NAME>") && PathPrefix("/.well-known/acme-challenge")
kind: Rule
services:
- name: <SERVICE-NAME>
port: <PROXIED-PORT>
middlewares:
- name: no-https-redirect # Не редиректить ACME
---
# Certificate для TLS-сертификата от Lets Encrypt
# Запрашивает сертификат для <YOU-DOMAIN-NAME> через cert-manager
# ВАЖНО: cert-manager должен быть установлен в кластере
# ВАЖНО: если манифесты принимаются (apply) последовательно, то манифест с сертификатом должен быть последним для
# избежания исчерпания лимитов Lets Encrypt (пять запросов в неделю)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: <SERVICE-NAME>-tls
namespace: <NAME-SPACE>
spec:
secretName: <SERVICE-NAME>-tls # Имя секрета для сертификата
dnsNames:
- <YOU-DOMAIN-NAME> # Домен для сертификата
issuerRef:
name: letsencrypt-prod # ClusterIssuer для Lets Encrypt
kind: ClusterIssuer
```
**ВАЖНО**: В манифесте используется letsencrypt-prod для получения сертификата от Lets Encrypt. Это нестандартный
ClusterIssuer cert-manager, создание которого описано в [документации](https://cert-manager.io/docs/usage/ingress/#tls-termination)
и [отдельной инструкции](k3s-custom-container-deployment.md#создание-clusterissuer)
(возможно, вам нужно будет создать его отдельно). Если вы используете другой ClusterIssuer, то замените letsencrypt-prod
на имя вашего ClusterIssuer в секции `issuerRef` в манифесте.
## Применяем манифест
```bash
sudo kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
```
Проверяем, что все ресурсы создались:
```bash
sudo kubectl get all -n <NAME-SPACE>
sudo kubectl get ingressroute -n <NAME-SPACE>
sudo kubectl get middleware -n <NAME-SPACE>
sudo kubectl get service -n <NAME-SPACE>
sudo kubectl get endpoints -n <NAME-SPACE>
```
Увидим что-то вроде:
```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/<SERVICE-NAME> ClusterIP 10.43.152.59 <none> <PROXIED-PORT>/TCP 3h
```
Для IngressRoute:
```text
NAME AGE
<SERVICE-NAME> 3h
<SERVICE-NAME>-acme 1h
<SERVICE-NAME>-http 1h
<SERVICE-NAME>-https 1h
```
Для Middleware:
```text
NAME AGE
https-redirect 1h
no-https-redirect 1h
```
Для Service:
```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
<SERVICE-NAME> ClusterIP 10.43.152.59 <none> <PROXIED-PORT>/TCP 3h
```
Для Endpoints:
```text
NAME ENDPOINTS AGE
<SERVICE-NAME> <PROXIED-HOST>:<PROXIED-PORT> 3h
```
Проверяем, что сертификат создан:
```bash
sudo kubectl describe certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
sudo kubectl get secret -n <NAME-SPACE> <SERVICE-NAME>-tls
```
## UPD
Почему-то SSL-сертификат Let's Encrypt через положенный срок не перевыпустился. Для начала выяснилось, что на
моем роутере перестал работать NAT-loopback, и внутрення проверка Certbot перестала работать. Похоже роутер получил
обновление прошивки, и все сломалось. В результате DNS кластера (coredns) разрешал домен во внешний IP-адрес роутера,
а не во внутренний IP-адрес кластера (VIP через keepalived), а без NAT-loopback запросы к домену не доходили до
кластера.
Как чинил:
Во-первых, руками поменял на всех узлах кластера (включая роутер) в файле `/etc/resolv.conf` IP-адрес DNS.
Обновление испортило и DHCP-сервер на роутере, и он стал раздавать свой IP-адреса как DNS-сервер, вместо моего
домшнего DNS-сервера (работат на Synology NAS).
Во-вторых, в кластере k3s обновил конфигурацию coredns:
```bash
kubectl edit configmap -n kube-system coredns
```
Отчего-то там было навернуты хосты 'hosts /etc/coredns/NodeHosts` внутри `.:53 ` блока, и это мешало. В результате
получалось следующее:
```yaml
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
}
import /etc/coredns/custom/*.server
...
...
...
```
После сохранения конфигурации манифет coredns (т.к. его редактируем пряма в кластере) применяется автоматически.
После пеускаем coredns:
```bash
kubectl rollout restart deployment -n kube-system coredns
```
Проверяем логи:
```bash
kubectl logs -n kube-system -l k8s-app=kube-dns
```
Проверяем статусы подов CoreDNS:
```shell
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide
```
Поды должны быть в статусе `Running`:
```text
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-646d9c4784-wsbbg 1/1 Running 0 58m 10.42.2.127 opi5plus-1 <none> <none>
coredns-646d9c4784-z5zqv 1/1 Running 0 58m 10.42.0.125 opi5 <none> <none>
```
И наконец, в-третьих, обновил сертификат Let's Encrypt. Сначала удалил старый сертификат и секрет:
```bash
kubectl delete certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
kubectl delete secret -n <NAME-SPACE> <SERVICE-NAME>-tls
```
И снова применил манифест с сертификатом:
```bash
kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
```
Проверяем, что сертификат обновился:
```bash
kubectl describe certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
kubectl get secret -n <NAME-SPACE> <SERVICE-NAME>-tls
```

View File

@@ -0,0 +1,302 @@
# Настройка доступа к панелям управления Lobghorn и Traefik
В k3s есть несколько панелей управления, которые можно настроить для доступа извне кластера.
# Панель управления блочный хранилищем (Persistent Volume) Longhorn
Панель управления Longhorn позволяет управлять блочными хранилищами (Persistent Volume) в k3s. Полезнейшая вещь!
Через нее можно не только отслеживать работу тома, но и создавать, удалять и изменять PVC-хранилища, и, главное,
делать бэкапы томов и восстанавливать их.
Проверим, поды Longhorn, и в нем есть UI (интерфейс управления):
```shell
kubectl get pod -n longhorn-system
```
Увидим что-то вроде:
```text
NAME READY STATUS RESTARTS AGE
...
...
longhorn-ui-f7ff9c74-7bbsw 1/1 Running 2 (26h ago) 21h
longhorn-ui-f7ff9c74-b4svq 1/1 Running 3 (26h ago) 21h
...
```
Поды longhorn-ui работают -- интерфейс управления Longhorn -- доступен.
Проверим доступные сервисы Longhorn:
```shell
kubectl get svc -n longhorn-system
```
Увидим что-то типа:
```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
longhorn-admission-webhook ClusterIP 10.43.123.226 <none> 9502/TCP 21h
longhorn-backend ClusterIP 10.43.226.46 <none> 9500/TCP 21h
longhorn-conversion-webhook ClusterIP 10.43.243.121 <none> 9501/TCP 21h
longhorn-frontend ClusterIP 10.43.152.91 <none> 80/TCP 21h
longhorn-recovery-backend ClusterIP 10.43.205.78 <none> 9503/TCP 21h
```
Как видим, есть работающий сервис `longhorn-frontend` с типом `ClusterIP` (внутренний IP-адрес кластера) и портом 80. Это
и есть интерфейс управления Longhorn. Проверим, что он доступен по этому адресу:
```shell
curl -v http://10.43.152.91
```
Увидим что-то вроде:
```text
* Trying 10.43.152.91:80...
* Connected to 10.43.152.91 (10.43.152.91) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.43.152.91
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.21.5
...
...
```
Как видим, Longhorn доступен, и выдает 200 OK.
### Манифес IngressRoute для доступа к панели управления Longhorn
Я настраиваю панель управления Longhorn на доступ по адресу `pvc.local` (достигается через соответствующий DNS-запись
в локальном DNS-сервере или редактированием файла `/etc/hosts`). Создадим IngressRoute-манифест для доступа
к дашборду Longhorn по домену `pvc.local` (или какому вы там сами пожелаете).
```yaml
# IngressRoute-манифест, для доступа к панели управления Longhorn по адресу http://pvc.local
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: longhorn-ui # имя ресурса (пода)
namespace: longhorn-system
spec:
entryPoints:
- web
routes:
- match: Host("pvc.local") # маршрутизируем запросы с хоста pvc.local
kind: Rule
services:
- name: longhorn-frontend # целевой сервис
port: 80
```
Что туту происходит:
* `apiVersion: traefik.io/v1alpha1` — стандартный CRD для Traefik в k3s.
* `kind: IngressRoute`ресурс Traefik для маршрутизации.
* `metadata`:
* `name: longhorn-ui` — имя ресурса (пода) longhorn-ui.
* `namespace: longhorn-system` — в пространстве имен longhorn-system.
* `spec:`
* `entryPoints: web` — используем порт 80 в сервисе traefik
* `routes:` — маршруты.
* `match: Host("pvc.local")` — маршрутизируем запросы с хоста `pvc.local`.
* `kind: Rule` — правило маршрутизации.
* `services:`
* `name: longhorn-frontend` — целевой сервис.
* `port: 80` — порт на котором работает сервис longhorn-frontend.
Применим манифест и проверим, что он применился:
```shell
kubectl apply -f <путь_к_файлу_санифестом>
kubectl get ingressroute -n longhorn-system
```
Увидим что-то вроде:
```text
NAME AGE
longhorn-ui 124m
```
Проверим, что панель управления Longhorn доступна по адресу `pvc.local`:
```shell
curl -v http://pvc.local
```
Увидим что-то вроде:
```text
* Trying <IP>:80...
* Connected to pvc.local (<IP>) port 80 (#0)
> GET / HTTP/1.1
> Host: pvc.local
> User-Agent: curl/7.81.0
> Accept: */*
> GET / HTTP/1.1
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.21.5
```
Как видим, Longhorn теперь доступен по адресу `pvc.local` и выдает 200 OK.
Можно открыть в браузере `http://pvc.local` и увидеть панель управления Longhorn:
![k3s--lonhorn-ui.png](../images/k3s--longhorn-ui.png)
### Изменение числа реплик Longhorn (не обязательно)
Если у вас всего одна нода, то в панели управления Longhorn вы можете увидеть, что тома находятся в состоянии
`degraded` (деградированное). Это связано с тем, что Longhorn не может создать реплики на других нодах, так как их нет.
Исправить это можно, изменив число глобальное число реплик Longhorn с 3 до 1. Это можно сделать через команду:
```shell
kubectl edit settings.longhorn.io -n longhorn-system default-replica-count
```
Найти и отредактировать:
```yaml
value: "3"
```
Тоже самое, кстати, можно проделать и через панель управления Longhorn **"Setting" → "General" → "Default Replica Count"**.
Это изменит число реплик для вновь создаваемых томов, но не изменит значение для уже существующих (они останутся
с тремя репликами, пока не измените вручную в UI). Измените в панели управления Longhorn число реплик для каждого
тома с 3 до 1, и все тома перейдут в состоянии `healthy` (здоровое).
## Панель управления Traefik
Дашборд Traefik позволяет визуализировать маршрутизацию и состояние сервисов. Не так чтоб сильно полезная вещь,
но с ней можно поиграться и к ней есть [https://plugins.traefik.io/plugins](множество плагинов и расширений).
Я настраиваю панель управления Traefik на доступ по адресу `traefik.local` (достигается через соответствующий DNS-запись
в локальном DNS-сервере или редактированием файла `/etc/hosts`).
### Изменение конфигурации Traefik (через Helm)
По умолчанию панель управления Traefik недоступна извне кластера. Чтобы это исправить, нужно создать нужно изменить
конфигурацию Traefik, чтобы проверить, что панель управления включена и разрешить доступ к ней по HTTP. Это можно
сделать через Helm, используя HelmChartConfig. Если у вас уже есть манифест HelmChartConfig для traefik, то просто
добавьте в него в блок `spec: valuesContent: additionalArguments:` дополнительные аргументы: `--api.dashboard=true` и
`--api.insecure=true`.
Если у вас нет HelmChartConfig, то создайте его:
```shell
mkdir -p ~/k3s/traefik
nano ~/k3s/traefik/traefik-helm-config.yaml
```
И вставьте в него следующее содержимое:
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |
additionalArguments:
- --api.dashboard=true # включает панель управления (dashboard) Traefik (обычно он уже включен)
- --api.insecure=true # разрешает доступ к dashboard Traefik по HTTP
```
Применим манифест:
```shell
kubectl apply -f ~/k3s/traefik/traefik-helm-config.yaml
```
Пезезапустим Traefik, чтобы изменения вступили в силу:
```shell
kubectl rollout restart deployment -n kube-system traefik
```
### Создание IngressRoute для доступа к панели управления Traefik с http
Создадим манифест IngressRoute для доступа к панели управления Traefik по домену `traefik.local` (или какому вы там
сами пожелаете):
```yaml
# IngressRoute-манифест, для доступа к панели управления Traefik по адресу http://traefik.local
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: kube-system
spec:
entryPoints:
- web
routes:
- match: Host("traefik.local") && PathPrefix("/dashboard") # доступ к панели управления
kind: Rule
services:
- name: api@internal # имя встроенного в k3s сервиса Traefik для доступа к панели управления
kind: TraefikService # тип сервиса
- match: Host("traefik.local") && PathPrefix("/api") # доступ к API
kind: Rule
services:
- name: api@internal
kind: TraefikService
- match: Host("traefik.local") && Path("/") # переадресация чтобы не вызывать по полному пути (`/dashboard`)
kind: Rule
services:
- name: api@internal
kind: TraefikService
```
Применим манифест и проверим, что он применился:
```shell
kubectl get ingressroute -n kube-system
```
Увидим что-то вроде:
```text
NAME AGE
traefik-dashboard 4m
```
Проверим, что панель управления Traefik доступна по адресу `traefik.local`:
```shell
curl -v http://traefik.local/dashboard/
```
Увидим что-то вроде:
```text
Trying <IP>:80...
* Connected to traefik.local (<IP>) port 80 (#0)
> GET /dashboard/ HTTP/1.1
> Host: traefik.local
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Security-Policy: frame-src 'self' https://traefik.io https://*.traefik.io;
...
...
```
Как видим, статус 200 OK, значит панель доступна и работает.
```shell
curl -v http://traefik.local/
```
Увидим что-то вроде:
```text
Trying <IP>:80...
* Connected to traefik.local (<IP>) port 80 (#0)
> GET / HTTP/1.1
> Host: traefik.local
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Location: /dashboard/
< Date: Sat, 03 May 2025 11:59:19 GMT
< Content-Length: 0
```
Как видим, статус 302 Found, значит переадресация тоже работает.
Откроем в браузере `http://traefik.local/dashboard/` и видим панель управления Traefik:
![k3s--traefik-dashboard.png](../images/k3s--traefik-dashboard.png)

View File

@@ -23,8 +23,8 @@
│ └── cclient-shadowsocks--izmir/ # Локация Измир
│ ├── config.yaml
│ └── deployment.yaml
├── ...
└── ...
├──
└──
```
Создаем файл `config.yaml` для первого Shadowsocks-клиента (Москва):
@@ -61,7 +61,7 @@ data:
- `data:` — данные конфигурации.
- `config.json:` — имя файла, в который будет записан конфиг.
- `|` — говорит, что дальше будет многострочный текст.
- `{...}` — Собственно JSON-конфигурация нашего Shadowsocks-клиента.
- `{}` — Собственно JSON-конфигурация нашего Shadowsocks-клиента.
- `server` и `server_port` — адрес и порт нашего VPS.
- `local_address` и `local_port` — где будет SOCKS5 внутри кластера.
- `password` и `method` — пароль и метод шифрования. Метод шифрования `chacha20-ietf-poly1305` -- используется,
@@ -150,15 +150,15 @@ sudo k3s kubectl get pods -n kube-system
Увидим что-то типа:
```text
NAME READY STATUS RESTARTS AGE
...
...
shadowsocks-client-moscow-54d64bf5f4-trb6p 1/1 Running 0 24m
...
```
Можно проверь логи:
```bash
sudo k3s kubectl logs -n kube-system shadowsocks-client-stockholm-54d64bf5f4-trb6p
sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-54d64bf5f4-trb6p
```
Увидим, что клиент shadowsocks запустился:
@@ -232,10 +232,10 @@ sudo k3s ctr images ls | grep shadowsocks
Увидим что-то типа:
```text
...
docker.io/library/shadowsocks-with-tools:latest application/vnd.oci.image.manifest.v1+json sha256:... 22.5 MiB linux/arm64 io.cri-containerd.image=managed
...
...
docker.io/library/shadowsocks-with-tools:latest application/vnd.oci.image.manifest.v1+json sha256: 22.5 MiB linux/arm64 io.cri-containerd.image=managed
```
Теперь нам нужно передать образ контейнера на другие ноды кластера. Как это сделать есть заметка "[Развертывание
@@ -246,26 +246,26 @@ docker.io/library/shadowsocks-with-tools:latest application/vnd.oci.image.ma
новый образ. Закомментируем строку `image: shadowsocks/shadowsocks-libev:latest` и вставим две строки после неё
(обратите внимание на заметки):
```yaml
...
spec:
containers:
- name: shadowsocks-client
# image: shadowsocks/shadowsocks-libev:latest
image: shadowsocks-with-tools # Без :latest, чтобы k3s не "ходил" за контейнером в реестр (например, DockerHub)
imagePullPolicy: Never # Только локальный образ, не тянуть из реестра
...
...
```
Уберём старый под из deployment и удалим сам под из k3s:
```bash
sudo k3s kubectl delete deployment -n kube-system shadowsocks-client-stockholm
sudo k3s kubectl delete pod -n kube-system -l app=shadowsocks-client-stockholm --force --grace-period=0
sudo k3s kubectl delete deployment -n kube-system shadowsocks-client-moscow
sudo k3s kubectl delete pod -n kube-system -l app=shadowsocks-client-moscow --force --grace-period=0
```
Запустим новый под с нашим новым образом:
```bash
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--stockholm/deployment.yaml
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--v/deployment.yaml
```
Проверим, что под запустился, посмотрев статус:
@@ -276,9 +276,9 @@ sudo k3s kubectl get pods -n kube-system
Увидим что-то типа:
```text
NAME READY STATUS RESTARTS AGE
...
shadowsocks-client-moscow-6cf7b956b8-mtsg4 1/1 Running 0 9s
...
```
#### Проверка работы Shadowsocks
@@ -344,12 +344,12 @@ sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-<hash>
Для этого нужно изменить _local_address_ в конфиге shadowsocks-клиента `config.yaml`:
```yaml
...
"server_port": <ПОРТ>,
# "local_address": "127.0.0.1",
"local_address": "0.0.0.0",
"local_port": 1081,
...
```
Применим конфиг:
@@ -437,9 +437,9 @@ sudo k3s kubectl get service -n kube-system
Увидим что-то типа:
```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
...
ss-moscow-service ClusterIP 10.43.236.81 <none> 1081/TCP,1081/UDP 5m5s
...
```
Теперь другие поды могут обращаться к `ss-moscow-service.kube-system.svc.cluster.local:1081` как к SOCKS5-прокси.
@@ -517,6 +517,16 @@ curl --socks5 <IP_УЗЛА>:31081 http://ifconfig.me
Увидим IP-адрес нашего VPS.
### Досутп из хостов домашней сети к прокси
Так как мы уже [настроили Keepalived при установке k3s](../raspberry-and-orange-pi/k3s.md), а socks5-прокси
доступен на хосте любого узла кластера, то автоматически socks5-прокси будет доступен и через VIP
(виртуальный IP-адрес). Поэтому можно использовать VIP-адрес кластера, а не IP-адрес конкретного узла кластера.
Проверим, что это сработало:
```bash
curl --socks5 <VIP>:31081 http://ifconfig.me
```

View File

@@ -0,0 +1,362 @@
# Кастомная страница ошибки 404 (и других) в Traefik
Страницы ошибок Traefik по умолчанию выглядят скучно. Это даже не страницы, а просто текстовые сообщения. Например,
404 выглядит как `404 page not found`. Это позволяет Traefik быть лёгким и быстрым.
Если хочется сделать страницы статусоы 4xx и 5xx более привлекательными, то кастомизация страниц ошибок — отличная идея!
Для каждого HTTP-сервиса внутри можно сделать свои страницы ошибок на уровне приложения (как например, на Gitea,
на которой ты сейчас сидишь). И это наиболее правильный способ. Но если http-запрос не привязан ни к какому сервису,
и Traefik не знает куда его отправить, то он выдаёт свои страницы ошибок. Например, при обращении по IP.
Traefik позволяет кастомизировать страницы ошибок через **middleware** типа `errors`, который перенаправляет запросы
с определёнными кодами ошибок (например, 404) на кастомный сервис, возвращающий нужную html-страницу. И все это
излишество в `k3s` нужно настраивать глобально, с помощью `Middleware` и `IngressRoute`, и применять ко всем маршрутам.
Чтобы кастомная страница 404 работала для всех запросов в кластере, нужно:
# Создать сервис, который возвращает кастомную страницу (например, контейнер с Nginx или простой HTTP-сервер).
# Настроить middleware `errors` для перехвата ошибок 404.
# Применить middleware глобально через `IngressRoute` или конфигурацию Traefik.
Самый простой подход — развернуть лёгкий контейнер (например, Nginx) с HTML-файлом для страницы 404 и настроить
Traefik для перенаправления ошибок на этот контейнер. Ну или, как альтернатива, использовать внешний сервис (по URL),
но это сложнее для глобальной настройки, и создаст зависимость от этого URL.
#### 2. План действий
- Создать кастомную страницу 404 (HTML-файл).
- Развернуть контейнер с Nginx, который будет отдавать эту страницу.
- Настроить Traefik middleware `errors` для перехвата 404.
- Применить middleware глобально для всех маршрутов в `k3s`.
- Проверить результат.
---
### Настройка кастомной страницы 404
#### 1. Создать кастомную страницу 404
- Создай HTML-файл для страницы 404 на ноде `opi5`:
```bash
mkdir -p ~/k3s/error-pages
cat > ~/k3s/error-pages/404.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { color: #ff5555; }
p { font-size: 18px; }
</style>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
</body>
</html>
EOF
```
#### 2. Развернуть Nginx для отдачи страницы
- Создай манифест для Nginx, который будет отдавать `404.html`:
```bash
cat > ~/k3s/error-pages/error-pages.yaml <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: error-pages
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: error-pages
namespace: error-pages
spec:
replicas: 1
selector:
matchLabels:
app: error-pages
template:
metadata:
labels:
app: error-pages
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: error-pages
mountPath: /usr/share/nginx/html
volumes:
- name: error-pages
configMap:
name: error-pages
---
apiVersion: v1
kind: Service
metadata:
name: error-pages
namespace: error-pages
spec:
selector:
app: error-pages
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: error-pages
namespace: error-pages
data:
404.html: |
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { color: #ff5555; }
p { font-size: 18px; }
</style>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
</body>
</html>
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
```
- Проверь поды и сервис:
```bash
kubectl get pod -n error-pages
kubectl get svc -n error-pages
```
#### 3. Настроить Traefik middleware для ошибок
- Создай манифест для middleware `errors`:
```bash
cat > ~/k3s/traefik/error-middleware.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: error-pages
namespace: kube-system
spec:
errors:
status:
- "404"
service:
name: error-pages
namespace: error-pages
port: 80
query: /404.html
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
```
- Проверь:
```bash
kubectl get middleware -n kube-system
```
#### 4. Применить middleware глобально
- В `k3s` дефолтный Traefik обрабатывает маршруты через `IngressRoute` или `Ingress`. Чтобы middleware применялся ко всем маршрутам, нужно либо:
- Добавить middleware к каждому `IngressRoute` вручную.
- Настроить Traefik для глобального применения middleware через `defaultMiddlewares`.
- Для простоты создадим глобальный `IngressRoute` для всех доменов:
```bash
cat > ~/k3s/traefik/global-error-route.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: global-error-route
namespace: kube-system
spec:
entryPoints:
- web
- websecure
routes:
- match: HostRegexp(`{host:.+}`)
kind: Rule
services:
- name: noop
namespace: kube-system
port: 9999
middlewares:
- name: error-pages
namespace: kube-system
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
- **Примечание**:
- Сервис `noop:9999` — это заглушка, так как `IngressRoute` требует сервис, но middleware `errors` перехватит 404 до обращения к сервису.
- Это обеспечивает, что любой запрос с кодом 404 (для любого домена) будет перенаправлен на `error-pages`.
#### 5. Проверить кастомную страницу 404
- Попробуй открыть несуществующий путь:
```bash
curl -v https://git.cube2.ru/nonexistent
```
- Ожидаемый ответ:
- Код: `404 Not Found`.
- HTML-страница:
```html
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
```
- Проверь для другого домена (например, `Bitwarden`):
```bash
curl -v https://<bitwarden-domain>/nonexistent
```
- Проверь логи Traefik:
```bash
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik | tail -n 20
```
#### 6. (Опционально) Настроить другие ошибки
- Чтобы добавить кастомные страницы для других кодов (например, 403, 500), обнови middleware:
```bash
cat > ~/k3s/traefik/error-middleware.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: error-pages
namespace: kube-system
spec:
errors:
status:
- "403"
- "404"
- "500-503"
service:
name: error-pages
namespace: error-pages
port: 80
query: /{status}.html
EOF
```
- Создай дополнительные файлы (`403.html`, `500.html`) в `ConfigMap`:
```bash
kubectl edit configmap -n error-pages error-pages
```
- Добавь, например:
```yaml
403.html: |
<!DOCTYPE html>
<html>
<head><title>403 Forbidden</title></head>
<body><h1>403 - Forbidden</h1><p>Access denied!</p></body>
</html>
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
kubectl delete pod -n error-pages -l app=error-pages
```
---
### Ответ на твой вопрос
> По умолчанию Traefik выдаёт скучные текстовые страницы для 404 и других ошибок. Как сделать кастомные 404 для всего `k3s` без привязки к домену? Где они лежат, и есть ли простой способ их переопределить?
- **Где лежат дефолтные страницы**:
- Они встроены в бинарник Traefik и генерируются как текст (не HTML), например: `404 page not found`.
- Физически их нет в виде файлов в кластере.
- **Почему не HTML**:
- Traefik использует текстовые ответы для минимизации ресурсов.
- **Как переопределить**:
- Использовать middleware `errors`, который перенаправляет ошибки (например, 404) на кастомный сервис с HTML-страницей.
- Развернуть контейнер (например, Nginx) с кастомной страницей.
- Настроить глобальный `IngressRoute` для применения middleware ко всем доменам.
- **Простой способ**:
1. Создать `ConfigMap` с HTML-файлом (`404.html`).
2. Развернуть Nginx в namespace `error-pages` для отдачи страницы.
3. Настроить middleware `errors` для перехвата 404.
4. Применить middleware через глобальный `IngressRoute`.
- **Для всего `k3s`**:
- Глобальный `IngressRoute` с `HostRegexp` перехватывает все запросы и применяет middleware `errors` для ошибок 404.
---
### Рекомендации
1. Создать и применить страницу 404:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
```
2. Настроить middleware:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
```
3. Применить глобальный маршрут:
```bash
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
4. Проверить:
```bash
curl -v https://git.cube2.ru/nonexistent
curl -v https://<bitwarden-domain>/nonexistent
```
5. Проверить логи:
```bash
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik | tail -n 20
```
6. (Опционально) Добавить другие ошибки:
- Обновить `ConfigMap` и middleware для 403, 500 и т.д.
---
### Итог
Дефолтные страницы ошибок Traefik — это встроенные текстовые ответы, которые можно переопределить с помощью middleware `errors` и кастомного сервиса (например, Nginx с HTML-страницей). Для глобальной настройки в `k3s` мы развернули контейнер с `404.html`, настроили middleware для перехвата ошибок 404, и применили его ко всем доменам через `IngressRoute` с `HostRegexp`. Это простой и универсальный способ сделать страницы ошибок яркими и весёлыми! 😄 Теперь твои 404 будут выглядеть стильно, и ты можешь добавить такие же для других ошибок.
**Действия**:
1. Применить:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
2. Проверить:
```bash
curl -v https://git.cube2.ru/nonexistent
```
**Напиши**:
1. Получилась ли кастомная страница 404? (`curl -v https://git.cube2.ru/nonexistent`)
2. Работает ли для других доменов? (`curl -v https://<bitwarden-domain>/nonexistent`)
3. Хочешь настроить страницы для других ошибок (403, 500)?
Теперь можно расслабиться и наслаждаться яркими страницами ошибок! 🚀

208
kubernetes/k3s_smokeping.md Normal file
View File

@@ -0,0 +1,208 @@
# Контейнер со smokeping в k3s
Полная жопа с домашним интеренетом? А поддержка провайдера, типа МГТС или Ростелеком, отвечает: "Проблем на линии нет,
у нас все работает"? А может быть, вы хотите следить за доступностью и сетевыми задержками в доступности своих серверов
в разных локациях, или в домашней сети?.. Smokeping -- это то, что вам нужно! Не такой громоздкий, как Zabbix,
и не такой сложный в настройке, как Grafana (хотя к Grafana его тоже можно подключить), и главное супер-компактный
и легкий.
И так, все манифесты в одном файле. Только поменяйте в манифесте `smokeping-config` меню, название и IP-адреса
на свои, и укажите на какое доменное имя публикуете веб-панель (у меня -- http://sp.local). Ну и читайте пояснения
и комментарии внутри манифестов:
```yaml
# ~/k3s/smokeping/smokeping.yaml
# Все манифесты для smokeping
# Манифест создания пространства имён `smokeping`. Если оно уже есть — kubectl apply ничего не изменит (т.е. безопасно.)
apiVersion: v1
kind: Namespace
metadata:
name: smokeping
---
# Манифест ConfigMap с конфигурацией Targets для smokeping
apiVersion: v1
kind: ConfigMap
metadata:
name: smokeping-config
namespace: smokeping
data:
# Это содержимое файла Targets. Здесь указываем, кого пинговать.
Targets: |
*** Targets ***
probe = FPing
menu = Top
title = Network Latency Grapher
remark = Привет тебе, путник. SmokePing website of Campany.\
Тут ты узнаешь все о задержках в вашей сети.
+ HOME
menu = HOME
title = HOME
++ Router
menu = Router
title = Router
alerts = someloss
host = 192.168.1.1
++ NAS
menu = NAS
title = NAS
alerts = someloss
host = 192.168.1.xxx
++ K3S_VIP
menu = K3S_VIP
title = K3S_VIP
alerts = someloss
host = 192.168.1.xxx
++ YANDEX_ALISA
menu = YANDEX_ALISA
title = YANDEX_ALISA
alerts = someloss
host = 192.168.1.xxx
+ INTERNET
menu = INTERNET
title = INTERNET
++ HOSTING_RU
menu = Russia
title = MasterHost_ru
alerts = someloss
host = xxx.xxx.xxx.xxx
++ HOSTING_EU
menu = Sweden
title = xxxxxxx
alerts = someloss
host = xxx.xxx.xxx.xxx
++ HOSTING_AS
menu = Tureky
title = xxxxxxx
alerts = someloss
host = xxx.xxx.xxx.xxx
---
# Манифест PVC (Longhorn) -- том для хранения данных графиков, чтоб при перезапуске пода данные не пропадали
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: smokeping-data # Имя PVC-хранилища
namespace: smokeping # Пространство имен `smokeping`
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn # Используем Longhorn как класс хранения
resources:
requests:
storage: 256Mi # Хватит на мониторинг 20-30 узлов глубиной 1.5-2 года (!)
---
# Манифест для развертывания smokeping (Deployment)
apiVersion: apps/v1
kind: Deployment
metadata:
name: smokeping
namespace: smokeping
spec:
replicas: 1
selector:
matchLabels:
app: smokeping
template:
metadata:
labels:
app: smokeping
spec:
containers:
- name: smokeping
# image: ghcr.io/linuxserver-arm64v8/smokeping # dля arm64v8
image: linuxserver/smokeping # оригинальный образ smokeping, и он заработал на amd64
env:
- name: TZ # Указываем временную зону
value: Europe/Moscow # ...чтобы на графиках не было UTC
ports:
- containerPort: 80
volumeMounts: # Монтируем файл Targets из ConfigMap в нужное место в контейнере
- name: config
mountPath: /config/Targets # mountPath указывает, куда будет "вставлен" файл
subPath: Targets # subPath = берём только один файл из configMap
- name: data
mountPath: /data # Данные графиков в Longhorn (PVC)
volumes:
- name: config # Используем том ConfigMap с конфигурацией
configMap:
name: smokeping-config
- name: data # Используем PVC (Longhorn) для хранения данных
persistentVolumeClaim:
claimName: smokeping-data
---
# Service — внутренний сервис для доступа к smokeping по сети внутри кластера
apiVersion: v1
kind: Service
metadata:
name: smokeping
namespace: smokeping
spec:
selector:
app: smokeping
ports:
- protocol: TCP
port: 80 # порт внутри кластера
targetPort: 80 # порт, на котором работает контейнер
type: ClusterIP # только для доступа внутри кластера (Ingress подключится к нему)
---
# IngressRoute для Traefik (под твою конфигурацию)
# Это публикует smokeping по адресу http://sp.local (заменить на свой домен)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: smokeping
namespace: smokeping
spec:
entryPoints:
- web # это должен быть один из entrypoints в Traefik (обычно "web" = порт 80)
routes:
- match: Host("sp.local") # доменное имя, по которому будет доступен сервис
kind: Rule
services:
- name: smokeping
port: 80
---
######## Это я пытался сделать редирект на favicon, но не заработало. У самого smokeping нет favicon, и это бесило.
# Манифест Middleware для перенаправление всех запросов к /favicon.ico на другой URL (к сожалению у smokeping нет favicon.ico).
#apiVersion: traefik.io/v1alpha1
#kind: Middleware
#metadata:
# name: favicon-redirect
# namespace: smokeping
#spec:
# redirectRegex:
# regex: "^/favicon\\.ico$"
# replacement: "http://ai.local/_graphmagnifier_118081.ico"
# permanent: true
```
Сохраним файл в `~/k3s/smokeping/smokeping.yaml` (или в другом месте, где вам удобно), и применим манифесты:
```bash
kubectl apply -f ~/k3s/smokeping/smokeping.yaml
```
После этого smokeping будет доступен по адресу http://sp.local (или по тому доменному имени, которое вы указали
в манифесте IngressRoute) и увидите занятные графики (только дождитесь пока данные соберутся, ну часок-другой):
![Примерно так выглядят графики](../images/k3s--pods-smokeping.png)

View File

@@ -0,0 +1,744 @@
# Защита хоста с помощью CrowdSec
Вы наверняка использовали (или как минимум слышали) о Fail2Ban. Он очень широко распространён для защиты SSH на хостах,
противодействия сканированию сайтов, легких DDoS-атак и "фонового bot-трафика". Fail2Ban существует с 2004 года
и давно стал стандартом для защиты серверов. Но он слабо подходит для защиты кластеров Kubernetes, так поды
обслуживающие внешний трафик (Ingress-контроллеры Traefik в случае k3s) могут находиться на разных узлах кластера.
Если Fail2Ban заблокирует IP-адрес на одной ноде, то он не сможет защитить другие узлы кластера, так как они ничего
не узнают о блокировках.
Для защиты распределённых систем (в том числе кластеров Kubernetes) набирает популярность CrowdSec. Это проект
с открытым исходным кодом, который, кроме обмена информацией об атаках между узлами (за периметром), использует
и внешний краудсорсинг (Community Blocklist) для защиты от атак. Он собирает данные о блокировках и позволяет
обмениваться этой информацией между всеми участниками сети (это отключаемая опция, и по умолчанию она отключена).
Таким образом, CrowdSec может не только защитить все узлы кластера (благодаря обмену информацией за периметром),
о блокировать IP-адреса, еще до их атаки на ваш сервер (если данные IP уже заблокированы другими участниками CrowdSec).
А еще CrowdSec модульный, поддерживает сценарии (http-cms-scanning, sshd-bf и тому-подобное),в 60 раз быстрее
Fail2Ban (он написан на Golang), работает с IPv6 и имеет интеграции с Traefik, Cloudflare, Nginx, k3s, Docker и другими
инструментами. CrowdSec активно растёт в нише DevOps, облаков, контейнеров и кластеров Kubernetes. А еще он не
требовательный по ресурсам (~100 МБ RAM) и подходит для Orange Pi.
----
## Утановка CrowdSec
В принципе, СrowdSec можно установить в кластер через Helm. Тогда он сам развернется на всех узлах кластера и это
отличный вариант для защиты Traefik (HTTP-запросы, сценарии http-cms-scanning, http-probing) и контейнеризированных
приложений (в моем случае [Gitea](../kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md), [3x-ui](../kubernetes/k3s-3xui-pod.md)
и тому подобного). Но мне нужно защитить еще и SSH самих узлов (узла) кластера. Поэтому план такой:
* Хостовый CrowdSec (на одном или всех узлах кластера) использует тот же Local API (LAPI) через виртуальный IP (VIP)
Keepalived для получения бан-листа и применяет его к SSH (через Firewall Bouncer) и через тот же LAPI сообщает
о банах ssh-bt в CrowdSec Agent внутри k3s.
* Кластерный CrowdSec Agent, внутри k3s, анализирует логи Traefik и создаёт решения (decisions) о бане IP.
* Traefik Bouncer в k3s, также подключается к LAPI для защиты HTTP (для git.cube2.ru и других web-приложений).
----
### CrowdSec на первом узле и защита SSH на хосте
----
#### Подготовка к установке CrowdSec
Делаем обновляем список пактов и систему:
```shell
sudo apt update
sudo apt upgrade
```
Добавляем репозиторий CrowdSec и ключи репозитория:
```shell
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
```
----
#### Установка CrowdSec и проверка
Устанавливаем CrowdSec:
```shell
sudo apt install crowdsec
```
Увидим, в число прочего:
```text
...
...
reating /etc/crowdsec/acquis.yaml
INFO[2025-xx-xx xx:xx:xx] crowdsec_wizard: service 'ssh': /var/log/auth.log
INFO[2025-xx-xx xx:xx:xx] crowdsec_wizard: using journald for 'smb'
INFO[2025-xx-xx xx:xx:xx] crowdsec_wizard: service 'linux': /var/log/syslog /var/log/kern.log
Machine 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' successfully added to the local API.
API credentials written to '/etc/crowdsec/local_api_credentials.yaml'.
Updating hub
Downloading /etc/crowdsec/hub/.index.json
Action plan:
🔄 check & update data files
INFO[2025-05-17 17:56:45] crowdsec_wizard: Installing collection 'crowdsecurity/linux'
downloading parsers:crowdsecurity/syslog-logs
downloading parsers:crowdsecurity/geoip-enrich
downloading https://hub-data.crowdsec.net/mmdb_update/GeoLite2-City.mmdb
downloading https://hub-data.crowdsec.net/mmdb_update/GeoLite2-ASN.mmdb
downloading parsers:crowdsecurity/dateparse-enrich
downloading parsers:crowdsecurity/sshd-logs
downloading scenarios:crowdsecurity/ssh-bf
downloading scenarios:crowdsecurity/ssh-slow-bf
downloading scenarios:crowdsecurity/ssh-cve-2024-6387
downloading scenarios:crowdsecurity/ssh-refused-conn
downloading contexts:crowdsecurity/bf_base
downloading collections:crowdsecurity/sshd
downloading collections:crowdsecurity/linux
enabling parsers:crowdsecurity/syslog-logs
enabling parsers:crowdsecurity/geoip-enrich
enabling parsers:crowdsecurity/dateparse-enrich
enabling parsers:crowdsecurity/sshd-logs
enabling scenarios:crowdsecurity/ssh-bf
enabling scenarios:crowdsecurity/ssh-slow-bf
enabling scenarios:crowdsecurity/ssh-cve-2024-6387
enabling scenarios:crowdsecurity/ssh-refused-conn
enabling contexts:crowdsecurity/bf_base
enabling collections:crowdsecurity/sshd
enabling collections:crowdsecurity/linux
...
...
```
Как видим, CrowdSec сам определил, что у нас есть SSH и Linux (syslog и kern.log). Создан локальный API (LAPI)
м логин/пароль для него записан в `/etc/crowdsec/local_api_credentials.yaml`.
Далее CrowdSec загрузил парсеры, сценарии и коллекции для настройки защиты SSH и Linux.
Проверим, что CrowdSec работает:
```shell
sudo systemctl status crowdsec
```
Увидим что-то вроде:
```text
● crowdsec.service - Crowdsec agent
Loaded: loaded (/lib/systemd/system/crowdsec.service; enabled; vendor preset: enabled)
Active: active (running) since Sat xxxx-xx-xx xx:xx:xx XXX; 51min ago
Main PID: 3357651 (crowdsec)
Tasks: 14 (limit: 18978)
Memory: 30.7M
CPU: 18.233s
CGroup: /system.slice/crowdsec.service
├─3357651 /usr/bin/crowdsec -c /etc/crowdsec/config.yaml
└─3357715 journalctl --follow -n 0 _SYSTEMD_UNIT=smb.service
Xxx xx xx:xx:xx xxxx systemd[1]: Starting Crowdsec agent...
Xxx xx xx:xx:xx xxxx systemd[1]: Started Crowdsec agent.
```
Проверим версию CrowdSec:
```shell
sudo cscli version
```
Увидим что-то вроде:
```text
version: v1.6.8-debian-pragmatic-arm64-f209766e
Codename: alphaga
BuildDate: 2025-03-25_14:50:57
GoVersion: 1.24.1
Platform: linux
libre2: C++
User-Agent: crowdsec/v1.6.8-debian-pragmatic-arm64-f209766e-linux
...
...
```
Проверим список установленных парсеров:
```shell
sudo cscli parsers list
```
Увидим что-то вроде:
```text
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
PARSERS
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name 📦 Status Version Local Path
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
crowdsecurity/dateparse-enrich ✔️ enabled 0.2 /etc/crowdsec/parsers/s02-enrich/dateparse-enrich.yaml
crowdsecurity/geoip-enrich ✔️ enabled 0.5 /etc/crowdsec/parsers/s02-enrich/geoip-enrich.yaml
crowdsecurity/smb-logs ✔️ enabled 0.2 /etc/crowdsec/parsers/s01-parse/smb-logs.yaml
crowdsecurity/sshd-logs ✔️ enabled 3.0 /etc/crowdsec/parsers/s01-parse/sshd-logs.yaml
crowdsecurity/syslog-logs ✔️ enabled 0.8 /etc/crowdsec/parsers/s00-raw/syslog-logs.yaml
crowdsecurity/whitelists ✔️ enabled 0.3 /etc/crowdsec/parsers/s02-enrich/whitelists.yaml
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
```
Как видим `crowdsecurity/sshd-logs` доступны, а значит CrowdSec может парсить логи SSH. Проверим список
установленных коллекций:
```shell
sudo cscli collections list
```
Увидим что-то вроде:
```text
─────────────────────────────────────────────────────────────────────────────────
COLLECTIONS
─────────────────────────────────────────────────────────────────────────────────
Name 📦 Status Version Local Path
─────────────────────────────────────────────────────────────────────────────────
crowdsecurity/linux ✔️ enabled 0.2 /etc/crowdsec/collections/linux.yaml
crowdsecurity/smb ✔️ enabled 0.1 /etc/crowdsec/collections/smb.yaml
crowdsecurity/sshd ✔️ enabled 0.6 /etc/crowdsec/collections/sshd.yaml
─────────────────────────────────────────────────────────────────────────────────
```
Видим, что `crowdsecurity/sshd` доступны. Проверим список установленных сценариев:
```shell
sudo cscli scenarios list
```
Увидим что-то вроде:
```text
SCENARIOS
───────────────────────────────────────────────────────────────────────────────────────────────────────
Name 📦 Status Version Local Path
───────────────────────────────────────────────────────────────────────────────────────────────────────
crowdsecurity/smb-bf ✔️ enabled 0.2 /etc/crowdsec/scenarios/smb-bf.yaml
crowdsecurity/ssh-bf ✔️ enabled 0.3 /etc/crowdsec/scenarios/ssh-bf.yaml
crowdsecurity/ssh-cve-2024-6387 ✔️ enabled 0.2 /etc/crowdsec/scenarios/ssh-cve-2024-6387.yaml
crowdsecurity/ssh-refused-conn ✔️ enabled 0.1 /etc/crowdsec/scenarios/ssh-refused-conn.yaml
crowdsecurity/ssh-slow-bf ✔️ enabled 0.4 /etc/crowdsec/scenarios/ssh-slow-bf.yaml
───────────────────────────────────────────────────────────────────────────────────────────────────────
```
Сценарии `ssh-bf`, `crowdsecurity/ssh-slow-bf` (брутфорсинг и медленный брутфорсинг SSH),
`crowdsecurity/ssh-cve-2024-6387` (защита от regreSSHion-атак на старые SSH-сервера) и
crowdsecurity/ssh-refused-conn` (отказ соединения SSH) доступны.
Кстати, обновлять все это богачество (парсеры, сценарии, коллекции и т.п.) можно командой:
```shell
sudo cscli hub update
```
Проверим конфиги CrowdSec, и убедимся, что он анализирует логи SSH:
```shell
sudo cat /etc/crowdsec/acquis.yaml
```
Должны увидеть вот такой блок:
```yaml
filenames:
- /var/log/auth.log
labels:
type: syslog
---
```
Если, вдруг, такого блока нет, добавьте его (лучше в начало) и перезапустим CrowdSec. Но обычно все уже настроено.
Проверим, что CrowdSec анализирует логи SSH:
```shell
sudo cscli metrics
```
Увидим что-то вроде:
```text
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Acquisition Metrics │
├────────────────────────┬────────────┬──────────────┬────────────────┬────────────────────────┬───────────────────┤
│ Source │ Lines read │ Lines parsed │ Lines unparsed │ Lines poured to bucket │ Lines whitelisted │
├────────────────────────┼────────────┼──────────────┼────────────────┼────────────────────────┼───────────────────┤
│ file:/var/log/auth.log │ 628 │ - │ 628 │ - │ - │
│ file:/var/log/kern.log │ 2.78k │ - │ 2.78k │ - │ - │
│ file:/var/log/syslog │ 3.46k │ - │ 3.46k │ - │ - │
╰────────────────────────┴────────────┴──────────────┴────────────────┴────────────────────────┴───────────────────╯
...
...
```
Как видим, CrowdSec читает `/var/log/auth.log` (логи SSH).
----
#### Установка CrowdSec Firewall Bouncer -- блокировщик IP-адресов
По мне, блокировки CrowdSec довольно беззубые. К счастью через "вышибалу" Firewall Bouncer можно блокировать
IP-адреса по iptables (или nftables) и сделать CrowdSec злее fail2ban. Для этого нужно установить
`crowdsec-firewall-bouncer-iptables`:
```shell
sudo apt-get install crowdsec-firewall-bouncer-iptables
```
Проверим, что "вышибала" запустилась:
```shell
sudo systemctl status crowdsec-firewall-bouncer
```
Увидим что-то вроде:
```text
● crowdsec-firewall-bouncer.service - The firewall bouncer for CrowdSec
Loaded: loaded (/etc/systemd/system/crowdsec-firewall-bouncer.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2025-05-18 14:47:10 MSK; 723ms ago
Process: 621537 ExecStartPre=/usr/bin/crowdsec-firewall-bouncer -c /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml -t (code=exited, status=0/SUCCESS)
Process: 621674 ExecStartPost=/bin/sleep 0.1 (code=exited, status=0/SUCCESS)
Main PID: 621622 (crowdsec-firewa)
Tasks: 10 (limit: 18978)
Memory: 7.4M
CPU: 401ms
CGroup: /system.slice/crowdsec-firewall-bouncer.service
└─621622 /usr/bin/crowdsec-firewall-bouncer -c /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
May 18 14:47:04 opi5 systemd[1]: Starting The firewall bouncer for CrowdSec...
May 18 14:47:10 opi5 systemd[1]: Started The firewall bouncer for CrowdSec.
```
Подключить его в CrowdSec:
```shell
sudo cscli bouncers add firewall-bounce
```
Проверим, что "вышибала" добавлен:
```shell
sudo cscli bouncers list
```
Увидим что-то вроде:
```text
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name IP Address Valid Last API pull Type Version Auth Type
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
cs-firewall-bouncer-xxxx 127.0.0.1 ✔️ xxxx-xx-xxTxx:xx:xxZ crowdsec-firewall-bouncer v0.0.31-debian-pragmatic-xxxxxx... api-key
firewall-bouncer ✔️ api-key
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
```
----
#### Подключаем наш CrowdSec к обмену данными об атаках
CrowdSec может обмениваться данными об атаках с другими участниками сети. Чтобы это сделать, нужно пойти [на сайт
CrowdSec](https://crowdsec.net/) и зарегистрироваться. После подтверждения регистрации по email, в личном кабинете
в самом низу, увидим строчку команды, типа:
```shell
sudo cscli console enroll -e context хеш-идентификатор-вашего-аккаунта
```
Скопируем эту команду и выполняем в терминале. Увидим что-то вроде:
```text
INFO manual set to true
INFO context set to true
INFO Enabled manual : Forward manual decisions to the console
INFO Enabled tainted : Forward alerts from tainted scenarios to the console
INFO Enabled context : Forward context with alerts to the console
INFO Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.
INFO Please restart crowdsec after accepting the enrollment.
```
Как видим, нужно перезапустить CrowdSec:
```shell
sudo systemctl restart crowdsec
```
Теперь нужно снова зайти в личный кабинет CrowdSec и подтвердить подключение Security Engine.
Все! Подключение локального CrowdSec к Community Blocklist завершено. В личном кабинете можно посмотреть статистику
(по каждому Security Engine, ведь на один аккаунт можно подключить несколько хостов с CrowdSec) и даже управлять
фильтрами и сценариями (это не точно).
![crowdsec--security-engine-registration.png](../images/crowdsec--security-engine-registration.png)
Проверим, что CrowdSec получает блокировки через Community Blocklist API (CAPI):
```shell
sudo cscli metrics
```
Увидим что-то типа:
```text
...
...
╭──────────────────────────────────────────╮
│ Local API Decisions │
├────────────────┬────────┬────────┬───────┤
│ Reason │ Origin │ Action │ Count │
├────────────────┼────────┼────────┼───────┤
│ generic:scan │ CAPI │ ban │ 3222 │
│ smb:bruteforce │ CAPI │ ban │ 427 │
│ ssh:bruteforce │ CAPI │ ban │ 10033 │
│ ssh:exploit │ CAPI │ ban │ 1315 │
╰────────────────┴────────┴────────┴───────╯
...
```
Как видим, CrowdSec получает блокировки. Если очень интересно, можно посмотреть, что именно и почему блокируется
(например, `ssh:bruteforce`):
```shell
sudo cscli decisions list --origin CAPI
```
Увидим длиннющий список, примерно такого содержания:
```text
╭───────┬────────┬────────────────────────────────────┬────────────────┬────────┬─────────┬────┬────────┬────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │ AS │ Events │ expiration │ Alert ID │
├───────┼────────┼────────────────────────────────────┼────────────────┼────────┼─────────┼────┼────────┼────────────┼──────────┤
..... .... ...................... .............. ... . ......... .
│ ..... │ CAPI │ Ip:129.211.204.27 │ ssh:bruteforce │ ban │ │ │ 0 │ 79h15m46s │ 1 │
│ ..... │ CAPI │ Ip:128.199.124.27 │ ssh:bruteforce │ ban │ │ │ 0 │ -1h44m14s │ 1 │
│ ..... │ CAPI │ Ip:Ip:2602:80d:1006::76 │ ssh:bruteforce │ ban │ │ │ 0 │ 48h15m46s │ 1 │
│ ..... │ CAPI │ Ip:123.58.213.127 │ ssh:bruteforce │ ban │ │ │ 0 │ 160h15m46s │ 1 │
╰───────┴────────┴────────────────────────────────────┴────────────────┴────────┴─────────┴────┴────────┴────────────┴──────────╯
```
----
#### Настройка Whitelist (белого списка)
Чтобы не заблокировать себя (случайно) нужно создать в Whitelist (белый список). Например, сделаем `home_whitelist`
(имя списка, таких списков может быть несколько, и
```shell
sudo cscli allowlist create home_whitelist -d 'Мой домашний whitelist'
```
Теперь добавим в него свои домашнюю подсеть или IP-адрес (через пробел можно указать несколько адресов или подсетей):
```shell
sudo cscli allowlist add home_whitelist 192.168.1.0/24 XXX.XXX.XXX.XXX
````
Проверим, что все добавилось:
```shell
sudo cscli allowlist inspect home_whitelist
```
Увидим что-то вроде:
```text
──────────────────────────────────────────────
Allowlist: home_whitelist
──────────────────────────────────────────────
Name home_whitelist
Description Мой домашний whitelist
Created at 2025-05-17T21:00:13.042Z
Updated at 2025-05-17T21:01:29.090Z
Managed by Console no
──────────────────────────────────────────────
───────────────────────────────────────────────────────────────
Value Comment Expiration Created at
───────────────────────────────────────────────────────────────
192.168.1.0/24 never 2025-05-17T21:00:13.042Z
XXX.XXX.XXX.XXX never 2025-05-17T21:00:13.042Z
XXX.XXX.XXX.XXX never 2025-05-17T21:00:13.042Z
...
...
───────────────────────────────────────────────────────────────
```
Еще один способ отредактировать (создать) Whitelist-конфиг парсера, который мы получили командой
`sudo cscli parsers list`. Конфиг `/etc/crowdsec/parsers/s02-enrich/whitelists.yaml` довольно простой, если его
отредактировать (добавить нужные IP-адреса, подсети или даже доменные имена), а затем перезапустить CrowdSec -- получим
тот же результат. Только управлять через списки (allowlist) удобнее.
[См. документацию](https://doc.crowdsec.net/u/getting_started/post_installation/whitelists/).
----
#### Настройка Firewall Bouncer (блокировщик IP-адресов)
----
##### Сценарии блокировок
Когда мы проверяли установку CrowdSec, и проверим список сценариев `shell sudo cscli scenarios list`, то нам был
показан список yaml-манифестов c конфигурациями сценариев блокировок. Эти **сценарии занимаются распознаванием атак**,
в частности касающихся SSH:
* `/etc/crowdsec/scenarios/ssh-bf.yaml` -- брутфорс SSH
* `/etc/crowdsec/scenarios/ssh-slow-bf.yaml` -- медленный брутфорс SSH
* `/etc/crowdsec/scenarios/ssh-cve-2024-6387.yaml` -- regreSSHion-атака (атаки уязвимости SSH-серверов старых версий)
* `/etc/crowdsec/scenarios/ssh-refused-conn.yaml` -- отказ соединения SSH, защищает от сканеров, которые ищут
открытые SSH-порты (на очень актуально, если у вас SSH открыт по стандартном 22-порту).
В некоторых манифестах может быть несколько блоков конфигурации блокировок для разных сценариев атак "зловредов".
Например, в `ssh-bf.yaml` есть блоки `crowdsecurity/ssh-bf` (для тупого брутфорса) и `crowdsecurity/ssh-bf_user-enum`
(для перебора пользователей).
Меняем "беззубые" параметры, на что-то более серьезное. Открываем на редактирование, например, `ssh-bf.yaml`:
```shell
sudo nano /etc/crowdsec/scenarios/ssh-bf.yaml
```
Увидим что-то типа:
```yaml
# ssh bruteforce
type: leaky
name: crowdsecurity/ssh-bf
description: "Detect ssh bruteforce"
filter: "evt.Meta.log_type == 'ssh_failed-auth'"
leakspeed: "10s"
references:
- http://wikipedia.com/ssh-bf-is-bad
capacity: 5
groupby: evt.Meta.source_ip
blackhole: 1m
reprocess: true
labels:
service: ssh
confidence: 3
spoofable: 0
classification:
- attack.T1110
label: "SSH Bruteforce"
behavior: "ssh:bruteforce"
remediation: true
---
# ssh user-enum
type: leaky
name: crowdsecurity/ssh-bf_user-enum
description: "Detect ssh user enum bruteforce"
filter: evt.Meta.log_type == 'ssh_failed-auth'
groupby: evt.Meta.source_ip
distinct: evt.Meta.target_user
leakspeed: 10s
capacity: 5
blackhole: 1m
labels:
service: ssh
remediation: true
confidence: 3
spoofable: 0
classification:
- attack.T1589
behavior: "ssh:bruteforce"
label: "SSH User Enumeration"
```
Что тут происходит:
* Сценарий `crowdsecurity/ssh-bf`:
* Тип: `leaky` -- leaky bucket — алгоритм "дырявое ведро", считающий события в окне времени. Метафора "дырявого
ведра" в том, что из дырок на дне идет утечка со скоростью одна попытка за `leakspeed`. Емкость ведра равна
`capacity`. Когда "ведро" было пустм, в него можно было поместить `capacity` событий, и после по одому событию
в `leakspeed`. Если ведро переполнено событиями, то включается `blackhole` (черная дыра) и события игнорируются
в течении `blackhole` времени.
* Фильтр: `evt.Meta.log_type == 'ssh_failed-auth'` -- ловит неудачные попытки входа по SSH из `/var/log/auth.log`.
* Логика:
* `groupby: evt.Meta.source_ip` -- группирует события по IP атакующего.
* `leakspeed: 10s` -- "окно времени" — 10 секунд (каждые 10 сек разрешена одна попытка).
* `capacity: 5` -- Бан после 5 неудачных попыток.
* `blackhole: 1m` -- Бан на 1 минуту.
* Сценарий `crowdsecurity/ssh-bf_user-enum`:
* Тип тот же.
* Фильтр тот же.
* Логика:
* `distinct: evt.Meta.target_user` -- считает попытки с разными пользователями (root, admin, pi, orangepi и т.д.).
* `leakspeed: 10s` -- "окно времени" — 10 секунд.
* `capacity: 5` -- Бан после 5 разных пользователей за 10 секунд.
* `blackhole: 1m` -- Бан на 1 минуту.
Как видим в обоих случаях бан срабатывает после пяти попыток за десять секунд, и блокировка всего на минуту. Конечно,
брутфорсеры -- это быстрые атаки, но "быстрота" понятие относительное. Я выставляю:
* `leakspeed: 10m`
* `capacity: 2`
* `blackhole: 1h`
И считаю, что это довольно мягко. Но чтоб случайно не заблокировать себя, когда буду подключаться с внешнего IP
не из белого списка (например, по мобильному интернету) -- это разумный компромисс.
После редактирования файла, нужно перезапустить CrowdSec, чтоб он применил изменения:
```shell
sudo systemctl restart crowdsec
sudo systemctl restart crowdsec-firewall-bouncer
```
Другие сценарии можно настроить по аналогии. "Злость" управляется параметрами `leakspeed`, `capacity` и `blackhole`.
Но имейте в виду: не стоит менять много параметров одновременно. Настройки разных сценариев могут конфликтовать
друг другом, и тогда CrowdSec не запустится.
После перезапуска CrowdSec:
```shell
sudo systemctl restart crowdsec
```
И еще, экспериментально я обнаружил, что настройки дней, например `2d` недопустимы. Надо указывать `48h` (48 часов),
и в целом не нужно сразу месть настройки сразу во всех сценариях. Они могут конфликтовать друг с другом, и CrowdSec
не перезапуститься.
Проверим, что CrowdSec начал банить на основании настроенных правил (особо ждать не придется, зловреды попадутся уже через
пару минут):
```shell
sudo cscli decisions list
```
Увидим что-то типа:
```text
╭───────┬──────────┬───────────────────┬────────────────────────────────┬─────┬────┬────────────────────────┬────────┬────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Act │ Co │ AS │ Events │ expiration │ Alert ID │
├───────┼──────────┼───────────────────┼────────────────────────────────┼─────┼────┼────────────────────────┼────────┼────────────┼──────────┤
│ 30004 │ crowdsec │ Ip:39.98.38.186 │ crowdsecurity/ssh-slow-bf │ ban │ CN │ 37963 Hangzhou Alibaba │ 11 │ 3h54m49s │ 6 │
│ 30002 │ crowdsec │ Ip:165.246.104.64 │ crowdsecurity/ssh-bf │ ban │ KR │ 9317 INHA UNIVERSITY │ 3 │ 3h50m0s │ 4 │
│ 90210 │ crowdsec │ Ip:180.10.143.248 │ crowdsecurity/ssh-bf_user-enum │ ban │ CN │ 4134 Chinanet │ 3 │ 3h6m38s │ 216 │
╰───────┴──────────┴───────────────────┴────────────────────────────────┴─────┴────┴────────────────────────┴────────┴────────────┴──────────╯
```
----
##### Время блокировок
Сценарии занимаются распознаванием угроз, но самими блокировками они не занимаются. Блокировки настроены по умолчанию
на четыре часа, и это указано в профилях `/etc/crowdsec/profiles.yaml`. Чтобы изменить время, на которое "зловред"
отправляется в бан, нужно отредактировать этот файл. По умолчанию он вот такой:
```yaml
name: default_ip_remediation
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
#duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
# notifications:
# - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this.
# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this.
# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this.
# - email_default # Set the required email parameters in /etc/crowdsec/notifications/email.yaml before enabling this.
on_success: break
---
name: default_range_remediation
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
- type: ban
duration: 4h
#duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
# notifications:
# - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this.
# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this.
# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this.
# - email_default # Set the required email parameters in /etc/crowdsec/notifications/email.yaml before enabling this.
on_success: break
```
Как видим, по умолчанию блокировка на 4 часа. Чтобы изменить время блокировок, нужно отредактировать `duration: 4h` на
нужное. Но в конфигурации есть "заготовка" для динамического времени блокировок:
`duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)` -- каждый раз, когда зловред
попадает в бан, время блокировки увеличивается на 4 часа. То есть, если зловред попался в бан 5 раз, то его блокировка
будет 20 часов. И так далее (формулу, при желании, можно изменить). Это то, что нам нужно. Имейте в виду, что
подключение `duration_expr` исключает возможность указать `duration` (время блокировки) в секции `decisions`. Таким
образом получаем вот такой конфиг:
```yaml
name: default_ip_remediation
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
# duration: 4h
duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
on_success: break
---
name: default_range_remediation
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
- type: ban
duration: 5h
on_success: break
```
Можно добавлять и свои правила. Например, для более длительных блокировок медленных брутфорсов, добавим в конце:
```yaml
---
name: ssh_slow_bf_remediation
filters:
- Alert.Remediation == true && Alert.Scenario == "crowdsecurity/ssh-slow-bf"
decisions:
- type: ban
duration: 10h
on_success: break
```
После сохранения конфига, перезапустим CrowdSec:
```shell
sudo systemctl restart crowdsec
```
И убедимся, что время блокировки увеличилось:
```shell
sudo cscli decisions list
```
```text
╭────────┬──────────┬───────────────────┬──────────────────────┬────────┬─────────┬──────────────────────┬────────┬────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │ AS │ Events │ expiration │ Alert ID │
├────────┼──────────┼───────────────────┼──────────────────────┼────────┼─────────┼──────────────────────┼────────┼────────────┼──────────┤
│ 165247 │ crowdsec │ Ip:165.246.104.64 │ crowdsecurity/ssh-bf │ ban │ KR │ 9317 INHA UNIVERSITY │ 3 │ 91h25m24s │ 258 │
╰────────┴──────────┴───────────────────┴──────────────────────┴────────┴─────────┴──────────────────────┴────────┴────────────┴──────────╯
```
----
#### Web-панель
Плюсом CrowdSec является то, что благодаря обмену информацией о блокировках, в личном кабинете на сайте CrowdSec можно
посмотреть ваши локальные блокировки через веб-интерфейсе:
![crowdsec--security-panel.png](../images/crowdsec--security-panel.png)
----
#### Управление блокировками
Можно добавить бан вручную (по умолчанию: `duration:4h` и `type:ban`):
```shell
sudo cscli decisions add -i xxx.xxx.xxx.xxx
sudo cscli decisions add --ip xxx.xxx.xxx.xxx --duration 24h --reason "любопытный безопасник"
sudo cscli decisions add --ip xxx.xxx.xxx.xxx --reason "web bruteforce" --type captcha
```
Снять блокировку отдельного IP, подсети (диапазона) или вообще все:
```shell
sudo cscli decisions delete --ip xxx.xxx.xxx.xxx
sudo cscli decisions delete --range yyy.yyy.yyyy.yyy/24
sudo cscli decisions delete --all
```
#### Блокировки по GeoIP
Проверим, что у нас есть парсер на основе GeoIP:
```shell
sudo cscli parsers lis
```
Увидим в числе прочих:
```text
PARSERS
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Name 📦 Status Version Local Path
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
crowdsecurity/geoip-enrich ✔️ enabled 0.5 /etc/crowdsec/parsers/s02-enrich/geoip-enrich.yaml
```
Он обогащает события (events) GeoIP-информацией: страна, город, ASN, континент и так далее, но сам ничего не блокирует —
он просто добавляет поля к событиям. Но это делает возможным создание собственных фильтров и сценариев,
завязанных на GeoIP. Используется двухбуквенный код страны (стандарт ISO-3166-1 alpha-2).
У меня на хосте половина зловредов из Китая и Кореи. За ними, с большим отрывом, Индия и Индонезия. Вот честно,
ничего против этих стран не имею, от из этих регионов 80% всех атак на мой SSH. Создаем свой сценарий:
```shell
sudo nano /etc/crowdsec/scenarios/ban-cn--geoip.yaml
```
И вставим туда:
```yaml
# /etc/crowdsec/scenarios/ban-cn--geoip.yaml
# Бан по GeoIP для Китая и Кореи
type: trigger
name: local/ban-russian-ips
description: "Ban any IP from China & Korea"
filter: evt.Meta.geoip_country in ['CN', 'KR'] and evt.Meta.service == 'ssh'
groupby: evt.Meta.source_ip
labels:
country_ban: CN
remediation: true
classification:
- abuse
behavior: "geoip:ban"
confidence: 5
label: "GeoIP Country Ban"
manual: true
```
Как видно, это `trigger`-сценарий, он срабатывает при одиночном совпадении, без необходимости "накопить
события", как в `leaky`. И бан срабатывает если "зловред" лезет в сервис `ssh`.
Перезапустим CrowdSec:
```shell
sudo systemctl restart crowdsec
```
Теперь CrowdSec будет автоматически блокировать все новые IP из указанных стран при появлении их в логах.
И если честно, можно вообще все страны забанить, кроме тех, где бываю в отпуске. Нечего им делать на моем сервере. :)

View File

@@ -20,7 +20,7 @@ IoT-устройства, edge-серверы и т.п.). Для кластер
контейнеры всё равно запускаются runtimeом (это containerd в k3s). И Docker все равно еще нужен для создания
образов, и если при установке k3s не указать `--docker` то k3s будет использовать его как runtime._
Но, есть у k3s и минус для конкретно моего случая -- распределенная база **etcd**, в которой хранится состояния
Но, есть у k3s и минус для конкретно моего случая распределенная база **etcd**, в которой хранится состояния
кластера, нод и подов, в нем заменена SQLite. Это круто для маленьких компьютеров: экономно по памяти и другим ресурсам,
и, что главное, никак не сказывается на производительности (пока узлов меньше 50-80), но означает, что в кластере k3s
может быть только одна мастер-нода. Если мастер-нода упадет, её некому будет заменить и весь кластер умрет.
@@ -29,7 +29,7 @@ IoT-устройства, edge-серверы и т.п.). Для кластер
### Важное предупреждение
k3s -- это не упрощенная мини-версия Kubernetes, здесь все компоненты упакованы в один бинарник, а значит намного
k3s это не упрощенная мини-версия Kubernetes, здесь все компоненты упакованы в один бинарник, а значит намного
проще не только добавлять узлы, но и удалять их. Так что если что-то пойдет не так с настройкой узла, просто удалите
и начните заново. Удаление k3s с узла:
```bash
@@ -41,7 +41,7 @@ sudo /usr/local/bin/k3s-agent-uninstall.sh # На воркере
Некоторые требования к узлам:
* На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04).
* Статические IP-адреса узлов (или зрезервированные под MAC-адреса IP в DHCP).
* Статические IP-адреса узлов (или зарезервированные под MAC-адреса IP в DHCP).
* На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet).
@@ -51,14 +51,14 @@ curl -sfL https://get.k3s.io | sh -s - server --cluster-init --tls-san=192.168.1
```
Здесь:
* `server` -- значение по умолчанию, устанавливает узел k3s в режиме *мастер* (control-plane). В этом режиме узел
* `server` значение по умолчанию, устанавливает узел k3s в режиме *мастер* (control-plane). В этом режиме узел
будет запускать все компоненты управления Kubernetes: API-сервер, контроллер-менеджер, планировщик (scheduler).
Такой узел отвечает за управление кластером и может также выполнять рабочие нагрузки (workloads), если
не настроены ограничения (taints). Если бы мы указали `agent` -- был бы установлен узел k3s в режиме *воркер*-узла.
* `--cluster-init` -- добавляет поддержку высокой доступности (HA -- High Availability) через встроенный `etcd`. Это
Такой узел отвечает за управление кластером и может также выполнять рабочие нагрузки (workloads), если
не настроены ограничения (taints). Если бы мы указали `agent` был бы установлен узел k3s в режиме *воркер*-узла.
* `--cluster-init` добавляет поддержку высокой доступности (HA High Availability) через встроенный `etcd`. Это
значит, что узел инициализирует новый кластер и готов к тому, чтобы другие мастер-узлы могли к нему подключиться
(для создания HA-конфигурации).
* `--tls-san=192.168.1.27` -- добавляет IP 192.168.1.27 в сертификаты API-сервера, чтобы другие узлы и клиенты
* `--tls-san=192.168.1.27` добавляет IP 192.168.1.27 в сертификаты API-сервера, чтобы другие узлы и клиенты
могли обращаться к нему по этому адресу.
Проверим, что все k3s запущен:
@@ -70,14 +70,64 @@ sudo service k3s status
```text
● k3s.service - Lightweight Kubernetes
Loaded: loaded (/etc/systemd/system/k3s.service; enabled; vendor preset: enabled)
Active: active (running) since ...
...
...
Active: active (running) since
```
Посмотрим сколько нод в кластере:
## Перемещение конфигурации kubectl
При выполнении команд `kubectl` используется файл конфигурации из переменной `KUBECONFIG`. При установке k3s создает
его в `/etc/rancher/k3s/k3s.yaml` и у него права `600` (только для root). Чтобы использовать `kubectl` от имени
пользователя хоста, нужно скопировать файл конфигурации в домашнюю директорию и изменить права доступа.
```bash
sudo kubectl get nodes
mkdir -p ~/.kube/config
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
```
Дадим права:
```bash
sudo chown opi:opi ~/.kube
chmod 755 ~/.kube
sudo chown opi:opi ~/.kube/config
chmod 755 ~/.kube/config
sudo chown opi:opi ~/.kube/config/k3s.yaml
chmod 755 ~/.kube/config/k3s.yaml
```
Проверим:
```bash
ls -ld ~/.kube ~/.kube/config ~/.kube/config/k3s.yaml
```
Должны увидеть что-то вроде:
```text
drwxr-xr-x 4 opi opi 4096 Apr 26 11:32 /home/opi/.kube
drwxr-xr-x 2 opi opi 4096 Apr 26 11:09 /home/opi/.kube/config
-rwxr-xr-x 1 opi opi 2961 Apr 27 16:31 /home/opi/.kube/config/k3s.yaml
```
Установить переменную KUBECONFIG:
```bash
export KUBECONFIG=~/.kube/config/k3s.yaml
```
И добавь её в `~/.bashrc` чтобы не вводить каждый раз после входа в систему или перезагрузки:
```bash
echo 'export KUBECONFIG=~/.kube/config/k3s.yaml' >> ~/.bashrc
```
Проверим, что переменная установлена:
```bash
echo $KUBECONFIG
```
## Проверим установку k3s
Посмотрим сколько нод в кластере (теперь можно не использовать `sudo`):
```bash
kubectl get nodes
```
И, та-да! Увидим одну ноду:
@@ -91,7 +141,7 @@ opi5plus-2 Ready control-plane,etcd,master 31m v1.31.5+k3s1
А что там внутри? Посмотрим на поды:
```bash
sudo kubectl get pods -A
kubectl get pod -A
```
Целых семь подов (минималистичная установка k3s):
@@ -106,23 +156,31 @@ kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 12m
```
Тут статус X/Y в выводе kubectl get pods показывает:
* Y — сколько контейнеров должно быть в поде (по спецификации).
* X — сколько из них сейчас работает (running).
* READY `X/Y` в выводе `kubectl get pod` показывает:
* Y — сколько контейнеров должно быть в поде (по спецификации).
* X — сколько из них сейчас работает (running).
* STATUS показывает состояние пода. Основные статусы которые могут быть:
* `Running` — под работает и все контейнеры в нем запущены.
* `Completed` — под завершил свою работу (например, Job или CronJob).
* `Error` — под завершился с ошибкой.
* `ContainerCreating` — под в процессе создания, контейнеры в нем еще не запущены.
* `CrashLoopBackOff` — под перезапускается из-за ошибки.
* `ImagePullBackOff` — не удалось загрузить образ контейнера.
* `Pending` — под ожидает ресурсов (например, памяти или CPU).
Представлены следующие поды:
1. `coredns` — это DNS-сервер для кластера. Он отвечает за разрешение имен внутри Kubernetes (например, чтобы поды
могли обращаться друг к другу по именам сервисов вроде my-service.default.svc.cluster.local).
2. `helm-install-traefik-crd` -- это временный под (Job), который устанавливает Custom Resource Definitions (CRD)
2. `helm-install-traefik-crd` это временный под (Job), который устанавливает Custom Resource Definitions (CRD)
для *Traefik* — ingress-контроллера, встроенного в k3s. CRD нужны для управления ingress-ресурсами
(маршрутизацией HTTP/HTTPS). Этот под — одноразовая задача (Job), а не постоянный сервис. Он запустился, выполнил
работу (установил CRD) и завершился. Статус "*Completed*" значит, что он больше не работает.
3. `helm-install-traefik` -- ещё один Job, который устанавливает сам Traefik через Helm-чарт. Этот под развернул
3. `helm-install-traefik` ещё один Job, который устанавливает сам Traefik через Helm-чарт. Этот под развернул
основной Traefik-под и завершился.
4. `local-path-provisioner` -- компонент для автоматического создания локальных Persistent Volumes (PV) на узлах. Он
4. `local-path-provisioner` компонент для автоматического создания локальных Persistent Volumes (PV) на узлах. Он
позволяет подам запрашивать хранилище (например, через PersistentVolumeClaim) без сложной настройки NFS или внешних
хранилищ. В k3s это встроено для простоты.
5. `metrics-server` -- собирает данные об использовании ресурсов (CPU, память) подов и узлов. Это нужно для команд
5. `metrics-server` собирает данные об использовании ресурсов (CPU, память) подов и узлов. Это нужно для команд
вроде `kubectl top` или для Horizontal Pod Autoscaler (HPA). Установку метрик можно отключить при запуске k3s
флагом `--disable=metrics-server`.
6. `svclb-traefik` - это под для балансировки нагрузки (Service Load Balancer) для Traefik. В k3s нет встроенного
@@ -131,19 +189,19 @@ kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0
* один для самой логики балансировки;
* другой для мониторинга или дополнительной функциональности (например, *keepalived* или аналога) и это зависит
от реализации в k3s.
7. `traefik` -- сам Traefik, ingress-контроллер, который обрабатывает HTTP/HTTPS трафик кластера и маршрутизирует
7. `traefik` сам Traefik, ingress-контроллер, который обрабатывает HTTP/HTTPS трафик кластера и маршрутизирует
его к соответствующим подам (с динамической конфигурацией нашим) и сервисам по правилам Ingress. Traefik в k3s
установлен по умолчанию, но его можно отключить при запуске k3s флагом `--disable=traefik` (не будет ни *traefik*,
ни *svclb*, ни связанных *Helm Jobs*).
Обратите внимание, что, например, под `coredns` получил имя `coredns-ccb96694c-tfjwj`. Имена подов (Pod Names)
в Kubernetes генерируются автоматически на основе правил, чтобы каждый под в кластере имел уникальное имя.
Структура имени -- `<имя-приложения>-<хеш-ревизии>-<случайный-суффикс>`. Впрочем, `<хеш-ревизии>` может отсутствовать,
Структура имени `<имя-приложения>-<хеш-ревизии>-<случайный-суффикс>`. Впрочем, `<хеш-ревизии>` может отсутствовать,
если под не имеет контроллера репликации (например, Job или CronJob).
Можно проверить, что API нашего узла (кластера) отвечает:
Можно проверить, что API нашего узла (кластера) отвечает на порту `6443` (порт по умолчанию):
```bash
curl -k https://192.168.1.27
curl -k https://192.168.1.27:6443
```
Здесь ключ `-k` означает, что мы не проверяем сертификаты (нам важно только, что сервер отвечает). Должны получить
@@ -160,15 +218,105 @@ Unauthorized JSON-ответ от API. Что-то вроде:
}
```
## Подключение второго узла (мастер)
ВАЖНО: Надо проверить версию Traefik, которая установилась. Например версия 3.3.2 отличается от 3.3.6 значением
`apiVersion` в которые надо указывать в манифесте:
* `traefik.io/v1alpha1` -- для Traefik v3.3.6
* `traefik.containo.us/v1alpha1` -- для Traefik v3.3.2
Проверить версию можно так (подставьте вместо `<хеш-ревизии>-<случайный-суффикс>` свой, из вывода `kubectl get pod`):
```bash
kubectl get pod -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -o jsonpath='{.spec.containers[0].image}'
```
Увидим что-то вроде:
```text
rancher/mirrkubectl exec -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -- traefik versionik-67bfb46dcb-prvjd -- traefik version
Version: 3.3.2
Codename: saintnectaire
Go version: go1.23.4
Built: 2025-01-14T15:52:27Z
OS/Arch: linux/arm64
```
## Установка блочного хранилища (PVC -- Persistent Volume Claim) Longhorn
Longhorn -- это блочное хранилище k3s, которое позволяет создавать и управлять блочными томами в кластере
для обеспечения высокой доступности и отказоустойчивости. Если узел, на котором находится том, выходит из строя,
Longhorn автоматически перемещает том на другой узел и контейнер продолжает работу с томом, как будто ничего
не произошло (с некоторой задержкой, конечно).
Если установить Longhorn сразу, то при добавлении новых узлов в кластер Longhorn автоматически будет устанавливаться
на них. Но если вы хотите установить Longhorn позже, то нужно будет вручную установить его на новых узлах.
| **ВАЖНО** |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Longhorn требует наличия iSCSI на узлах. Убедитесь, что молуль iSCSI включен или собран в ядро Linux на вашем хосте. Для Orange Pi 5 Plus (Ubuntu на ядре 6.1.43-rockchip-rk3588) [потребует перекомпиляцию ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md), на Orange Pi 5 (Ubuntu ядро 5.10.160-rockchip-rk3588) iSCSI разрешен к подключению как модуль. |
Установим Longhorn на первый узел (мастер):
```bash
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
```
Проверим, что все поды Longhorn запустились:
```bash
kubectl get pod -n longhorn-system
```
Увидим что-то вроде:
```text
NAME READY STATUS RESTARTS AGE
csi-attacher-5d68b48d9-6wsff 1/1 Running 0 9m18s
csi-attacher-5d68b48d9-t9wht 1/1 Running 0 9m19s
csi-attacher-5d68b48d9-w772m 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-bd26m 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-vg9g9 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-xc8ks 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-47pw9 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-n5ndj 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-xjflz 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-2cbn8 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-5x9fd 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-79dgn 1/1 Running 0 9m18s
engine-image-ei-b907910b-2jgjr 1/1 Running 0 10m
instance-manager-fba73d00a7ff718b4ddabef450cfe759 1/1 Running 0 9m34s
longhorn-csi-plugin-h92s8 3/3 Running 0 9m18s
longhorn-driver-deployer-5f44b4dc59-z7tlc 1/1 Running 0 9m34s
longhorn-manager-k2gtm 2/2 Running 0 10m
longhorn-ui-f7ff9c74-7bbsw 1/1 Running 0 10m
longhorn-ui-f7ff9c74-b4svq 1/1 Running 0 10m
```
Что это за поды? Longhorn состоит из нескольких компонентов, каждый из которых отвечает за свою задачу:
* CSI-компоненты (attacher, provisioner, resizer, snapshotter) интегрируют Longhorn с Kubernetes для работы
с Persistent Volumes (PV) и Persistent Volume Claims (PVC).
* `csi-attacher` -- (3 пода) для присоединения (attaches) тома Longhorn к подам Kubernetes, когда PVC монтируется.
Три реплики CSI -- для отказоустойчивости. На одном узле достаточно одной, но Longhorn по умолчанию разворачивает 3.
* `csi-provisioner` -- (3 пода) для сздания новыех PV (томов) при запросе PVC. Отвечает за динамическое
выделение хранилища.
* `csi-resizer` -- (3 пода) позволяет изменять размер томов (можно только увеличивать PVC) без остановки приложений.
* `csi-snapshotter` -- (3 пода): управля.т созданием и восстановлением снапшотов томов.
* `engine-image` -- хранит бинарники движка Longhorn, используемые для работы томов (чтение/запись данных).
Один под на узел, содержит образ движка для всех томов на этом узле и запускается на каждом узле.
* `instance-manager` -- (1 под) Управляет движками и репликами томов на узле (например, запускает процессы для
чтения/записи данных). Один под на узел для локального управления томами.
* `longhorn-csi-plugin` -- (1 под, 3 контейнера) интерфейс между Kubernetes CSI и Longhorn. Обрабатывает
монтирование/управление томами на узле.
* `longhorn-driver-deployer` -- (1 под) устанавливает CSI-драйвер и регистрирует его в Kubernetes.
Нужен для инициализации CSI-интеграции и он обычно один в кластере.
* `longhorn-manager` -- (1 под, 2 контейнера) Основной компонент Longhorn. Управляет узлами, томами, репликами,
снапшотами и синхронизацией данных. Один под на узел, 2 контейнера (основной + kube-rbac-proxy для авторизации).
* `longhorn-ui` -- (2 реплики) Предоставляют веб-интерфейс для управления Longhorn (графики, настройка томов,
* мониторинг). Две реплики для отказоустойчивости, хотя на одном узле это избыточно.
## Подключение второго узла (мастер)
Для начала, на первой ноде получим токен для подключения нового узла к кластеру:
```bash
sudo cat /var/lib/rancher/k3s/server/node-token
```
Вывод будет что-то вроде `K10...::server:longrandomstring`. Это и есть токен, который нужно будет использовать.
Вывод будет что-то вроде `K10::server:longrandomstring`. Это и есть токен, который нужно будет использовать.
Теперь на втором Orange Pi (например, с IP 192.168.1.28) можно запустить второй мастер-узел (вставим токен
из предыдущего шага):
@@ -176,9 +324,9 @@ sudo cat /var/lib/rancher/k3s/server/node-token
curl -sfL https://get.k3s.io | sh -s - server --server https://192.168.1.27:6443 --token <ТОКЕН> --tls-san=192.168.1.28
```
Здесь ключи:
* `--server https://192.168.1.27:6443` -- указывает на API мастер-узла, чтобы наш новый узел мог подключиться к кластеру.
* `--server https://192.168.1.27:6443` указывает на API мастер-узла, чтобы наш новый узел мог подключиться к кластеру.
* `--token` — токен аутентификации из предыдущего шага.
* `--tls-san=192.168.1.28` -- добавляет IP нашего второго мастера в сертификаты (для будущих подключений).
* `--tls-san=192.168.1.28` добавляет IP нашего второго мастера в сертификаты (для будущих подключений).
Проверим какие теперь ноды в кластере:
```bash
@@ -210,7 +358,7 @@ kube-system svclb-traefik-4f8c2580-xzt5d 2/2 Running 0
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 2h 10.42.0.8 opi5plus-2 <none> <none>
```
Как видим, у нас появился еще один `svclb-traefik` на второй ноде. Это под -- Service Load Balancer (SLB) для Traefik.
Как видим, у нас появился еще один `svclb-traefik` на второй ноде. Это под Service Load Balancer (SLB) для Traefik.
Он эмулирует облачный балансировщик нагрузки (типа AWS ELB), которого нет в локальном окружении вроде Orange Pi.
SLB перенаправляет внешний трафик (например, на порты 80/443) к сервисам типа LoadBalancer внутри кластера.
@@ -226,7 +374,7 @@ curl -sfL https://get.k3s.io | sh -s - agent --server https://192.168.1.10:6443
```
Здесь ключ:
* `agent` -- устанавливает узел в режиме воркера (worker). Это значит, что узел будет выполнять рабочие нагрузки
* `agent` устанавливает узел в режиме воркера (worker). Это значит, что узел будет выполнять рабочие нагрузки
(поды), но не будет управлять кластером (без *control-plane*, *master* и на нем нет реплики *etcd*).
Посмотрим на ноды (команда выполняется на одном из мастер-узлов):
@@ -276,10 +424,9 @@ metrics-server ClusterIP 10.43.248.208 <none>
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 3d
```
Можем проверить доступность панели `Traefik` через браузер через IP-адрес нового узла и (в нашем случае `http://192.168.1.26:9000/dashboard/#/`)
и увидим, что балаансировщик работает и перенаправляет трафик и с ноды воркера.
Можем так же настроить [web-панель Traefik](../kubernetes/k3s-setting-up-web-access-to-dashboard.md).
![k3s--traefic-dashboard.png](../images/k3s--traefic-dashboard.png)
![k3s--traefic-dashboard.png](../images/k3s--traefik-dashboard.png)
Что ж, теперь у нас есть кластер k3s с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не
идеальная конфигурация, так как у нас четное количество мастер-узлов.
@@ -301,7 +448,7 @@ traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.16
им не нужен доступ к etcd в реальном времени.
В чем может быть смысл иметь два мастера? Это обеспечивает репликацию данных (второй хранит копию etcd), но не
даёт отказоустойчивости -- когда один мастер упал, кластер становится неуправляемым (нет управления через kubectl),
даёт отказоустойчивости когда один мастер упал, кластер становится неуправляемым (нет управления через kubectl),
рабочие нагрузки (поды) могут продолжать работать, пока жив хотя бы один узел, но новые изменения (развертывание
подов и обновления) невозможны.
@@ -399,7 +546,7 @@ traefik-6c979cd89d-z6wwm 1/1 Running 0 2
```
Хотя, в целом, кластер остается рабочим, и сам чинится при отключении и восстановлении узлов, но если отключается нода
на которой исполняется под с `coredns` -- то временно будет затруднен перезапуска и создание новых подов, а значит
на которой исполняется под с `coredns` то временно будет затруднен перезапуска и создание новых подов, а значит
и "переезд" подов с погашенного узла, до восстановления `coredns` тоже будет замедлен. Кроме того, если сценарий
приложения(ий) развернутых внутри k3s предполагает переподключение с использованием имен подов или обнаружение подов,
то это тоже перестанет работать.
@@ -410,12 +557,12 @@ sudo k3s kubectl edit deployment coredns -n kube-system
```
Здесь:
* `kubectl edit` -- Открывает редактор (по умолчанию *vim*) для изменения ресурса Kubernetes напрямую в кластере.
* `kubectl edit` Открывает редактор (по умолчанию *vim*) для изменения ресурса Kubernetes напрямую в кластере.
Вместо создания локального YAML-файла и применения его через `kubectl apply`, мы сразу редактируем "живой" конфиг.
* `deployment coredns` -- Указывает, что редактируем объект типа *deployment* с именем `coredns`. Deployment — это
* `deployment coredns` Указывает, что редактируем объект типа *deployment* с именем `coredns`. Deployment — это
контроллер, который управляет набором подов (в данном случае coredns), обеспечивая их количество (реплики),
перезапуск и обновления.
* `-n kube-system` -- Указывает пространство имён (namespace), где находится *coredns8. В k3s системные компоненты,
* `-n kube-system` Указывает пространство имён (namespace), где находится *coredns8. В k3s системные компоненты,
к которым относится *coredns(, обычно живут в kube-system.
В открывшемся окне найдем строку `replicas: 1` и заменим её на `replicas: 2`.
@@ -431,7 +578,7 @@ spec:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-ccb96694c-n4qsp 0/1 ContainerCreating 0 5s <none> opi5plus-1 <none> <none>
coredns-ccb96694c-wzh96 1/1 Running 0 3h10m 10.42.1.8 opi5plus-3 <none> <none>
...
```
А затем:
@@ -439,7 +586,7 @@ coredns-ccb96694c-wzh96 1/1 Running 0
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-ccb96694c-n4qsp 1/1 Running 0 15s 10.42.2.6 opi5plus-1 <none> <none>
coredns-ccb96694c-wzh96 1/1 Running 0 3h10m 10.42.1.8 opi5plus-3 <none> <none>
...
```
**Как это будет работать?** Обе реплики `coredns` привязаны к сервису `kube-dns` в пространстве имён `kube-system`.
@@ -455,10 +602,10 @@ sudo k3s kubectl get endpoints kube-dns -n kube-system
гасили при экспериментах с устойчивостью кластера:
```text
NAME ENDPOINTS AGE
kube-dns 10.42.1.8:53,10.42.2.6:53,10.42.1.8:53 + 3 more... 5d23h
kube-dns 10.42.1.8:53,10.42.2.6:53,10.42.1.8:53 + 3 more 5d23h
```
Каждый под `coredns` -- самостоятельный DNS-сервер. Они не взаимодействуют друг с другом и не обмениваются данными. Это
Каждый под `coredns` самостоятельный DNS-сервер. Они не взаимодействуют друг с другом и не обмениваются данными. Это
просто экземпляры одного и того же сервиса, работающие параллельно. Они независимы, получают данные из API Kubernetes
и отвечают на запросы параллельно. В каждом поде кластера в качестве DNS настроен `kube-dns` (задаётся в файле
`/etc/resolv.conf` внутри пода). Когда под отправляет DNS-запрос, его получит `kube-dns` и перенаправит запрос
@@ -488,6 +635,10 @@ rpi3b Ready <none> 27s v1.31.6+k3s1
официальной поддержки Kubernetes/k3s для узлов на macOS (можно использовать обходные пути с использованием виртуальныех
машин).
> **На всякий случай:**
>
> Если для вашего Kubernetes-кластера требуется блочное хранилище `longhorn` (для обеспечения репликации файлов между узлами кластера и высокой доступности данных), то понадобится модуль `iSCSI` (_Internet Small Computer System Interface_) на уровне системы. В составе Ubuntu 22.04 для Orange Pi 5 этого модуля нет. Потребуется [компиляция ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md).
### Добавление узлов во "внешнем" интернете
В моем проекте (специализированном поисковике) будет нужно парсить и интернет сайты, включая заблокированные сайты.
@@ -495,6 +646,8 @@ rpi3b Ready <none> 27s v1.31.6+k3s1
не сработает. Выходом может стать использование воркер-узлов во внешнем интернете. Идея в том, что если какой-нибудь
URL не получится обработать на поде одного узла, то можно попробовать обработать его на другом узле, в другой локации.
#### Настройка Keepalived
Так как узлы k3s взаимодействуют через API на 6443-порте, то для доступа к кластеру из внешнего интернета нужно будет
обеспечить проброс трафика через роутер сети на один из мастер-узлов. НО у нас три мастер-узла, а значит если упадет
узел на который происходит проброс, то удаленный воркер-узел "отвелится" и потеряет доступ к кластеру. Объединить
@@ -514,8 +667,8 @@ sudo apt install keepalived
sudo nano /etc/keepalived/keepalived.conf
```
На первом мастер-узле (хост -- `opi5plus-1`, IP -- `192.168.1.26`):
```text
На первом мастер-узле (хост `opi5plus-1`, IP `192.168.1.26`):
```pycon
vrrp_instance VI_1 {
state MASTER # ЭТО ГЛАВНЫЙ ХОСТ. ПО УМОЛЧАНИЮ ТРАФИК С VIP БУДЕТ ПЕРЕНАПРАВЛЯТЬСЯ НА ЭТОТ ХОСТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
@@ -533,8 +686,8 @@ vrrp_instance VI_1 {
}
```
На втором мастер-узле (хост -- `opi5plus-2`, IP -- `192.168.1.27`):
```text
На втором мастер-узле (хост `opi5plus-2`, IP `192.168.1.27`):
```pycon
vrrp_instance VI_1 {
state BACKUP # ЭТО ВТОРОЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ ХОСТ УПАДЕТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
@@ -552,8 +705,8 @@ vrrp_instance VI_1 {
}
```
И, наконец, на третьем мастер-узле (хост -- `opi5plus-3`, IP -- `192.168.1.28`):
```text
И, наконец, на третьем мастер-узле (хост `opi5plus-3`, IP `192.168.1.28`):
```pycon
vrrp_instance VI_1 {
state BACKUP # ЭТО ТРЕТИЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ- И БЭКАП-ХОСТ УПАДЕТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и этот узел подключен по enP4p65s0
@@ -584,15 +737,15 @@ ip addr show
то увидим:
```text
...
...
2: enP4p65s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether c0:74:2b:fd:42:3c brd ff:ff:ff:ff:ff:ff
inet 192.168.1.26/24 brd 192.168.1.255 scope global dynamic noprefixroute enP4p65s0
valid_lft 68779sec preferred_lft 68779sec
inet 192.168.1.200/32 scope global enP4p65s0
valid_lft forever preferred_lft forever
...
```
Обратите внимание на виртуальный IP-адрес `192.168.1.200` находится в другой подсети (CIDR) и имеет маску `/32` (то
есть с маской подсети `255.255.255.255`). Это "точечная" подсеть, содержащая только один адрес, не привязан к основной
@@ -648,9 +801,9 @@ vps-sw-eye Ready <none> 35m v1.31.6+k3s1
Таким образом, для управления удаленным узлом нужно чтобы он имел локальный IP-адрес в домашней сети, а не внешний.
SSH-тоннель с помощью `autossh` и упаковкой UDP-трафика в TCP через `socat` не сработает (а я надеялся). Таким образом
"пробросить" Flannel для полноценного подключения удаленного k3s-узла -- VPN-туннель между каждой мастер-нодой на
удаленный узел. Это вполне рабочия вариант, если удаленные узлы -- полноценные и произвольные хосты. Но в моём
случае удаленный узел -- хост на 1 ядро и 1 ГБ ОЗУ. К тому же он на платформе x86_64, а не ARM, а значит ради одного
"пробросить" Flannel для полноценного подключения удаленного k3s-узла VPN-туннель между каждой мастер-нодой на
удаленный узел. Это вполне рабочия вариант, если удаленные узлы полноценные и произвольные хосты. Но в моём
случае удаленный узел хост на 1 ядро и 1 ГБ ОЗУ. К тому же он на платформе x86_64, а не ARM, а значит ради одного
узла не стоит заморачиваться с VPN.
Другим вариантом является подключение внутри самих подов на удаленном узле к необходимым сервисам напрямую. Но таким
@@ -658,5 +811,30 @@ SSH-тоннель с помощью `autossh` и упаковкой UDP-тра
-----
Cертификаты
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
PVC Longhorn
sudo kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
Установить Helm (менеджер пакетов для Kubernetes):
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Добавить репозиторий Traefik в Helm:
helm repo add traefik https://helm.traefik.io/traefik
Обновить репозитории:
helm repo update
Установить Traefik:
```bash
helm install traefik traefik/traefik -n kube-system --set service.type=LoadBalancer
```

View File

@@ -9,14 +9,14 @@ SSD NVMe, и для eMMC. Но, как и всякая магия, она мож
## Установка накопителей
Выключим Orange Pi 5 Plus и установим в него eMMC-носитель...
Выключим Orange Pi 5 Plus и установим в него eMMC-носитель
| Фото до и после установки eMMC. Внимание, устанавливайте до щелчка с обоих сторон! |
|:-------------------------------------------------------------------------------------|
| ![Orange Pi 5 Plus без eMMC](../images/orange-pi--photo--without-emmc.webp) |
| ![Orange Pi 5 Plus c установленным eMMC](../images/orange-pi--photo--with-emmc.webp) |
...и/или SSD-накопитель NVMe
и/или SSD-накопитель NVMe
| Фото до и после установки NVMe. Более просто в установке. Для надёжности закрепить винтиком. |
|:---------------------------------------------------------------------------------------------------|

View File

@@ -0,0 +1,399 @@
# Сборка ядра Linux для Orange Pi 5 Plus (и других устройств на Rockchip RK3588) с поддержкой iSCSI
Для моего проекта (k3s на Orange Pi 5 Plus) потребовалась поддержка система управления блочным хранилищем 'longhorn'
(longhorn.io). _Longhorn_ позволяет создавать надёжные тома для контейнеров, автоматически реплицируя их между узлами
и обеспечения высокую доступности приложений (если один узел выходит из строя, данные остаются доступны на репликах
на других узлах).
Для _longhorn_ нужен `iSCSI` (**Internet Small Computer System Interface**) — сетевой протокол, который по TCP/IP
подсоединяет удалённые диски так, будто они подключены физически. Longhorn через iSCSI обеспечивает передачу
данных между узлами по сети.
В ядре Linux поддержка _iSCSI_ реализована через модули, такие как `iscsi_tcp` и `scsi_transport_iscsi`. Однако,
в большинстве дистрибутивов для одноплатных компьютеров, включая и Orange Pi 5 Plus, эти модули отключены. Это делают
для экономии ресурсов, да и в целом, это нетипичный сценарий использования Orange Pi. В версии Ubuntu 22.04
для Orange Pi 5 Plus (в ядре от Xunlong), поддержка iSCSI тоже отключена, и это делает невозможным запуск Longhorn
из коробки.
**А значит нужно компиляция ядра (сбока)**
Мы вручную включим поддержку _iSCSI_ (модули `iscsi_tcp` и `scsi_transport_iscsi`), и cможем настроить
ядро под свои задачи. Однако компиляция и его замена требует осторожности и внимательности, ошибка в процессе может
привести к тому, что система не загрузится (но мы сделаем резервную копию текущего ядра, загрузчика, чтобы можно
было восстановить систему).
Обновления системы (например, через `apt`) с новым ядром будет проходить, как обычно: пакеты, утилиты и приложения
обновятся без проблем. Однако пересобранное ядро больше не будет автоматически обновляться через стандартные средства
дистрибутива. Если выйдет новая версия ядра, то придётся вручную повторить процесс компиляции. К счастью,
поставщик (это Xunlong) обновляет ядро не часто.
| ✎ Заметка |
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Orange Pi 5 Plus использует процессор **Rockchip RK3588** (как и многие другие устройства под брендом Orange). Процесс компиляции ядра для всех RK35xx почти идентичен, и базируется на одном и том же дистрибутиве. Однако, разные периферийные устройства: например, GPIO, USB или Ethernet-контроллеры могут отличаться. Мы будем использовать `.config` от текущего, рабочего ядра, так что настройки будут актуальны. Но следует иметь в виду, что возможно, в будущем могут появиться изменения в конфигурации, дистрибутива и т.п. Следует учитывать, это и **никто ничего не гарантирует**. |
## План сборки ядра с iSCSI
* Резервное копирование текущего ядра, загрузчика, дерева устройств, /boot и т.п.
* Установить инструментарий для настройки и компиляции ядра
* Скачать исходники.
* Настроить конфиг для включения модулей iSCSI (и мониторинга NVMe).
* Скомпилировать ядро и модули.
* Установить новое ядро.
* Перезагрузиться в новое ядро.
* Проверка.
## Резервное копирование текущего ядра и /boot
Архивируем том `/boot` на нашем Orange Pi 5 Plus во временный каталог `/tmp`:
```bash
sudo tar -czf /tmp/boot-backup.tar.gz /boot
```
Проверим, что архив создался:
```bash
ls -al /tmp/boot-backup.tar.gz
```
Увидим что архив создался:
```text
-rw-r--r-- 1 root root 62116591 мар 23 20:33 /tmp/boot-backup.tar.gz
```
Теперь нам нужно скопировать его на какой-нибудь внешний хост (или носитель, но я буду копировать на хост другого
компьютера). Если что-то пойдёт не так (например, после обновления ядра система не загрузится) можно будет восстановить
файлы из этого архива. Мой Orange Pi 5 Plus, на котором я буду компилировать ядро имеет хост — `opi5plus-3` (замени
на свой хост), а имя пользователя от которого я работаю — `opi` (замени на свой). На рабочем компьютере, с которого
я захожу по SSH на Orange, у меня есть папка `~/backup/` (`mkdir -p ~/backup`). Скачиваю в неё архив:
```bash
scp opi@opi5plus-3.local:/tmp/boot-backup.tar.gz ~/backup/opi5plus-3-boot-backup.tar.gz
```
### Что в архиве?
Если что-то пойдет не так, нам надо знать какие файлы в `/boot` важны и для чего предназначены. Вот список файлов
('ls -al /boot'):
```text
drwxr-xr-x 4 root root 4096 янв 1 1970 ./
drwxr-xr-x 19 root root 4096 фев 16 19:59 ../
-rwxr-xr-x 1 root root 230456 ноя 21 09:56 boot.bmp*
-rwxr-xr-x 1 root root 3341 ноя 21 09:55 boot.cmd*
-rwxr-xr-x 1 root root 3413 ноя 21 10:04 boot.scr*
-rwxr-xr-x 1 root root 230393 ноя 21 07:08 config-6.1.43-rockchip-rk3588*
drwxr-xr-x 3 root root 4096 ноя 21 09:55 dtb/
drwxr-xr-x 3 root root 4096 ноя 21 09:55 dtb-6.1.43-rockchip-rk3588/
-rwxr-xr-x 1 root root 40579584 ноя 21 07:08 Image*
-rwxr-xr-x 1 root root 15462140 мар 21 16:36 initrd.img-6.1.43-rockchip-rk3588*
-rwxr-xr-x 1 root root 1152056 ноя 21 09:56 logo.bmp*
-rwxr-xr-x 1 root root 0 ноя 21 09:55 .next*
-rwxr-xr-x 1 root root 178 фев 16 19:59 orangepiEnv.txt*
-rwxr-xr-x 1 root root 1542 ноя 21 09:56 orangepi_first_run.txt.template*
-rwxr-xr-x 1 root root 6209932 ноя 21 07:08 System.map-6.1.43-rockchip-rk3588*
-rwxr-xr-x 1 root root 15462204 мар 21 16:36 uInitrd*
-rwxr-xr-x 1 root root 40579584 ноя 21 07:08 vmlinuz-6.1.43-rockchip-rk3588*
```
Ключевые файлы:
* `Image` — собственно ядро Linux (в данном случае версия 6.1.43 для Rockchip RK3588). При старте компьютера загрузчик
U-Boot загрузит его в память и передаст ему управление. Без этого файла система не запустится.
* `vmlinuz-6.1.43-rockchip-rk3588` — копия ядра (в системе уже есть резервная копия).
* `dtb/` — Каталог файлами **Device Tree Blob** (`DTB`). Это бинарные файлы, описывающие аппаратное обеспечение устройства.
Для Orange Pi 5 Plus используется файл вроде `rk3588-orangepi-5-plus.dtb` (находится в подкаталоге `dtb/rockchip/`).
DTB передаётся ядру, чтобы оно знало, как работать с процессором (количество ядер, частоты), памятью (RAM, её объём
и адреса), периферией (UART, I2C, SPI, Ethernet, USB, GPIO, прерывания и тому подобное). На ARM-устройствах
(в отличие от x86, где есть ACPI) нет стандартного способа обнаружения оборудования. DTB решает эту проблему,
предоставляя ядру "карту" железа. U-Boot загружает DTB из `/boot/dtb/ `и передаёт его ядру при старте. Кстати,
в подкаталоге dtb/rockchip/ есть `overlay/` — это дополнительные файлы, которые могут использоваться для добавления
и/или изменения функциональности устройства. Например, можно добавить поддержку новых периферийных устройств (камеру,
дисплей и т.п.).
* `uInitrd` — Начальный RAM-диск (initrd), адаптированный для U-Boot. Содержит модули и скрипты, необходимые для
пред-загрузки (выбор накопителя, монтирование корневой файловой системы и т.п.). Если он повреждён или несовместим
с ядром, загрузка может упасть на этапе инициализации.
* `orangepiEnv.txt` — Конфигурационный файл для U-Boot. Здесь задаются параметры загрузки, такие как путь расположение
дерева DTB, UUID корневой файловой системы, тип файловой системы… Без правильных настроек в этом файле U-Boot не
найдёт нужные для загрузки файлы.
* `boot.scr` — Скрипт загрузки для U-Boot. Содержит команды для загрузки ядра, initrd и DTB. U-Boot выполняет его
при старте системы. Этот файл создаётся из `boot.cmd` с помощью утилиты `mkimage`. Если он повреждён или отсутствует,
U-Boot не сможет загрузить систему.
* `dtb-6.1.43-rockchip-rk3588/` — Копия каталога `dtb/`, обычно появляется, когда ядро устанавливается или обновляется
через пакетный менеджер (например, `apt`). Она привязана к конкретной версии ядра — в данном случае
`6.1.43-rockchip-rk3588`, для того, чтобы: Хранить DTB-файлы, соответствующие этой версии ядра и избегать конфликты
между DTB от разных версий ядра (если используется несколько ядер на одной системе).
Менее критичные, но полезные файлы:
* `config-6.1.43-rockchip-rk3588` — Конфигурация ядра, использованная при его сборке (он нам понадобится, чтобы
пересобрать ядро с iSCSI).
* `System.map-6.1.43-rockchip-rk3588` — Карта меток (символов) ядра, полезна для отладки.
* `initrd.img-6.1.43-rockchip-rk3588` — Обычный initrd, из которого генерируется uInitrd.
* `boot.bmp` и `logo.bmp` — Изображения для экрана загрузки (не влияют на работу системы).
## Устанавливать инструменты для сборки ядра
Обновим репозитории и установим минимальный инструментарий для сборки ядра:
```bash
sudo apt update
sudo apt install -y build-essential bc flex bison libssl-dev libncurses-dev git
```
Нам понадобятся следующие пакеты:
* `build-essential` — Включает `gcc` (для ARM64, make и другие базовые инструменты компиляции.
* `bc` — Утилита для точных математических вычислений в командной строке. Используется для вычислений в скриптах сборки ядра.
* `flex` и `bison` — генератор лексических анализаторов и парсер. Нужны для обработки конфигурационных файлов ядра.
* `libssl-dev` — Для поддержки криптографии в ядре и `OpenSSL`.
* `libncurses-dev` — Библиотека для создания текстовых интерфейсов в терминале. Необходимо для работы интерфейса
`menuconfig` при настройке параметров ядра.
* `git` — Для клонирования исходного кода ядра из репозитория Xunlong.
> **Примечание:** Если вы хотите собрать ядро на x86-системе, установите кросс-компилятор `gcc-aarch64-linux-gnu` (`sudo apt install gcc-aarch64-linux-gnu`) и используйте его вместо обычного gcc в командах сборки. Он позволит собрать ядро для ARM64-архитектуры на x86-системе.
## Скачивание и подготавливаем исходники ядра
Клонируем репозиторий Orange Pi для ядра 6.1 из репозитория Xunlong. Мы будем брать ветку (branch)
`orange-pi-6.1-rk35xx`. Это ветка с исходниками для всех устройств на базе Rockchip RK35xx (включая Orange Pi 5 Plus):
```bash
git clone --depth 1 https://github.com/orangepi-xunlong/linux-orangepi -b orange-pi-6.1-rk35xx ~/linux-orangepi
```
После клонирования репозитория, переходим в каталог с ним:
```bash
cd ~/linux-orangepi
```
И первым делом в `Makefile` установим `EXTRAVERSION` (модификатор версии ядра). Узнаем текущую версию ядра:
```bash
uname -r
```
Я на своем Orange Pi 5 Plus вижу (у вас может быть другая версия):
```text
6.1.43-rockchip-rk3588
```
Открываем `Makefile` в текстовом редакторе:
```bash
nano Makefile
```
И устанавливаем модификатор версии ядра, строку `XTRAVERSION =` (у меня это пятая строчка):
```text
EXTRAVERSION = -rockchip-rk3588
```
Затем копируем конфиг текущего ядра в конфиг для сборки.
```bash
zcat /proc/config.gz > .config
```
`.config` содержит все текущие настройки ядра. Это очень удобно, так как нужно добавить только поддержку нужных модулей
(в нашем случае `iSCSI`), а не настраивать всё с нуля.
## Настройка конфига с iSCSI
Теперь нужно отредактировать, `.config` чтобы включить `iSCSI`. Заходим в интерфейс конфигуратора ядра `menuconfig`:
```bash
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
```
Используя стрелки на клавиатуре, чтобы двигаться по пунктам меню.
1. Перейди в `Device Drivers` → `SCSI device support` → `SCSI Transports`.
2. Найти внутри `iSCSI Transport Attributes`, с помощью стрелок выбрать его.
3. Нажимает `M` (для модуля) — появится `<M>` рядом с пунктом. Это означает, что модуль будет собираться как модуль,
а не встраиваться в ядро. Модуль (`iscsi_tcp.ko`) проще протестировать без перезагрузки.
4. В нижнем меню выбрать `<Save>` и подтвердим сохранение в `.config`.
6. Вернуться в меню выше `SCSI device support` (через нижнее меню `<Exit>`).
7. Зайти в раздел `SCSI low-level drivers`.
8. Выбрать `iSCSI Initiator over TCP/IP` и нажать `M` (для модуля).
9. Снова сохраняем конфиг в `.config` (через нижнее меню `<Save>`).
10. Выйти из `menuconfig` (через нижнее меню `<Exit>`, три раза).
| ✎ Может быть полезно: |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| На моём Orange Pi 5 Plus загрузка идет с NVMe диска ([см. как настроить](opi5plus-move-system-to-nvme-or-emmc.md)), и мне было полезно так же включить ещё и систему мониторинга NVMe: `Device Drivers` → `NVME Support`, найти пункт `SNVMe hardware monitoring` и включить его. В нижнем меню выбрать `<Save>` и подтвердим сохранение в `.config`. Выйти из `menuconfig` (через нижнее меню `<Exit>`, три раза). Теперь у меня будет возможность отслеживать состояние NVMe-диска (температуру, износ и т.д.) через инструменты вроде `nvme-cli`. |
Проверим, что iSCSI включён:
```bash
grep -E "ISCSI_TCP|SCSI_ISCSI_ATTRS|ISCSI_BOOT" .config
```
Должны увидеть что-то типа:
```text
CONFIG_SCSI_ISCSI_ATTRS=m
CONFIG_ISCSI_TCP=m
# CONFIG_ISCSI_BOOT_SYSFS is not set
```
| ✎ Проверка, что включен мониторинг NVMe (если включали): |
|---------------------------------------------------------------|
| `grep -E "NVME_HWMON" .config` и увидим `CONFIG_NVME_HWMON=y` |
## Компиляция ядра и модулей
Запускаем компиляцию ядра и модулей:
```bash
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8
```
`-j8` — это количество потоков, используем столько, сколько у нас ядер. На Orange Pi 5 Plus восемь ядер. Но, например,
если у вас четыре ядра, то `-j4`.
Компиляция займёт 20-40 минут (Orange Pi 5 Plus).
Установим модули ядра:
```bash
sudo make modules_install
```
После завершения ядро будет находиться в каталоге `~/linux-orangepi/arch/arm64/boot/` (файл `Image`), а дерево описания
устройства в `~/linux-orangepi/arch/arm64/boot/dts/rockchip/` (наш файл для Orange Pi 5 Plus --
`rk3588-orangepi-5-plus.dtb`, дле версий 5, 5 Max, AI, Ultra соответствующие файлы там же).
Проверим, что ядро скомпилировалось:
```bash
ls -l arch/arm64/boot/Image
```
Ожидаемый размер: около 40 МБ:
```text
-rw-rw-r-- 1 opi opi 40579584 Mar 22 19:09 arch/arm64/boot/Image
```
Размер должен почти совпасть (или полностью совпадать) с размером текущего ядра:
```bash
ls -l /boot/Image
```
```text
-rwxr-xr-x 1 root root 40579584 Nov 21 07:08 /boot/Image
```
И это не ошибка. Мы добавили только `CONFIG_ISCSI_TCP=m` и `CONFIG_SCSI_ISCSI_ATTRS=m`. Это подключит модули (`iscsi_tcp.ko`),
но само ядро (`Image`) почти не увеличивается, ведь `.ko`-модуль iSCSI не встраивается в ядро (мы выбрали `<M>`,
а не `<*>`). Кроме того, Linux выравнивает размеры файлов по блокам (например, 4 Кб), и добавление
пары небольших модулей могут не повлиять на итоговый размер.
## Устанавливаем новое ядро рядом со старым
Мы должны скопировать два файла в `/boot`. Первый — это `Image`. Это само ядро, и оно скомпилировалось в каталог
`~/linux-orangepi/arch/arm64/boot/`. Второй файл — это `rk3588-orangepi-5-plus.dtb`. Он находится
в `~/linux-orangepi/arch/arm64/boot/dts/rockchip/`. Как было сказано выше, это дерево описания оборудования
(Device Tree Blob), и нужно при загрузке ядра. Кстати, при компиляции мы получили `.dtb` для всех моделей Orange Pi 5
(5, 5 Plus, 5 Max, AI, Ultra). Для моей модели (Orange Pi 5 Plus) нужен `rk3588-orangepi-5-plus.dtb`, а вы можете
взять другой, для своей модели.
Копируем новые файлы в `/boot` с уникальными именами (с суффиксом `-iscsi`):
```bash
sudo cp ~/linux-orangepi/arch/arm64/boot/Image /boot/Image-iscsi
sudo cp ~/linux-orangepi/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dtb /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-iscsi
```
Старое ядро и дерево описания переименуем (это будет резервная копия, с суффиксом `-original`):
```bash
sudo mv /boot/Image /boot/Image-original
sudo mv /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-original
```
Устанавливаем новые ядро и дерево:
```bash
sudo cp /boot/Image-iscsi /boot/Image
sudo cp /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-iscsi /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb
```
> **Важно**: Не обновляйте `uInitrd`, если текущий работает, — его замена может сломать загрузку системы.
Все готово.
## Перезагрузка в новое ядро
Тут все просто:
```bash
sudo reboot
```
#### Если что-то пошло не так и система не загрузилась
Если вы все делали правильно, то такого быть не должно. Но, тем не менее, если загрузка не произошла, то это может
выглядеть двумя способами:
* Система зависает на этапе загрузки: синенький огонек на Orange Pi не загорается и Ethernet тоже не мигает —
_вы неправильно собрали ядро или испортили загрузчик.
* Система, вроде как, грузится, но все никак… огоньки весело мигают, но не получается подключиться ни по SSH, ни
другим способом, пинги на IP-адрес не проходят — вы забыли подключить модули, накосячили с конфигом или с `.dtb`_.
Чтобы починить, загружайтесь с MicroSD-карты (не забудьте отключить питание перед тем как вставить MicroSD-карту).
Затем смонтируйте, в зависимости где у вас система, eMMC:
```bash
mkdir -p /mnt/emmc
mount /dev/mmcblk2p1 /mnt/emmc
cd /mnt/emmc
```
…или NVMe:
```bash
mkdir -p /mnt/nvme
mount /dev/nvme0n1p1 /mnt/nvme
cd /mnt/nvme
```
И восстанавливаем старое ядро и дерево описания:
```bash
sudo cp Image-original Image
sudo cp dtb/rockchip/rk3588-orangepi-5-plus.dtb-original dtb/rockchip/rk3588-orangepi-5-plus.dtb
```
Выключаем систему:
```bash
sudo poweroff
```
Вынимаем MicroSD-карту, включаем, и загрузиться старая система.
## Проверка нового ядра
Если же иситма поле обновления ядра загрузилась, проверим текущую версию ядра:
```bash
uname -r
```
Она должна остаться той же:
```text
6.1.43-rockchip-rk3588
```
Проверим, работает ли iSCSI:
```bash
sudo systemctl status iscsid
```
Увидим что-то вроде:
```text
● iscsid.service - iSCSI initiator daemon (iscsid)
Loaded: loaded (/lib/systemd/system/iscsid.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2025-03-24 17:51:59 MSK; 28min ago
TriggeredBy: ● iscsid.socket
Docs: man:iscsid(8)
Main PID: 1070 (iscsid)
Tasks: 2 (limit: 18977)
Memory: 4.6M
CPU: 94ms
CGroup: /system.slice/iscsid.service
├─1069 /sbin/iscsid
└─1070 /sbin/iscsid
мар 24 17:51:58 opi5plus-3 systemd[1]: Starting iSCSI initiator daemon (iscsid)...
мар 24 17:51:59 opi5plus-3 iscsid[1061]: iSCSI logger with pid=1069 started!
мар 24 17:51:59 opi5plus-3 iscsid[1069]: iSCSI daemon with pid=1070 started!
мар 24 17:51:59 opi5plus-3 systemd[1]: Started iSCSI initiator daemon (iscsid).
```
Как видим, `active (running)`, значит iSCSI работает.
| ✎ Заметка |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Если вы включили мониторинг NVMe, то проверим и его. Установим nvme-cli: `sudo apt install nvme-cli` и проверим состояние нашего накопителя `sudo nvme smart-log /dev/nvme0n1`. |
**ВСË!**
А, да, не забудьте удалить репозиторий с исходниками ядра, если он вам больше не нужен:
```bash
rm -rf ~/linux-orangepi
```