Compare commits

...

178 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
84a10dbd90 add: k3s (ss-client ....) 2025-03-21 12:42:06 +03:00
4ac540eaa9 add: k3s (ss-client ...) 2025-03-20 18:14:42 +03:00
e9443f8086 add: k3s (ss-client ..) 2025-03-18 17:56:42 +03:00
56f512ebf3 add: k3s (ss-client .) 2025-03-15 03:26:48 +03:00
03fc03bc6a add: k3s (ss-client) 2025-03-15 00:27:25 +03:00
92a9fd2bc6 add: k3s (img traefic dashboard) 2025-03-08 15:15:25 +03:00
3ea61d918c add: k3s (img traefic dashboard) 2025-03-08 15:15:10 +03:00
8634efc350 add: k3s (подключение внешнего
узла, part-fin)
2025-03-07 12:09:22 +03:00
a86d2d5ed7 add: k3s (подключение внешнего
узла, part-1)
2025-03-02 20:00:06 +03:00
ea88f3065a add: k3s (реплика coredn 2) 2025-02-28 17:13:00 +03:00
5d2e53d313 add: k3s (реплика coredns) 2025-02-27 18:42:31 +03:00
242f6ca5fb add: k3s (удаляем воркер-узел №3 и добавим его как мастер) 2025-02-27 14:54:59 +03:00
857d0dd7e1 ...: minor 2025-02-27 10:30:28 +03:00
ef04ea4e26 add: k3s (узел №3, воркер) 2025-02-25 17:37:40 +03:00
bbaf3c4014 add: k3s (второй мастер) 2025-02-25 13:17:15 +03:00
374f356435 add: k3s (1) 2025-02-23 15:09:12 +03:00
dc2dca04e1 add: k3s 2025-02-22 11:47:34 +03:00
def4abb094 mod: получение сертификата через работающий контейнер 2025-02-20 16:09:43 +03:00
1a72d6ea9b add: прокси хоста из docker 2025-02-20 14:00:23 +03:00
22538dcde5 add: docker-compose.yml 2025-02-14 15:57:31 +03:00
a64615386d add: Зависимости и хелсчеки (6) 2025-02-14 15:54:33 +03:00
9de84306d8 ...: 2025-02-14 10:55:02 +03:00
91fafb96cf add: Nginx и Certboot в Docker (5) 2025-02-14 10:24:57 +03:00
9064bad6d1 add: Nginx и Certboot в Docker (4) 2025-02-14 02:30:36 +03:00
2b9a7e7c7c add: Nginx и Certboot в Docker (4) 2025-02-14 02:26:33 +03:00
6470803172 add: Nginx и Certboot в Docker (3) 2025-02-14 02:16:18 +03:00
a149cab8c5 add: Nginx и Certboot в Docker (2) 2025-02-14 01:53:02 +03:00
bef2817577 add: Nginx в Docker (1) 2025-02-12 21:50:43 +03:00
c45661a2b8 add: установка ru-локали 2025-02-12 12:21:35 +03:00
8148cc2e65 add: . 2025-01-09 17:54:00 +03:00
21bf903d45 add: Ping nodes (-1-) 2025-01-05 15:52:26 +03:00
a62f875e60 add: Adjust nodes (-1-) 2025-01-05 14:31:23 +03:00
1a974ed53b add: join node (-1-) 2025-01-04 22:54:50 +03:00
9b646dfa28 add: join node 2025-01-04 22:52:52 +03:00
e6d139fb0e add: keepalived (-4-) 2025-01-04 01:36:31 +03:00
efd085e1e9 add: keepalived (-3-) 2025-01-02 16:37:35 +03:00
6132c774c3 add: keepalived (-2-) 2025-01-02 16:21:02 +03:00
02c8e7d9da add: keepalived (-1-) 2025-01-02 00:50:36 +03:00
7b45aa7762 add: gitignore 2025-01-01 18:14:30 +03:00
195b6928d5 add: cri-dockerd (_2_) 2024-12-31 19:23:54 +03:00
e55d4362d4 add: cri-dockerd (_1_) 2024-12-25 09:46:45 +03:00
c31ff0395a add: Вариант для старого ARMv7 2024-12-21 19:05:57 +03:00
1f96d09fc6 add: ssh 2024-12-20 23:56:29 +03:00
14b0f403fa add: отключаем системный swap 2024-12-19 23:18:33 +03:00
488b47eb17 add: модули и параметры ядра 2024-12-19 22:32:30 +03:00
54cf07242b add: ссылка 2024-12-17 23:40:40 +03:00
5c1a2dbf04 add: ссылка 2024-12-17 23:39:50 +03:00
8ff5cd3dd7 add: настройка времени 2024-12-17 23:20:39 +03:00
b30ebec857 add: настройка сети (2) и пакеты 2024-12-17 21:53:54 +03:00
e7d272c7d8 add: настройка сети 2024-12-17 01:16:43 +03:00
edaba86d24 add: CSI-коды 2024-12-08 18:41:02 +03:00
f17b5a62b7 add: CSI-коды 2024-12-08 18:40:04 +03:00
75a4c49d36 ...: минор 2024-12-08 17:51:37 +03:00
33125bac4f ...: минор 2024-12-08 17:50:56 +03:00
9d610804bc ...: минор 2024-12-08 17:50:17 +03:00
937d7e3167 add: развертывание Django-проекта в прод 2024-12-08 17:48:10 +03:00
4850f8d151 mod: url fix 2024-12-03 23:11:07 +03:00
3c342007e7 mod: Измерение скорости накопителя данных (fin) 2024-11-30 12:57:58 +03:00
3ee178b1e6 mod: Измерение скорости накопителя данных (3) 2024-11-30 12:33:32 +03:00
32b50a347e mod: Измерение скорости накопителя данных (2) 2024-11-30 00:56:25 +03:00
d3b6bb8476 add: Измерение скорости накопителя данных 2024-11-29 14:35:14 +03:00
74f74f091e add: Измерение скорости накопителя данных 2024-11-29 14:33:48 +03:00
1d11a62504 mod: OrangePi перенос системы на SSD NVMe или eMMC (fin) 2024-11-29 14:31:05 +03:00
934ed4e017 mod: OrangePi перенос системы на eMMC (fin+) 2024-11-29 01:06:37 +03:00
d86651d1d5 mod: k8s (2.) 2024-11-27 00:54:33 +03:00
888ce8a686 mod: k8s (2) 2024-11-27 00:53:45 +03:00
293bbd5de7 add: OrangePi перенос системы на eMMC (fin) 2024-11-24 16:15:13 +03:00
7e16b04f0a add: OrangePi перенос системы на eMMC (5+) 2024-11-24 13:48:05 +03:00
545040792a add: OrangePi перенос системы на eMMC (4+) 2024-11-24 13:44:31 +03:00
cb77d20583 add: OrangePi перенос системы на eMMC (3+) 2024-11-24 12:04:43 +03:00
8860ee5299 add: OrangePi перенос системы на eMMC (2+) 2024-11-21 22:58:25 +03:00
06f4298e63 add: OrangePi перенос системы на eMMC (2) 2024-11-21 22:53:14 +03:00
11c91eb3b0 add: OrangePi перенос системы на eMMC (1, начало) 2024-11-21 22:02:45 +03:00
980d3ffdb5 mod: minor 2024-11-20 18:32:38 +03:00
4243679bc5 ---: minor 2024-11-20 18:24:44 +03:00
4675399af6 add: 72 dpi 2024-11-20 18:22:25 +03:00
fb491e4c36 add: minor (разметка) 2024-11-20 18:12:29 +03:00
16b84a68ef add: Полечили предупреждения avahi о fe80::1 2024-11-20 18:10:37 +03:00
397d01d94e add: установка ключей Kubernetes 2024-11-18 01:15:17 +03:00
7a62738c59 add: возможно потребуется доустановить пакеты в систему 2024-11-18 01:00:30 +03:00
db379ecb6b minor 2024-11-17 01:10:33 +03:00
57862884c8 Kubernetes на Orange Pi 5 Plus под управлением Ubuntu 20.04 2024-11-17 01:08:44 +03:00
fef7a45268 Установка GPG-ключа для репозиториев Docker 2024-11-16 21:44:02 +03:00
90be311f72 минор 2024-11-12 10:39:26 +03:00
4f8ac49905 Python-скрипт как служба Linux 2024-11-12 10:37:10 +03:00
d80ad3d09c Настройка nginx как прямого прокси 2024-11-09 20:54:49 +03:00
d90b9e1fd0 Восстановление Orange Pi (Raspberry Pi) из backup 2024-11-09 19:59:27 +03:00
8dde0f5c75 minor 2024-11-09 19:29:03 +03:00
be86f430c6 minor 2024-11-09 19:28:43 +03:00
48 changed files with 12832 additions and 7 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea
# User-specific stuff # User-specific stuff
.idea/**/workspace.xml .idea/**/workspace.xml
.idea/**/tasks.xml .idea/**/tasks.xml

View File

@@ -2,20 +2,46 @@
## Docker ## Docker
* [Расположение образов Docker](docker/docker-adjasting.md) * [Расположение образов Docker](docker/docker-adjasting.md)
* [Установка GPG-ключа для репозиториев Docker](docker/docker-trusted-gpg.md)
* [Контейнер MariaDB/MySQL](docker/docker-mariadb.md) * [Контейнер MariaDB/MySQL](docker/docker-mariadb.md)
* [Nginx и letsencrypt в контейнерах](docker/docker-nginx-w-certbot.md) на примере Portainer
* [Контейнер MySQL под Windows 10](docker/docker-mysql-in-windows10.md) * [Контейнер MySQL под Windows 10](docker/docker-mysql-in-windows10.md)
* [Развертывание VPN-сервера на базе MS SSTP](docker/docker-sstp-vpn.md) * [Развертывание VPN-сервера на базе MS SSTP](docker/docker-sstp-vpn.md)
* [Развертывание прокси базе Shadowsocks (сервер и клиент)](docker/docker-shadowsocks.md) * [Развертывание прокси базе Shadowsocks (сервер и клиент)](docker/docker-shadowsocks.md)
## Kubernetes (k3s/k8s)
* [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md)
* [Под с 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-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
* [Python-скрипт как служба Linux](python/python_as_service.md)
## Raspberry Pi / Orange Pi
* [Установка Docker и Docker Compose](raspberry-pi/install-docker-compose.md)
* [Резервное копирование и восстановление](raspberry-pi/backup-restore.md)
## 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 ## Nginx / Apache
* [Ограничение доступа по User-Agent (на примере GPTBot)](nginx/nginx-ban-user-agent.md) * [Ограничение доступа по User-Agent (на примере GPTBot)](nginx/nginx-ban-user-agent.md)
* [Настройка nginx как прямого прокси](nginx/nginx_as_direct_proxy.md)
## Разное ## Разное
* [Настройка RU-локали в Ubuntu/Debian](misc/set-locale-ru.md)
* [Развертывание Django-приложения (сайта) на VDS-хостинге](misc/deploying-django-site-to-dvs-hosting.md)
* [Сплиттер для разделения логов](misc/splitter-for-logs.md) * [Сплиттер для разделения логов](misc/splitter-for-logs.md)

View File

@@ -0,0 +1,663 @@
# Веб-сервер Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker
Для удобного переноса сайтов или веб-приложений между серверами, а также для упрощения обновления и обслуживания
веб-сервера nginx, удобно держать его в контейнере Docker. В данной инструкции рассмотрено развертывание веб-сервера
Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker. В качестве примера сайта используется контейнер Portainer --
отличный инструмент для управления Docker-контейнерами через веб-интерфейс.
### Соглашения:
| | |
|-----------------------------|----------------------------------------------------------------------------------------|
| `web` | Пользователь от имени которого мы работаем |
| `/home/web` | Домашний каталог. |
| `/home/web/docker-data` | Каталог для хранения данных Docker-контейнеров (место куда монтируют тома контейнеров) |
| `portainer.you.domain.name` | Домен, по которому будет доступен Portainer |
| `email@you.domain.name` | Email для сертификатов Let's Encrypt | |
Чтобы не случилось путаницы во время копи-паста рекомендуется сразу произвести все замены в тексте.
## Portainer + nginx в контейнерах Docker
И так, для начала создадим каталог для хранения данных Portainer (можно опустить если вам не нужен Portainer):
```bash
mkdir -p /home/web/docker-data/portainer
```
Теперь создадим каталог для хранения конфигурационных файлов Nginx. Сам Nginx будет сидеть в контейнере, но
конфигурационные файлы, которые он будет использовать, находятся на хосте в каталоге `/home/web/docker-data/nginx/conf.d`:
```bash
mkdir -p /home/web/docker-data/nginx/conf.d
```
Теперь создадим файл конфигурации Nginx, который будет использоваться для проксирования запросов к контейнеру Portainer:
```bash
nano /home/web/docker-data/nginx/conf.d/portainer.conf
```
Вставьте в файл следующее содержимое:
```nginx configuration
server {
listen 80;
server_name portainer.you.domain.name;
location / {
proxy_pass http://portainer:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Что происходит в этом файле конфигурации:
- `listen 80;` — слушаем порт 80 (обычный HTTP-трафик);
- `server_name portainer.you.domain.name;` — имя хоста, по которому будет доступен Portainer (замените `you.domain.name`
на ваш домен);
- `proxy_pass http://portainer:9000;` — проксируем запросы в контейнер Portainer, который будет доступен по хосту (имени
контейнера `portainer`) и порту 9000;
- `proxy_set_header Host $host;` — передаем заголовок `Host` в запросе;
- `proxy_set_header X-Real-IP $remote_addr;` — передаем заголовок `X-Real-IP` в запросе;
- `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` — передаем заголовок `X-Forwarded-For` в запросе;
Сохраните файл и выйдите из редактора (`Ctrl + X`, затем `Y` для подтверждения).
Теперь создадим файл `docker-compose.yml` для развертывания контейнеров Nginx и Portainer:
```bash
nano /home/web/docker-data/docker-compose.yml
```
Вставьте в файл следующее содержимое:
```yaml
version: '3'
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
# Гасим порт 9000, чтобы он не светил на хост, а был доступен только во внутри-контейнерной сети
# ports:
# - "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/web/docker-data/portainer:/data
restart: always
networks:
- web
# Можно закомментировать строку выше и раскомментировать строки ниже если зачем-то нужен закрепленный IP-адрес
# web:
# ipv4_address: 172.20.0.10
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
restart: always
networks:
- web
networks:
web:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24 # Подсеть для пользовательской сети
```
Что у нас настроено в этом `docker-compose.yml`:
- `portainer` (у вас его может и не быть, или быть какой-то другой сервис, который вы будете производить):
- `image: portainer/portainer-ce:latest` — используем образ Portainer Community Edition;
- `container_name: portainer` — имя контейнера `portainer`;
- `volumes: ...` — монтируем файлы или тома изнутри контейнера на хост. В данном случае маппим: сокет —
чтобы изнутри контейнера Portainer можно было через Docker API управлять Docker самого хоста (вот так хитро) и
каталог `/home/web/docker-data/portainer` — чтобы сохранять настройки и данные Portainer между перезапусками;
- `restart: always` — автоматически перезапускаем контейнер при его остановке или перезагрузке хоста;
- `networks: ...` — подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
- `nginx`:
- `image: nginx:latest` — используем образ Nginx;
- `container_name: nginx` — имя контейнера `nginx`;
- `ports: ...` — пробрасываем порты 80 и 443 из контейнера на хост;
- `volumes: ...` — монтируем каталог с конфигурационными файлами Nginx;
- `restart: always` — автоматически перезапускаем контейнер при его остановке или перезагрузке хоста;
- `networks: ...` — подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
- `networks: ...`:
- `web`:
- `driver: bridge` — используем драйвер сети `bridge` (по умолчанию);
- `ipam: ...` — настраиваем IP-адреса для контейнеров внутри сети `web`. В данном случае используем подсеть
Сохраняем файл `docker-compose.yml` и выходим из редактора (`Ctrl + X`, затем `Y` для подтверждения).
Теперь развернем контейнеры Nginx и Portainer:
```bash
cd /home/web/docker-data
docker-compose up -d
```
После того как контейнеры запустятся, можно зайти в веб-интерфейс Portainer по адресу `http://portainer.you.domain.name`.
## Добавляем контейнер Let's Encrypt
Создадим каталог для хранения ключей сертификатов Let's Encrypt и временных файлов для проверки владения доменом:
```bash
mkdir -p /home/web/docker-data/letsencrypt
mkdir -p /home/web/docker-data/letsencrypt/_cert
mkdir -p /home/web/docker-data/letsencrypt/_ownership_check
```
Добавим в `docker-compose.yml` Certbot-контейнер (для получения сертификатов Let's Encrypt):
```yaml
certbot:
image: certbot/certbot:latest
container_name: letsencrypt-certbot
volumes:
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt # Для хранения сертификатов
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html # Для временных файлов Let's Encrypt
- /var/run/docker.sock:/var/run/docker.sock # Для доступа к Docker API и контейнерам хоста
networks:
- web
entrypoint: "/bin/sh -c 'apk add --no-cache curl && trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done'"
```
*Что тут происходит и зачем нам такие мапинги томов?*
1. Когда certbot запрашивает сертификат у Let's Encrypt, то тот требует подтверждения владения доменом. При работе
certbot в контейнере самый популярный (и лучший при работе в контейнере) способ подтверждения — это HTTP-проверка.
Certbot создает временные файлы в каталоге `/var/www/html/letsencrypt` (мы будем указывать этот каталог при инициализации
сертификата). Сервер же Let's Encrypt перед выдачей сертификата делает HTTP-запрос к этому временному файлу по URL
(например, в нашем случае по `http://portainer.you.domain.namey/.well-known/acme-challenge/`) и если файл доступен,
владение доменом считается подтвержденным, и сертификат выдается.
Таким образом, маппинг `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/html` позволяет certbot создавать
временные файлы в каталоге `/home/web/docker-data/letsencrypt/_ownership_check` хоста, и эти файлы nginx сможет
"отдать" при проверке со стороны Let's Encrypt.
Конечно, нам еще придется настроить nginx, чтобы он мог отдавать эти временные файлы. Но об этом чуть позже.
2. Мапинг `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` позволяет certbot сохранять получаемые
и обновляемые сертификаты Let's Encrypt в каталоге хоста `/home/web/docker-data/letsencrypt/_cert`, чтобы они не
пропадали при перезапуске контейнера certbot.
3. Мапинг `/var/run/docker.sock:/var/run/docker.sock` позволяет certbot управлять контейнерами Docker, чтобы он мог
перезапускать контейнеры (в нашем случае контейнер nginx) в случае обновления сертификатов. В принципе, этот мапинг
можно не делать, и обойтись хуками certbot, но это сложнее.
4. `entrypoint: ...`:
- `apk add --no-cache curl` — устанавливаем пакет `curl`, который потребуется для работы с Docker API через сокет;
- `trap exit TERM;` — устанавливаем обработчик сигнала `TERM`, чтобы контейнер certbot корректно завершал работу;
- `while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done`
— это скрипт, который запускается при старте контейнера certbot. Он запускает certbot в режиме `renew` каждые 12 часов.
Таким образом, сертификаты будут автоматически провереться каждые 12 часов и обновляться, если это необходимо.
Также нам нужно добавить маппинг тома для сертификатов Let's Encrypt в контейнере `nginx`. Теперь описание этого контейнера
в `docker-compose.yml` будет выглядеть так:
```yaml
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt # <- этот маппинг для сертификатов
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt # <- этот маппинг для временных файлов
restart: always
networks:
- web
```
*Тут происходит очень похожий маппинг тома для сертификатов Let's Encrypt и временных файлов для проверки
владения, но теперь в контейнер с nginx*:
- `volumes: ...`:
- `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` — маппинг тома для сертификатов Let's Encrypt, чтобы их
можно было использовать в контейнере nginx;
- `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt` — маппинг тома временных файлов для
проверки владения доменом со стороны Let's Encrypt.
Сохраняем файл `docker-compose.yml`.
Теперь нам нужно настроить nginx, чтобы он мог отдавать временные файлы Certbot. Для этого изменим конфигурационный файл
`/home/web/docker-data/nginx/conf.d/portainer.conf` на следующий:
```nginx configuration
server {
listen 80;
server_name portainer.you.domain.name;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
# Или так, с помощью alias:
# alias /var/www/letsencrypt/.well-known/acme-challenge/;
# try_files $uri =404;
}
location / {
proxy_pass http://portainer:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Как видно, мы добавили новый *location*-блок, который отдает временные файлы Certbot из каталога `/var/www/letsencrypt/`
(а это каталог контейнера `nginx`, который мы ранее замаппили в каталог хоста `/home/web/docker-data/letsencrypt/_ownership_check`).
Он явно указывает, что запросы к `/.well-known/acme-challenge/` не должны идти в прокси, а должны обслуживаться локально.
Используем директиву `location ^~` — она приоритетнее `location /` и такой *location* будет срабатывать даже при
включённом proxy_pass.
**Важно!** После того как сертификаты Let's Encrypt будут получены, не надо удалять этот `location ^~` из конфигурации!
Он нужен для автоматического обновления сертификатов: certbot будет снова создавать временные файлы в каталоге
`/var/www/letsencrypt/`, а сервера Let's Encrypt "дергать" их и тем самым проверять права владения. Если nginx
не сможет отдать эти файлы, то и обновление сертификатов не произойдет.
Теперь, если все контейнеры docker-compose, и `nginx`, и `letsencrypt-certbot`, запущены можно инициализировать
получение сертификатов Let's Encrypt:
```bash
docker exec -it letsencrypt-certbot certbot certonly \
--webroot -w /var/www/html \
-d ortainer.you.domain.name \
--email email@you.domain.name \
--agree-tos --no-eff-email --force-renewal
```
Или можно поднять только nginx (чтобы он отдавал временные файлы для проверки владения доменом):
```bash
cd /home/web/docker-data
docker-compose up -d nginx
```
И запусть контейнер `letsencrypt-certbot` с инициализацией получения сертификатов:
```bash
docker run --rm --name letsencrypt-certbot \
-v /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html \
-v /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt \
certbot/certbot certonly --webroot \
-w /var/www/html \
-d portainer.you.domain.name \
--email email@you.domain.name \
--agree-tos --no-eff-email --force-renewal
```
Если все пройдет успешно (должно пройти) мы увидим примерно такой вывод:
```text
certbot certonly --webroot -w /var/www/html -d portainer.you.domain.name --email email@you.domain.name --agree-tos --no-eff-email --force-renewal
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for portainer.you.domain.name
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem
Key is saved at: /etc/letsencrypt/live/portainer.you.domain.name/privkey.pem
This certificate expires on 2025-05-14.
These files will be updated when the certificate renews.
NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```
Если у вас что-то пойдёт не так, то скорее всего напутано в маппингах томов или в конфигурации nginx. Вы можете
посмотреть логи `certbot` (возможно вам придется замаппить каталог логов из контейнера certbot на хост).
Уже хочется проверить, что все работает? Рано! Нам нужно добавить в конфигурацию nginx SSL-сертификаты и настроить
перенаправление с HTTP на HTTPS. Отредактируем конфиг nginx, теперь он будет выглядеть так:
```nginx configuration
server {
listen 443 ssl;
server_name portainer.you.domain.name;
ssl_certificate /etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/portainer.you.domain.name/privkey.pem;
# Рекомендуемые SSL настройки
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
# Или так, с помощью alias:
# alias /var/www/letsencrypt/.well-known/acme-challenge/;
# try_files $uri =404;
}
location / {
proxy_pass http://portainer:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Перенаправление с HTTP на HTTPS
server {
listen 80;
server_name portainer.you.domain.name;
return 301 https://$host$request_uri;
}
```
Что изменилось в конфигурации:
- Добавлены директивы для SSL для сервера в котором живет прокси на Portainer:
- `listen 443 ssl;` — теперь сервер слушает порт 443 (HTTPS) и использует SSL;
- `ssl_certificate ...` и `ssl_certificate_key ...` — указываем пути к сертификату и ключу сертификата Let's Encrypt;
- `ssl_protocols TLSv1.2 TLSv1.3;` — указываем протоколы SSL/TLS, которые будут использоваться;
- `ssl_ciphers HIGH:!aNULL:!MD5;` — указываем шифры, которые будут использоваться (`HIGH` → Разрешает только сильные
шифры, например, AES256, `!aNULL` → Запрещает анонимные шифры, которые не используют аутентификацию и уязвимы
к MITM-атакам, `!MD5` → Запрещает использование хэша MD5, так как он давно признан небезопасным;
- `ssl_prefer_server_ciphers on;` — указываем, что сервер предпочтет использовать свои шифры, а не клиентские.
- Добавлен блок `server` для перенаправления с HTTP на HTTPS:
- `return 301 https://$host$request_uri;` — перенаправляем все запросы с порта 80 на порт 443 c кодом 301 (перемещено
навсегда).
Сохраним файл и, наконец запустим все контейнеры:
```bash
cd /home/web/docker-data
docker-compose down
docker-compose up -d
```
Теперь можно проверить, что все работает, и зайдите в веб-интерфейс Portainer по адресу `https://portainer.you.domain.name`.
Осталось проверить, что перевыпуск сертификатов будет происходить и мы всё ещё ничего не сломали в мапингах:
```bash
docker exec -it letsencrypt-certbot certbot renew --dry-run -v
```
Должны увидеть примерно такой вывод:
```text
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/portainer.you.domain.name.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator webroot, Installer None
Simulating renewal of an existing certificate for portainer.you.domain.name
Performing the following challenges:
http-01 challenge for portainer.you.domain.name
Using the webroot path /var/www/html for all unmatched domains.
Waiting for verification...
Cleaning up challenges
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```
И наконец, при обновлении сертификатов нужно перезапускать контейнер `nginx` чтобы он перподключил новые сертификаты.
Изнутри контейнера `letsencrypt-certbot` мы не можем управлять контейнерами Docker (и даже ничего не знаем о них),
но можно добавить **хук** в `letsencrypt-certbot`, который будет перезапускать контейнер `nginx` сразу после успешного
обновления сертификатов (и только если обновление прошло успешно)!
Что такое **хук**? Это скрипт, который выполняется в определенный момент жизненного цикла `certbot`. Certbot автоматически
ищет и выполняет скрипты в специальных каталогах, которые и называют **хуками**:
- `/etc/letsencrypt/renewal-hooks/deploy/` скрипты выполняются после успешного обновления;
- `/etc/letsencrypt/renewal-hooks/pre/` выполняются до начала обновления;
- `/etc/letsencrypt/renewal-hooks/post/` выполняются после любой попытки обновления (даже если оно не удалось);
Таким образом наш маппинг `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` обеспечит нам исполнение хуков
внутри контейнера certbot, хотя сами хуки будут лежать на хосте. Добавим скрипт
`/home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh`:
```bash
mkdir -p /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy
sudo nano /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh
```
Обратите внимание, что хук мы создаем через `sudo`. Все содержимое `/home/web/docker-data/letsencrypt/_cert` было создано
изнутри контейнера `certbot`, и поэтому принадлежит пользователю `root` (`root` из контейнера, но на хосте он превратился
в `root` хоста). Поэтому нам нужно использовать `sudo` чтобы редактировать файл.
Вставим в скрипт следующее содержимое:
```bash
#!/bin/sh
echo "СРАБОТАЛ ХУК \"deploy/restart-nginx.sh\": перезапускаем контейнер nginx"
curl -s -o /dev/null --unix-socket /var/run/docker.sock -X POST http:/v1.41/containers/nginx/restart
```
Что тут важно? Несмотря на то, что скрипт лежит на хосте, он все равно будет исполняться внутри контейнера `letsencrypt-certbot`.
Потому просто так перезапустить контейнер `nginx` не получится. Нам нужно использовать Docker API, чтобы сделать это.
Именно поэтому мы в нашем `docker-compose.yml` замаппили сокет Docker в контейнер `letsencrypt-certbot` и устанавливали `curl`.
Сохраним файл и сделаем его исполняемым:
```bash
sudo chmod +x /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh
```
Теперь можно проверить как все это сработает. Посмотрим на наши контейнеры:
```bash
docker ps
```
И увидим наши три контейнера:
```text
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8be62353e563 nginx:latest "/docker-entrypoint.…" 13 minutes ago Up 9 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp nginx
6b805c7df486 portainer/portainer-ce:latest "/portainer" 13 minutes ago Up 13 minutes 8000/tcp, 9000/tcp, 9443/tcp portainer
dd0b7a683dde certbot/certbot:latest "/bin/sh -c 'apk add…" 13 minutes ago Up 13 minutes 80/tcp, 443/tcp letsencrypt-certbot
```
Дадим команду в контейнер `letsencrypt-certbot` на принудительное обновление сертификатов:
```bash
docker exec -it letsencrypt-certbot certbot renew --force-renewal
```
Увидим как certbot обновляет сертификаты:
```text
aving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/portainer.you.domain.name.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for portainer.you.domain.name
Hook 'deploy-hook' ran with output:
СРАБОТАЛ ХУК "deploy/restart-nginx.sh": перезапускаем контейнер nginx
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all renewals succeeded:
/etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```
Теперь можно убедиться, что контейнер nginx перезапустился:
```bash
docker ps
```
И увидим, что STATUS контейнера nginx изменился (uptime сброшен):
```text
8be62353e563 nginx:latest "/docker-entrypoint.…" 2 seconds ago Up 24 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp nginx
6b805c7df486 portainer/portainer-ce:latest "/portainer" 13 minutes ago Up 13 minutes 8000/tcp, 9000/tcp, 9443/tcp portainer
dd0b7a683dde certbot/certbot:latest "/bin/sh -c 'apk add…" 13 minutes ago Up 13 minutes 80/tcp, 443/tcp
```
**Все!** Теперь у нас полностью контейнеризированное решение, без лишних зависимостей на хосте, и при переносе каталога
`~/docker-data` на другой сервер (с Docker + docker-compose) всё должно точно также запуститься.
## Зависимости и "проверки здоровья"
В нашем случае контейнеры не особо зависят друг от друга, но если стремиться к идеальной контейнеризации, то можно
рассмотреть "кто на ком стоит", какие зависимости и какой порядок запуска контейнеров.
Самый независимый контейнер — это `portainer`. Он просто запускается и работает. Начали его проксировать через nginx
или нет — ему важно.
Контейнер `letsencrypt-certbot` зависит от контейнера `nginx`, так как он без него не пройдет валидация домена.
Решение установить `depends_on` от контейнера `nginx` не идеально, т.к. старт контейнера `nginx` не означает, что он
готов принимать запросы. Избежать проблем можно сделав **healthcheck** для Nginx.
В docker-compose.yml добавим:
```yaml
nginx:
...
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
...
...
certbot:
...
depends_on:
nginx:
condition: service_healthy
...
...
```
Что тут происходит:
- `healthcheck: ...` — добавляем блок в контейнере `nginx` для проверки его здоровья. В данном случае проверяем
доступность Nginx выполняя команду `curl -f http://localhost`. Если Nginx отвечает, то контейнер считается здоровым;
- `interval: 10s` — проверка состояния каждые 10 секунд;
- `timeout: 3s` — ожиданий проверки 3 секунды (когда curl зависнет дольше 3 секунд, то это считается ошибкой);
- `retries: 3` — количество попыток проверки (если команда завершается с ошибкой (код ≠ 0) — Docker попробует ещё
раз... и так до 3-х раз);
- `start_period: 10s` — время ожидания перед началом проверок (первая проверка будет выполнена через 10 секунд после
старта контейнера);
- `depends_on: ...` — добавляем зависимость контейнера `certbot` от контейнера `nginx`. Но не просто так, а с условием
`service_healthy`. Это означает, что контейнер `certbot` не будет запущен, пока контейнер `nginx` не будет здоров.
Теперь при запуске контейнера `certbot` он будет ждать, пока контейнер `nginx` не станет здоровым (а еще задержку
в 10 секунд будет заметно даже на глаз).
К сожалению, если `nginx` упадет в процессе, то `certbot` не будет перезапущен. Но это уже другая история,
мир не идеален.
### Погасить проверку здоровья после первого успешного запуска (раздел для параноиков)
Бдительный читатель может заметить, что проверка `curl -f http://localhost` будет порождать бесполезную нагрузку на
сервере (ведь кажется, что по locahost будет отвечать `portainer`, а не `nginx`). Но это не так. Прокси на `portainer`
отвечать там не будет (ведь он настроен на домен `portainer.you.domain.name`), а будет отдавать дефолтную страницу
`nginx`. У нас она даже не настроена, и будет 301, а это вообще очень-очень мало не потребляет...
Тем не менее есть способ погасить проверку здоровья, как только контейнер стал `healthy` (допустим, чтобы "не следить"
в логах). У нас есть мапп `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` внутри контейнера `nginx`. Если мы
создадим скрипт в каталоге `/home/web/docker-data/letsencrypt/_cert` он будет доступен внутри контейнера в каталоге
`/etc/letsencrypt`. Создадим скрипт `healthcheck-nginx.sh`:
```bash
nano /home/web/docker-data/letsencrypt/_cert/healthcheck-nginx.sh
```
И поместим в него следующее содержимое:
```bash
#!/bin/sh
HEALTH_FILE_FLAG="/tmp/nginx_healthy"
if [ -f "HEALTH_FILE_FLAG" ]; then
exit 0 # Уже healthy, больше не проверяем
fi
if curl -fs http://localhost > /dev/null; then
touch "HEALTH_FILE_FLAG" # Создаем файл-флаг
exit 0 # Успешная проверка
else
exit 1 # Nginx еще не поднялся, пусть проверяют еще
fi
```
Что будет происходить. При старте контейнера `nginx` через *healthcheck* будем запускать этот скрипт.
При первом запуске он проверит есть ли файл-флаг `/tmp/nginx_healthy` внутри контейнера. Сразу после старта контейнера
этого фал-флага нет (контейнер запускается "чистым"). Затем скрипт попытается выполнить `curl -fs http://localhost`.
Если Nginx еще не поднялся, то **curl** возвращает ошибку и контейнер остается *unhealthy*. Но если Nginx уже работает,
то **touch** создается файл-флаг `/tmp/nginx_healthy` то **exit 0** ответит, что контейнер *healthy*.⃣ При всех
следующих *healthcheck* проверка файл-флага пройдет успешно, сразу будет получен `exit 0` и проверка *healthy* будет
завершена без вызова `curl`.
Осталось добавить этот скрипт в `docker-compose.yml`:
```yaml
nginx:
...
healthcheck:
test: ["CMD", "sh", "/etc/letsencrypt/healthcheck-nginx.sh"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
...
...
```
### Зависимости и healthcheck для `portainer` (или параноики могут сломать бизнес-логику)
А что с `nginx` и `portainer`? Они зависят друг от друга? Кажется нет. Если `nginx` упадет, то `portainer` просто
станет недоступен (ничего не будет доступно вообще). Если же `portainer` упадет, то `nginx` будет отдавать ошибку 502.
Ошибка, в данном случае, это тоже информация. Как минимум мы увидим что nginx работает, а упало приложение.
Тем не менее можно добавить `healthcheck` в `portainer`, проверять, что он отвечает внутри себя по порту 9000,
а затем установить зависимость `nginx` от здоровья `portainer`.
Но, кажется, это уже перебор.
## Итоговый docker-compose.yml
```yaml
version: '3'
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/web/docker-data/portainer:/data
restart: always
networks:
- web
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt
restart: always
healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost/.well-known/acme-challenge/"]
# test: ["CMD", "sh", "-c", "netstat -tln | grep -q ':80'"]
test: ["CMD", "sh", "/etc/letsencrypt/healthcheck-nginx.sh"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
networks:
- web
certbot:
image: certbot/certbot:latest
container_name: letsencrypt-certbot
volumes:
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
nginx:
condition: service_healthy
networks:
- web
# entrypoint: "/bin/sh -c 'trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew; done'"
entrypoint: "/bin/sh -c 'apk add --no-cache curl && trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done'"
networks:
web:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24 # Подсеть для пользовательской сети
```

View File

@@ -0,0 +1,79 @@
# Установка GPG-ключа для репозиториев Docker
Иногда, при установке Docker, возникает ошибка с ключом GPG. Например, при установке Docker на Ubuntu 20.04. Тогда
при обновлении списка пакета при обновлении командой `sudo apt-get update` выдается сообщение (или подобное):
> W: https://mirrors.aliyun.com/docker-ce/linux/ubuntu/dists/jammy/InRelease: Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in apt-key(8) for details. ??
Это связано с устаревшей практикой хранения ключей репозиториев в общем файле `/etc/apt/trusted.gpg`, а предупреждение
означает, что ключ GPG для репозитория Docker CE хранится в старом формате, который в будущем будет удален из
APT (Advanced Package Tool). Ранее APT использовал общий файл /etc/apt/trusted.gpg для хранения **всех** ключей
репозиториев. Это устарело из-за соображений безопасности. Новая практика заключается в том, чтобы хранить ключи
в отдельных файлах в директории `/etc/apt/trusted.gpg.d/`.
## Как исправить
Найдём ключ для репозитория:
```shell
apt-key list
```
Ключ, связанный с репозиторием Docker может выглядеть, например, так (и радом будет предупреждение):
```text
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
/etc/apt/trusted.gpg
--------------------
pub rsa4096 2017-02-22 [SCEA]
9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
uid [ unknown] Docker Release (CE deb) <docker@docker.com>
sub rsa4096 2017-02-22 [S]
```
Удалим ключ из старой связки ключей (keyring):
```shell
sudo apt-key del 0EBFCD88
```
На всякий случай, проверим, что установлены покеты для работы с HTTPS, curl для загрузки ключей по интернет,
ca-certificates для проверки сертификатов и gpg для работы с ключами. Просто установим их (если они не установлены,
то ничего не произойдет):
```shell
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
```
На Orange Pi 5 Plus у меня не получилось установить GPG-ключ для Docker нормальным образом через команду (у вас, может, и получится):
```shell
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --keyserver keyserver.ubuntu.com --recv-keys 7EA0A9C3F273FCD8
```
И потому я пошел другим путем. Скачал ключ с сайта Docker и установил его вручную:
```shell
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
```
Проверим, что ключ сохранен:
```shell
ls -l /usr/share/keyrings/docker-archive-keyring.gpg
```
Исправим список репозиториев для Docker. Отроем на редактирование _docker.list_ командой:
```shell
sudo nano /etc/apt/sources.list.d/docker.list
```
У меня рабочий вариант на Orange Pi 5 (с процессором ARM64) он выглядел так:
```text
deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu jammy stable
```
А на стареньком Raspberry Pi 3 (с процессором 32-битным ARMv7) он выглядел так:
```text
deb [arch=armhf signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable
```
Обновим список пакетов:
```shell
sudo apt-get update
```
Теперь при обновлении списка пакетов не будет предупреждения о старом ключе GPG!

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 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,343 @@
# Развертывание пользовательского контейнера в k3s
Теперь нам нужно передать образ контейнера на другие ноды кластера. Для этого сначала извлечём образ из k3s в tar-файл:
```bash
sudo docker save shadowsocks-with-tools:latest -o /tmp/shadowsocks.tar
```
Затем передадим на вторую ноду, в нашем случае на хосте `192.168.1.28`, затем на третью (придется копировать через
`sudo`, т.к. при создании образа в tar-файл мы тоже использовали `sudo`):
```bash
sudo scp /tmp/shadowsocks.tar opi@192.168.1.28:/tmp/
```
Импортируем на второй ноде этот образ в k3s. Небезопасное использование пароля удаленного хоста в команде, требуется т.к.
в команде через `ssh` не предоставляется интерактивный терминал, а SSH-ключи решают аутентификацию только для входа, но
не для выполнения `sudo` на удалённой машине... c помощью ключа `-S` мы заставляем `sudo` га уделенном хсте
читать пароль из `stdin`, и потому при введении пароля на текущем хосте он будет виден на экране... это выглядит опасно,
но менее безопасно чем передать его с помощью `echo` в команде `echo 'your_password' |` добавив перед `sido -S k3s ...`.
Так пароль, по крайне мере, не будет сохранён в `bash_history`, что было бы совсем, совсем не безопасно (а экран мы
можем очистить после, выполнив команду `clear`):
```bash
ssh opi@192.168.1.28 "echo 'your_password' | sudo -S k3s ctr images import /tmp/shadowsocks.tar"
```
Увидим что-то вроде:
```text
unpacking docker.io/library/shadowsocks-with-tools:latest (sha256:ae615618ce9d2aac7d3764ef735108452adf3fc30bb65f23f28c345798880c80)...done
```
Удалим временный файл на второй ноде:
```bash
ssh opi@192.168.1.28 "rm /tmp/shadowsocks.tar"
```
И убедимся, что образ появился в k3s на второй ноде (снова используем ключ `-S`):
```bash
ssh opi@192.168.1.28 "sudo -S k3s ctr images ls | grep shadowsocks"
```
**Повторяем операцию для третьей ноды**. И можем удалить временный файл на первой ноде:
```bash
sudo rm /tmp/shadowsocks.tar
```
----
Отлично, что всё получилось! Рад, что мы дошли до этого момента вместе. Твой вопрос про "секретные" контейнеры и необходимость публиковать их в Docker Hub — очень важный и актуальный, особенно для больших кластеров или чувствительных данных. Давай разберём это подробно, чтобы ты понял все варианты и мог выбрать тот, который подходит тебе.
Нужно ли выкладывать "секретный" контейнер в Docker Hub?
Ты прав: если твой контейнер содержит секреты (например, ключи, пароли, защищённые алгоритмы), выкладывать его в публичный реестр вроде Docker Hub — не лучшая идея. Даже если сделать репозиторий приватным на Docker Hub, это всё равно означает передачу твоего образа третьей стороне, что может быть нежелательно с точки зрения безопасности или лицензирования.
Ответ: Нет, тебе не обязательно публиковать контейнер в Docker Hub. Есть несколько способов хранить и распространять образы "у себя", внутри твоей сети, без выхода в интернет. Давай разберём варианты.
Варианты хранения и использования контейнеров
1. Локальное использование (как сейчас)
Как это работает:
Ты уже сделал это: собрал образ shadowsocks-with-tools локально и импортировал его в containerd на каждом узле через k3s ctr images import.
В deployment.yaml указал imagePullPolicy: Never, чтобы k3s использовал только локальный образ.
Плюсы:
Никаких внешних реестров — всё остаётся на твоих машинах.
Полный контроль над секретами.
Минусы:
Не масштабируется: нужно вручную копировать образ на каждый узел.
Если узлов сотни, это становится непрактичным.
Вывод: Подходит для небольшого кластера (как твои три мастера), но не для больших систем.
2. Приватный реестр Docker Hub
Как это работает:
Создаёшь приватный репозиторий на Docker Hub (бесплатно одно приватное репо, дальше платно).
Выкладываешь образ:
bash
Collapse
Wrap
Copy
sudo docker tag shadowsocks-with-tools yourusername/shadowsocks-with-tools:latest
sudo docker push yourusername/shadowsocks-with-tools:latest
В deployment.yaml:
yaml
Collapse
Wrap
Copy
image: yourusername/shadowsocks-with-tools:latest
Настраиваешь доступ через imagePullSecrets в k3s:
yaml
Collapse
Wrap
Copy
apiVersion: v1
kind: Secret
metadata:
name: dockerhub-credentials
namespace: kube-system
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-docker-config>
Где <base64-encoded-docker-config> — это закодированный JSON с твоими логином и паролем от Docker Hub:
bash
Collapse
Wrap
Copy
echo -n '{"auths":{"https://index.docker.io/v1/":{"username":"yourusername","password":"yourpassword"}}}' | base64
Добавь в Deployment:
yaml
Collapse
Wrap
Copy
spec:
imagePullSecrets:
- name: dockerhub-credentials
Плюсы:
k3s сам скачивает образ на всех узлах.
Репозиторий приватный, доступ только по ключам.
Минусы:
Образ всё равно уходит в облако.
Ограничения бесплатного плана (1 приватное репо).
Вывод: Подходит, если доверяешь Docker Hub, но не идеально для "секретов".
3. Локальный приватный реестр в твоей сети
Как это работает:
Разворачиваешь свой собственный реестр на одном из серверов (например, opi5plus-3, 192.168.1.26).
Используешь официальный образ registry:
bash
Collapse
Wrap
Copy
sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2
Тегни и выложи свой образ:
bash
Collapse
Wrap
Copy
sudo docker tag shadowsocks-with-tools:latest 192.168.1.26:5000/shadowsocks-with-tools:latest
sudo docker push 192.168.1.26:5000/shadowsocks-with-tools:latest
В deployment.yaml:
yaml
Collapse
Wrap
Copy
image: 192.168.1.26:5000/shadowsocks-with-tools:latest
Безопасность (опционально):
Добавь TLS и аутентификацию:
Сгенерируй сертификаты:
bash
Collapse
Wrap
Copy
mkdir -p /etc/docker/certs
openssl req -newkey rsa:4096 -nodes -sha256 -keyout /etc/docker/certs/registry.key -x509 -days 365 -out /etc/docker/certs/registry.crt
Запусти реестр с TLS:
bash
Collapse
Wrap
Copy
sudo docker run -d -p 5000:5000 --restart=always --name registry \
-v /etc/docker/certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
registry:2
Настрой аутентификацию через htpasswd:
bash
Collapse
Wrap
Copy
sudo apt install apache2-utils
mkdir -p /etc/docker/auth
htpasswd -Bc /etc/docker/auth/htpasswd username password
docker run -d -p 5000:5000 --restart=always --name registry \
-v /etc/docker/certs:/certs \
-v /etc/docker/auth:/auth \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
-e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
registry:2
Создай Secret для k3s:
yaml
Collapse
Wrap
Copy
apiVersion: v1
kind: Secret
metadata:
name: registry-credentials
namespace: kube-system
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-config>
Где <base64-encoded-config>:
bash
Collapse
Wrap
Copy
echo -n '{"auths":{"192.168.1.26:5000":{"username":"username","password":"password"}}}' | base64
В deployment.yaml:
yaml
Collapse
Wrap
Copy
spec:
imagePullSecrets:
- name: registry-credentials
Плюсы:
Образ остаётся в твоей сети.
Полный контроль над доступом.
k3s сам скачивает образ на узлах.
Минусы:
Нужно настроить реестр и поддерживать его.
Вывод: Идеально для "секретных" контейнеров в большом кластере.
4. Другие приватные реестры (Harbor, Nexus, etc.)
Если кластер большой, можешь развернуть более мощный реестр, например:
Harbor: Open-source, с UI, ролями и интеграцией с Kubernetes.
Nexus Repository: Поддерживает Docker и другие форматы.
Установка сложнее, но для сотен узлов это оправдано.
Ответ на твой вопрос
Можно ли хранить контейнеры у себя? Да, можно! Локальный реестр (Вариант 3) — это то, что тебе нужно. Ты разворачиваешь его на своём сервере (например, 192.168.1.26:5000), загружаешь туда образы, и k3s тянет их оттуда. Никакого Docker Hub, всё под твоим контролем.
Секреты: Они остаются в твоей сети, и доступ ограничивается твоими правилами (TLS, пароли).
Для твоего случая
Сейчас у тебя 3 узла, и локальный импорт работает. Но если кластер вырастет, настрой локальный реестр:
bash
Collapse
Wrap
Copy
sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2
sudo docker tag shadowsocks-with-tools:latest 192.168.1.26:5000/shadowsocks-with-tools:latest
sudo docker push 192.168.1.26:5000/shadowsocks-with-tools:latest
В deployment.yaml:
yaml
Collapse
Wrap
Copy
image: 192.168.1.26:5000/shadowsocks-with-tools:latest
Проверка Shadowsocks
Теперь, когда под работает, давай проверим Shadowsocks:
Зайди в под:
bash
Collapse
Wrap
Copy
sudo k3s kubectl exec -it -n kube-system shadowsocks-client-stockholm-<hash> -- sh
TCP:
bash
Collapse
Wrap
Copy
curl --socks5 127.0.0.1:1081 http://ifconfig.me
Ожидаемый результат: <VPS_IP>.
UDP:
bash
Collapse
Wrap
Copy
nc -u -l 12345
С VPS:
bash
Collapse
Wrap
Copy
echo "test" | nc -u v-panel.sw.cube2.ru 12345
Логи:
bash
Collapse
Wrap
Copy
sudo k3s kubectl logs -n kube-system -l app=shadowsocks-client-stockholm
Итог
"Секретные" контейнеры не нужно выкладывать в Docker Hub — храни их в локальном реестре.
Для проверки Shadowsocks используй команды выше.
Напиши:
<PUBLIC_IP>, <VPS_IP>, имя пользователя.
Результат проверки (curl, nc, логи).
Ты круто всё освоил — теперь ты готов к любым кластерам! Я с тобой!

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

@@ -0,0 +1,532 @@
# Создаём под с Shadowsocks
Для каждого VPN-сервера (локации) нужен отдельный клиентский под, который создаст SOCKS5-прокси внутри кластера.
Другие поды будут подключаться к тому или иному SOCKS5-прокси в зависимости от их назначения.
Все конфиги и манифесты K3S хранит в `etcd` и распространится по всем нодам кластера. Но создавать и вносить изменения
непосредственно в `etcd` не удобно. Намного удобнее k3s-конфиги передавать через ConfigMap. К тому же это позволяет
иметь копии конфигов на каком-нибудь хосте и делать резервные копии и восстанавливать их в случае необходимости.
Не принципиально, где хранить конфиги и манифесты, так как после с помощью `kubectl` они будут загружены в k3s. Но
лучше хранить их в одном месте, чтобы не искать по всему кластеру, где же они хранятся.
Предлагаемая структура каталогов для хранения конфигураций и манифестов Kubernetes:
```text
~/k3s/
├── vpn/ # Все VPN-клиенты
│ ├── client-shadowsocks--moscow/ # Локация Москва
│ │ ├── config.yaml # ConfigMap для Shadowsocks
│ │ └── deployment.yaml # Deployment для Shadowsocks
│ ├── client-shadowsocks--stockholm/ # Локация Стокгольм
│ │ ├── config.yaml
│ │ └── deployment.yaml
│ └── cclient-shadowsocks--izmir/ # Локация Измир
│ ├── config.yaml
│ └── deployment.yaml
├── …
└── …
```
Создаем файл `config.yaml` для первого Shadowsocks-клиента (Москва):
```bash
nano ~/k3s/vpn/client-shadowsocks--moscow/config.yaml
```
И вставляем в него следующее:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: shadowsocks-client-moscow
namespace: kube-system # Ставим в kube-system, чтобы было системно
data:
config.json: |
{
"server": "<IP_ИЛИ_ИМЯ_СЕРВЕРА>",
"server_port": <ПОРТ>,
"local_address": "127.0.0.1",
"local_port": 1081,
"password": "<PASSWORD_FOR_SHADOWSOCKS_CLIENT>",
"method": "chacha20-ietf-poly1305",
"mode": "tcp_and_udp"
}
```
Что тут происходит:
- `apiVersion: v1` — версия API Kubernetes.
- `kind: ConfigMap` — это способ хранить конфиги внутри k3s.
- `metadata:` — метаданные о конфиге.
- `name:` — имя конфигурации.
- `namespace:` — пространство имен, в котором будет храниться конфигурация. Мы используем `kube-system`, чтобы сделать его системным.
- `data:` — данные конфигурации.
- `config.json:` — имя файла, в который будет записан конфиг.
- `|` — говорит, что дальше будет многострочный текст.
- `{…}` — Собственно JSON-конфигурация нашего Shadowsocks-клиента.
- `server` и `server_port` — адрес и порт нашего VPS.
- `local_address` и `local_port` — где будет SOCKS5 внутри кластера.
- `password` и `method` — пароль и метод шифрования. Метод шифрования `chacha20-ietf-poly1305` -- используется,
например, VPN-сервисом Outline. Получить пароль для Outline можно с помощью base64 декодирования ключа.
Структура строки подключения `ss://<ПАРОЛЬ_КОДИРОВАННЫЙ_В_BASE64>@<IP_ИЛИ_ИМЯ_СЕРВЕРА>:<ПОРТ>?type=tcp#<ИМЯ-КЛИЕНТА>`
- `mode: tcp_and_udp` — включает поддержку TCP и UDP.
Применим ConfigMap:
```bash
sudo k3s kubectl apply -f /home/<ПОЛЬЗОВАТЕЛЬ>/k3s/vpn/client-shadowsocks--moscow/config.yaml
```
Важно указывать полный путь к файлу, а не от домашнего каталога `~\`. Запуская `kubectl` из под `sudo` (или от имени
`root`), мы исполняем команды `k3s` от имени другого пользователя, а не от имени текущего.
Когда выполним команду, то увидим что-то вроде:
```text
configmap/shadowsocks-config-moscow created
```
Теперь создадим `Deployment` для Shadowsocks-клиента. Создаём файл `deployment.yaml`:
```bash
nano ~/k3s/vpn/client-shadowsocks--moscow/deployment.yaml
```
И вставляем в него следующее:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: shadowsocks-client-moscow # Уникальное имя (должно совпадать с именем в config.yaml для ConfigMap)
namespace: kube-system # В системном пространстве
spec:
replicas: 1
selector:
matchLabels:
app: shadowsocks-client-moscow
template:
metadata:
labels:
app: shadowsocks-client-moscow
spec:
containers:
- name: shadowsocks-client
image: shadowsocks/shadowsocks-libev:latest # Официальный образ
command: ["ss-local"] # Запускаем клиент
args:
- "-c" # Указываем конфиг
- "/etc/shadowsocks/config.json" # Путь внутри контейнер
volumeMounts:
- name: config-volume
mountPath: /etc/shadowsocks # Монтируем ConfigMap
ports:
- containerPort: 1081 # Открываем порт SOCKS5 (TCP)
protocol: TCP
- containerPort: 1081 # Открываем порт SOCKS5 (UDP)
protocol: UDP
securityContext:
privileged: true # Нужно для работы с сетью
volumes:
- name: config-volume
configMap:
name: shadowsocks-client-moscow # Связываем с ConfigMap
```
Объяснение:
* `Pod` — это простейший объект в k3s, запускающий один контейнер.
* `image` — официальный образ Shadowsocks.
* `command` и `args` — запускают `ss-local` с конфигом из `ConfigMap`.
* `volumeMounts` — подключают `config.json` из `ConfigMap` в контейнер.
* `ports` — открываем 1080/TCP и 1080/UDP для SOCKS5.
* `privileged: true` — даёт права для работы с сетью (в k3s это иногда нужно).
Применим под:
```bash
sudo k3s kubectl apply -f /home/opi/k3s/vpn/client-shadowsocks--moscow/deployment.yaml
```
### Проверка
Проверяем, что под запустился, посмотрев статус:
```
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-moscow-54d64bf5f4-trb6p
```
Увидим, что клиент shadowsocks запустился:
```text
2025-03-09 09:48:24 INFO: initializing ciphers... chacha20-ietf-poly1305
2025-03-09 09:48:24 INFO: listening at 127.0.0.1:1081
2025-03-09 09:48:24 INFO: udprelay enabled
```
Запустился, но не подключился. Подключение произойдет при отправке первых пакетов через соединение. Для этого нужно
зайти в под и запросить что-нибудь через `curl`. Но на поде нет `curl`, поэтому что по умолчанию образ контейнера
shadowsocks-клиента минималистичен и в нём нет ничего лишнего. Нам придется собрать свой образ с `curl`. Создадим
файл `Dockerfile` для сборки образа (да, сам Kubernetes не умеет собирать образы, для этого нужен Docker):
```bash
nano k3s/vpn/client-shadowsocks--moscow/Dockerfile
```
И вставим в него следующее:
```dockerfile
FROM shadowsocks/shadowsocks-libev:latest
USER root
RUN apk update && apk add curl netcat-openbsd
```
Что тут происходит:
* `FROM` — базовый образ, от которого мы будем отталкиваться.
* `USER root` — переключаемся на пользователя root, чтобы иметь возможность устанавливать пакеты.
* `RUN` — выполнить команду в контейнере. В данном случае обновляем пакеты (`apk update`) и устанавливаем `curl` и
`netcat-openbsd` (`apk add curl netcat-openbsd`).
Cоберём образ:
```bash
sudo docker build -t shadowsocks-with-tools:latest ~/k3s/vpn/client-shadowsocks--moscow/
```
Увидим, что образ собрался:
```text
[+] Building 1.4s (6/6) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 135B 0.0s
=> [internal] load metadata for docker.io/shadowsocks/shadowsocks-libev:latest 1.4s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/2] FROM docker.io/shadowsocks/shadowsocks-libev:latest@sha256:124d1bff89bf9e6be19d3843fdcd40c5f26524a7931c8accc5560a88d0a42374 0.0s
=> CACHED [2/2] RUN apk update && apk add curl netcat-openbsd 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:5708432467bcac4a0015cd97dbca968e9b69af06da192018169fff18673ed13f 0.0s
=> => naming to docker.io/library/shadowsocks-with-tools:latest
```
Перенесем полученный образ в k3s с помощью `ctr` (containerd CLI):
```bash
sudo docker save shadowsocks-with-tools:latest | sudo k3s ctr images import -
```
Здесь:
* `docker save` — экспортирует образ в tar-формат.
* `k3s ctr` — вызывает ctr внутри k3s.
* `images import -` — импортирует образ из stdin.
Увидим что-то вроде:
```text
unpacking docker.io/library/shadowsocks-with-tools:latest (sha256:ae615618ce9d2aac7d3764ef735108452adf3fc30bb65f23f28c345798880c80)...done
```
Проверим, что образ появился в k3s:
```bash
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
```
Теперь нам нужно передать образ контейнера на другие ноды кластера. Как это сделать есть заметка "[Развертывание
пользовательского контейнера в k3s](k3s-custom-container-deployment.md)"
Когда наш контейнер окажется на всех нодах, изменим `deployment.yaml` Shadowsocks-клиента, чтобы использовать наш
новый образ. Закомментируем строку `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-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--v/deployment.yaml
```
Проверим, что под запустился, посмотрев статус:
```bash
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
Посмотрим логи пода с Shadowsocks-клиентом:
```bash
sudo k3s kubectl logs -n kube-system -l app=shadowsocks-client-moscow
```
Увидим, что клиент shadowsocks запустился:
```text
2025-03-14 21:01:59 INFO: initializing ciphers... chacha20-ietf-poly1305
2025-03-14 21:01:59 INFO: listening at 127.0.0.1:1081
2025-03-14 21:01:59 INFO: udprelay enabled
2025-03-14 21:01:59 INFO: running from root user
```
Проверим TCP-соединение. Зайдём в под:
```bash
sudo k3s kubectl exec -it -n kube-system shadowsocks-client-moscow-<hash> -- sh
```
И выполним внутри пода команду:
```bash
curl --socks5 127.0.0.1:1081 http://ifconfig.me
curl -k --socks5 127.0.0.1:1081 https://ifconfig.me
```
`ifconfig.me` -- это публичный сервис, который показывает IP-адрес, с которого к нему пришёл запрос. В первом случае
проверяем http-соединение, а во втором — https. Ожидаемый результат: `<VPS_IP>` (IP-адрес нашего VPS).
Выходим из пода:
```bash
exit
```
Проверим логи еще раз:
```bash
sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-<hash>
```
Увидим, что клиент shadowsocks отработал:
```text
2025-03-14 21:01:59 INFO: running from root user
2025-03-14 21:03:01 INFO: connection from 127.0.0.1:55226
2025-03-14 21:03:01 INFO: connect to 34.160.111.145:80
2025-03-14 21:03:01 INFO: remote: <VPS_IP>:56553
2025-03-14 21:03:10 INFO: connection from 127.0.0.1:33382
2025-03-14 21:03:10 INFO: connect to 34.160.111.145:443
2025-03-14 21:03:10 INFO: remote: <VPS_IP>:56553
```
## Изменение конфигурации для доступа с других подов (внутри кластера)
Кстати, если нам понадобится внести изменения в конфиг, то можно просто отредактировать файл и применить его снова.
Старые данные автоматически заменятся на новые. "Умная" команда `kubectl apply` сравнивает текущий объект в k3s
(в `etcd`) с тем, что указан в файле. Если объект уже существует (по `metadata.name` и `namespace`), он обновляется.
Если объекта нет, он создаётся.
Сейчас `SOCKS5` shadowsocks-контейнера доступен только внутри пода и для других контейнеров в том же поде (если
такие контейнеры появятся). Нр для моего проекта нужно чтобы shadowsocks-прокси были доступны из других подов
(поды-парсеры поисковика и сборщики данных). Для этого shadowsocks-контейнер должен слушать на `0.0.0.0` (внешний IP).
Для этого нужно изменить _local_address_ в конфиге shadowsocks-клиента `config.yaml`:
```yaml
"server_port": <ПОРТ>,
# "local_address": "127.0.0.1",
"local_address": "0.0.0.0",
"local_port": 1081,
```
Применим конфиг:
```bash
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/config.yaml
```
И обновим под. Обратите внимание, что сам собой под не обновится. Он в памяти, исполняется и никак
не может узнать, что конфиг изменился. Поэтому удалиv старый под и Deployment автоматически создаст его заново, но уже
с новым конфигом:
```bash
sudo k3s kubectl delete pod -n kube-system -l app=shadowsocks-client-moscow --force --grace-period=0
```
Здесь `-l` — это селектор, который выбирает все поды с меткой `app=shadowsocks-client-moscow`.
`--force` и `--grace-period=0` — принудительно удалить под без ожидания завершения работы.
## Создание сервиса для доступа к поду
Так как в Kubernetes (и k3s) поды — это временные сущности (они создаются, умирают, перезапускаются, переезжают на
другие ноды и тому подобное) их IP-адреса и полные имена (из-за изменения суффиксов) постоянно меняются. Для решения
этой проблемы в k3s есть абстракция **Service**. Она позволяет обращаться к подам по имени, а не по IP-адресу. _Service_
предоставляет стабильный IP-адрес (и имя) для доступа к подам, независимо от их текущих IP. Кроме того он обеспечивает
Балансировку. Например, если у нас несколько подов с Shadowsocks (_replicas: 3_), _Service_ распределит запросы между
ними. Так же, благодаря внутреннему DNS _Service_ позволяет обращаться к поду/подам по имени.
Создадим манифест `service.yaml`:
```bash
nano ~/k3s/vpn/client-shadowsocks--moscow/service.yaml
```
И вставим в него следующее:
```yaml
apiVersion: v1
kind: Service
metadata:
name: ss-moscow-service
namespace: kube-system
spec:
selector:
app: shadowsocks-client-moscow
ports:
- name: tcp-1081 # Уникальное имя для TCP-порта
protocol: TCP
port: 1081
targetPort: 1081
- name: udp-1081 # Уникальное имя для UDP-порта
protocol: UDP
port: 1081
targetPort: 1081
type: ClusterIP
```
Что тут происходит:
* `apiVersion: v1` — версия API Kubernetes.
* `kind: Service` — это тип способ создать сервис внутри k3s.
* `metadata:` — метаданные о сервисе.
* `name:` — имя сервиса.
* `namespace:` — пространство имен, в котором будет храниться сервис. Мы используем `kube-system`, чтобы сделать его
системным.
* `spec:` — спецификация сервиса.
* `selector:` — селектор, который определяет, какие поды будут обслуживаться этим сервисом.
* `app: shadowsocks-client-moscow` — поды с меткой `app=shadowsocks-client-moscow` (из нашего `deployment.yaml`)
выше будут обслуживаться этим сервисом. Service автоматически находит все поды с такой меткой даже если их IP
или хэш меняются.
* `ports:` — порты, которые будут открыты для доступа к подам.
* `name:` — уникальное имя для порта. Kubernetes требует `name` для портов в Service, если их больше одного, чтобы
избежать путаницы при маршрутизации или логировании.
* `protocol:` — протокол (TCP или UDP).
* `port:` — порт, на котором будет доступен сервис.
* `targetPort:` — порт, на который будет перенаправлен трафик внутри подов.
* `type:` — тип сервиса. `ClusterIP` — это внутренний сервис, доступный только внутри кластера. Если нужно
сделать его доступным извне, то можно использовать `NodePort` или `LoadBalancer`. В нашем случае
`ClusterIP` достаточно.
Применим сервис:
```bash
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml
```
Проверим, что сервис создался:
```bash
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-прокси.
### Проверим как работает доступ к прокси из другого пода
Создай тестовый под: (`test-pod`):
```bash
sudo k3s kubectl run -n kube-system test-pod --image=alpine --restart=Never -- sh -c "sleep 3600"
```
Заходим в него:
```bash
sudo k3s kubectl exec -it -n kube-system test-pod -- sh
```
Устанавливаем `curl` внутри пода:
```bash
apk add curl
```
Проверяем доступ из `test-pod` к прокси на `ss-moscow-service` (не важно, полное имя или короткое):
```bash
curl --socks5 ss-moscow-service:1081 http://ifconfig.me
curl --socks5 ss-moscow-service.kube-system.svc.cluster.local:1081 http://ifconfig.me
exit
```
Увидим, что запросы прошли и мы получили IP-адрес нашего VPS.
## Изменение конфигурации для доступа с хостов домашней сети (внешний доступ, не обязательно)
Чтобы прокси был доступен из домашней сети, нужно "вывесить" SOCKS5-прокси изнутри пода наружу. Для этого в Kubernetes
тоже можно использовать _Service_. Если использовать тип `NodePort`. NodePort — это тип сервиса в Kubernetes (и k3s),
который делает порты пода доступными на всех узлах кластера (nodes) на определённом порту хоста. k3s использует
_iptables_ (или _ipvs_) на каждом узле, чтобы перенаправлять трафик с NodePort (с порта IP узла) на внутренний IP пода
(в нашем случае -- 10.42.x.x:1081) через CLUSTER-IP. Даже если под "живёт" только на одном узле, трафик с других узлов
маршрутизируется к нему по внутренней сети k3s.
Откроем `service.yaml` и изменим его:
```yaml
apiVersion: v1
kind: Service
metadata:
name: ss-moscow-service
namespace: kube-system
spec:
selector:
app: shadowsocks-client-moscow
ports:
- name: tcp-1081 # Уникальное имя для TCP-порта
protocol: TCP
port: 1081
targetPort: 1081
nodePort: 31081 # Порт на хосте (TCP, будет доступен на всех нодах кластера)
- name: udp-1081 # Уникальное имя для UDP-порта
protocol: UDP
port: 1081
targetPort: 1081
nodePort: 31081 # Порт на хосте (UDP, будет доступен на всех нодах кластера)
# type: ClusterIP
type: NodePort
```
Применим сервис:
```bash
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml
```
Можно, что теперь сервис доступен на любой ноде кластера по порту `31081` (TCP и UDP). Для этого с любого хоста
домашней сети можно выполнить:
```bash
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)

File diff suppressed because it is too large Load Diff

67
misc/set-locale-ru.md Normal file
View File

@@ -0,0 +1,67 @@
# Установка RU-локали в Ubuntu
Чтобы добавить русскую локаль `ru_RU.UTF-8` в Ubuntu, выполните следующие шаги:
Проверим, доступна ли локаль ru_RU.UTF-8 в системе:
```shell
locale -a | grep ru_RU
```
Если локаль ru_RU.UTF-8 уже есть в списке, значит, она уже установлена. Если её нет, cгенерируем локаль `ru_RU.UTF-8`
и добавим её в систему:
```shell
sudo locale-gen ru_RU.UTF-8
```
Обновим настройку локалей:
```shell
sudo update-locale
```
## Установим локаль как системную по умолчанию. :
Чтобы локаль `ru_RU.UTF-8` была установлена по умолчанию для всей системы, отредактируем файл `/etc/default/locale`:
```shell
sudo nano /etc/default/locale
```
Добавим (или изменим) строки на следующие:
```text
LANG=ru_RU.UTF-8
LC_ALL=ru_RU.UTF-8
```
Сохраним файл и выйдем из редактора (`Ctrl + X`, затем `Y` для подтверждения).
Чтобы изменения вступили в силу, перезагрузим систему или выполним:
```shell
source /etc/default/locale
```
Проверим текущую локаль:
```shell
locale
```
Увидим что-то типа:
```text
LANG=ru_RU.UTF-8
LANGUAGE=
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC="ru_RU.UTF-8"
LC_TIME="ru_RU.UTF-8"
LC_COLLATE="ru_RU.UTF-8"
LC_MONETARY="ru_RU.UTF-8"
LC_MESSAGES="ru_RU.UTF-8"
LC_PAPER="ru_RU.UTF-8"
LC_NAME="ru_RU.UTF-8"
LC_ADDRESS="ru_RU.UTF-8"
LC_TELEPHONE="ru_RU.UTF-8"
LC_MEASUREMENT="ru_RU.UTF-8"
LC_IDENTIFICATION="ru_RU.UTF-8"
LC_ALL=ru_RU.UTF-8
```
Локаль `ru_RU.UTF-8` установлена корректно.

View File

@@ -0,0 +1,85 @@
# Настройка nginx как прямого прокси
Собственно, прямой прокси — это прокси, который просто перенаправляет запросы на другой сервер. Очень полезно,
когда у вас внутри сети есть один компьютер, который виден из интернет (DMZ или через проброс портов), и мы хотим
перенаправить внешние запросы на другие сервера внутри сети. Заодно можно настроить SSL-терминацию.
На примере AudioBookShelf, который должен быть доступен снаружи по адресу `some.you.site` у нас будет вот такой конфиг:
```nginx configuration
# config for AudioBookShelf [some.you.site]
server {
server_name [some.you.site]; # доменное имя сайта
charset utf-8; # кодировка по умолчанию
access_log /home/orangepi/web-data/audiobookshelf/logs/audiobookshelf-access.log; # логи с доступом
error_log /home/orangepi/web-data/audiobookshelf/logs/audiobookshelf-error.log; # логи с ошибками
client_max_body_size 512M; # максимальный объем файла для загрузки на сайт (max upload size)
# listen 80; # managed by Certbot
listen 443 ssl http2; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/[some.you.site]/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/[some.you.site]/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location /favicon.ico { root /home/orangepi/web-data/audiobookshelf/html; } # Расположение favicon.ico
location /favicon.png { root /home/orangepi/web-data/audiobookshelf/html; } # Расположение favicon.png
location /robots.txt { root /home/orangepi/web-data/audiobookshelf/html; } # robots.txt (dissalow all)
location / {
proxy_pass http://[какой-то-ip]:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Nginx-Proxy true;
# proxy_redirect off;
proxy_set_header X-Scheme $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
# location / {
# index index.html;
# }
}
server {
if ($host = [some.you.site]) { return 301 https://$host$request_uri; } # managed by Certbot
server_name [some.you.site];
listen 80;
return 404; # managed by Certbot
}
```
## Проксирование на host если nginx находится внутри Docker
Если [nginx находится внутри Docker или Docker Compose](../docker/docker-nginx-w-certbot.md), то он сможет увидеть
только свои контейнерные IP-адреса и хосты. Если таким nginx нужно проксировать на сам хост, то в конфиге nginx
нужно указать:
```nginx configuration
proxy_pass http://host.docker.internal:xxxx;
```
* `host.docker.internal` -- это специальный DNS-имя, которое указывает на хост, на котором запущен Docker.
Начиная с Docker 20.10.0+ сам контейнер надо запускать с дополнительным параметром `--add-host=host.docker.internal:host-gateway`.
```shell
docker run --add-host=host.docker.internal:host-gateway ...
```
Или добавить дополнительную инструкцию `extra_hosts: "host.docker.internal:host-gateway"` в `docker-compose.yml` при
использовании Docker Compose:
```yaml
services:
nginx:
image: nginx:latest
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "80:80"
...
...
```

View File

@@ -0,0 +1,44 @@
# Python-скрипт как служба Linux
| ВАЖНО |
|:------|
| Python-скрипт работающий как служба Linux не должен быть блокирующим. В противном случае, он может привести к зависанию системы. Чтобы избежать блокировок в нем долны быть пердусмотрены механизмы асинхронного выполнения. Самое простое решение -- использовангие `sleep()` в вечных циклах. |
Чтобы превратить Python-скрипт в службу Linux, можно использовать systemd (систему инициализации, используемую в большинстве дистрибутивов Linux). Нам потребуется создать файл службы systemd, который будет запускать ваш Python-скрипт:
1. Создадим файл службы systemd. Этот файл обычно находится в каталоге `/etc/systemd/system/`. Назовем его, например, `my_python_service.service`.
2. В этом файле укажем следующее:
```ini
[Unit]
Description = My_Python_Service
After = network.target
[Service]
ExecStart = /usr/bin/python3 /path/to/your/python/script.py
Restart = always
User = username
Group = groupname
Environment = NAME=value
[Install]
WantedBy=multi-user.target
```
Где:
- `Description` -- описание службы.
- `After` -- указание systemd запускать службу после того, как сеть будет доступна.
- `ExecStart` -- команда, которую systemd выполнит для запуска вашей службы. Соответственно в ней указание какой Python запускать (и где он находится) и полный путь к нашему Python-скрипту.
- `Restart` -- указание systemd перезапустить службу, если она упадет.
- `User` и `Group` -- имя пользователя и группы, от имени которых будет запущена служба.
- `Environment` -- переменные окружения, которые будут установлены при запуске службы.
- `WantedBy` -- это указание systemd, когда должна быть запущена служба.
3. После создания файла службы systemd, надо обновить systemd, чтобы он знал о новой службе. Можно сделать это с помощью команды `systemctl daemon-reload`.
4. Теперь можно запустить службу с помощью команды `systemctl start my_python_service`.
5. Если необходимо, чтобы ваша служба автоматически запускалась при загрузке системы, то включаем её в автозапуск с помощью команды `systemctl enable my_python_service`.

View File

@@ -170,7 +170,7 @@ sudo chown -R 777 /media/backup/
sudo chown -R [user]:[user] /media/backup/ sudo chown -R [user]:[user] /media/backup/
``` ```
### Скрипт резервного копирования в SAMBA-папку внутри домашней сети ### Скрипт резервного копирования в SAMBA-папку внутри домашней сети (на NAS)
Скрипт резервного копирования (*не забудьте заменить `[ip]`, `[user]` и `[login]` -- ip-адрес NAS Скрипт резервного копирования (*не забудьте заменить `[ip]`, `[user]` и `[login]` -- ip-адрес NAS
в домашней сети, NAS-логин и NAS-пароль*) сохранит zip-архивы образа flash-накопителя и домашний в домашней сети, NAS-логин и NAS-пароль*) сохранит zip-архивы образа flash-накопителя и домашний
@@ -269,4 +269,64 @@ sudo crontab -e
``` ```
Скрипт будет запускаться каждый понедельник в 00:05. Таким образом в каждый момент времени Скрипт будет запускаться каждый понедельник в 00:05. Таким образом в каждый момент времени
в облаке будет храниться две последних резервных копий (за две предыдущих недели). в облаке будет храниться две последних резервных копий (за две предыдущих недели).
-----
## Восстановление из резервной копии
### Восстановление загрузочного образа flash-накопителя
Для восстановления загрузочного образа flash-накопителя на Orange Pi, нужно извлечь его из
устройства (raspberry pi или orange pi) и, предварительно распокавав нужный `img` из zip-архива
просто перезаписать заново флешку. Под windows надо использовать [rufus](https://rufus.ie/ru/)
(но это работает хреново, иногда вообще все USB-порты компьютера перестают работать до перезагрузки)
а под MacOS -- [balenaEtcher](https://www.balena.io/etcher/) (все работает идеально).
Так же под MacOS и Linux можно восстановить образ с помощью команды `dd`. Сначала узнаем имя устройства
в которое вставлена флешка:
```shell
diskutil list
```
Увидим список всех дисков, в том числе и флешек. Что-то типа:
```txt
...
...
/dev/disk5 (external, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *128.2 GB disk5
1: BC13C2FF-59E6-4262-A352-B275FD6F7172 268.4 MB disk5s1
2: Linux Filesystem 126.6 GB disk5s2
(free space) 1.3 GB
...
...
```
Таким образом, флешка вставлена в `/dev/disk5`. Теперь можно записать образ на флешку:
```shell
sudo dd if=/path/to/flash-disk.img of=/dev/disk5 bs=1m
```
### Восстановление домашней папки из SMB
Восстановление из резервной копии -- не регулярное действие, поэтому не нужно автоматизировать
процесс. Для восстановления из резервной копии на Orange Pi, нужно смонтировать сетевую папку:\
```shell
mount -t cifs -o username=[samba-login],password=[smaba-pwd] //192.168.1.50/NetBackup /media/backup/
```
И восстановить файлы из архива:
```shell
sudo unzip -o /media/backup/orange-pi-backup/home-volum--YYYY-MM-DD.zip -d /
```
Проверим права доступа после восстановления и убедимся, что права доступа к файлам восстановлены корректно:
```shell
ls -l /home/имя_пользователя
```
Если права сбились, установим правильного владельца и группу для вашего пользователя:
```shell
sudo chown -R orangepi:orangepi /home/orangepi
```

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

@@ -0,0 +1,840 @@
# Развертывание k3s на Orange Pi
K3s — это облегчённая версия Kubernetes, созданная для слабых или малых серверов (Raspberry Pi, Orange Pi,
IoT-устройства, edge-серверы и т.п.). Для кластера из нескольких Orange Pi он предпочтительнее, так как:
* K3S менее требователен к ресурсам (Полный k8s на ARM может сожрать 1-2 ГБ только на управление кластером,
а k3s занимает ~500 МБ.
* K3s проще устанавливать и обновлять. Shell-скрипт с [https://get.k3s.io](get.k3s.io) все сделает сам, и не нужно
погружаться сложные настройки kubeadm. Обычный Kubernetes состоит из множества компонентов: kube-apiserver,
kube-controller-manager, kube-scheduler, kubelet на каждой ноде, kube-proxy, etcd и т.д. В K3s всё это
упаковано в один бинарник.
* Всё работает "из коробки" благодаря встроенному Flannel (CNI) и не надо вручную настраивать Calico, Weave, Cilium.
* В отличие от "классического" Kubernetes (например, kubeadm), где мастер-узлы по умолчанию изолированы от рабочих нагрузок с помощью taint'ов (например, NoSchedule), k3s не добавляет такие ограничения автоматически. Это значит:
* Для моего проекта особо важно, что из коробки мастер-узел(ы)) в k3s является "гибридным" и выполняет одновременно
функции управления (control-plane) и может запускать обычные поды, как воркер. Компоненты управления (API-сервер,
контроллеры, etcd) работают как системные сервисы, а для пользовательских подов используется тот же kubelet,
что и на воркерах. _**Кстати, что такое "поды".** Контейнеры в Kubernates называют "поды", чтобы отличать их от
Docker-контейнеров и подчёркивать, что это абстракция уровня оркестрации. Но под — это не просто контейнер, это
сущность Kubernetes, которая может включать несколько контейнеров, сетевые настройки и тома. Но под капотом
контейнеры всё равно запускаются runtimeом (это containerd в k3s). И Docker все равно еще нужен для создания
образов, и если при установке k3s не указать `--docker` то k3s будет использовать его как runtime._
Но, есть у k3s и минус для конкретно моего случая — распределенная база **etcd**, в которой хранится состояния
кластера, нод и подов, в нем заменена SQLite. Это круто для маленьких компьютеров: экономно по памяти и другим ресурсам,
и, что главное, никак не сказывается на производительности (пока узлов меньше 50-80), но означает, что в кластере k3s
может быть только одна мастер-нода. Если мастер-нода упадет, её некому будет заменить и весь кластер умрет.
Мне же надо, чтобы как миниум две (а лучше все) ноды могли быть мастерами, так что я буду делать k3s-кластер
с использованием *etcd*.
### Важное предупреждение
k3s — это не упрощенная мини-версия Kubernetes, здесь все компоненты упакованы в один бинарник, а значит намного
проще не только добавлять узлы, но и удалять их. Так что если что-то пойдет не так с настройкой узла, просто удалите
и начните заново. Удаление k3s с узла:
```bash
sudo /usr/local/bin/k3s-uninstall.sh # На мастерах
sudo /usr/local/bin/k3s-agent-uninstall.sh # На воркере
```
## Установка k3s на первом узле (мастер)
Некоторые требования к узлам:
* На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04).
* Статические IP-адреса узлов (или зарезервированные под MAC-адреса IP в DHCP).
* На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet).
Установливаем первый мастер:
```bash
curl -sfL https://get.k3s.io | sh -s - server --cluster-init --tls-san=192.168.1.27
```
Здесь:
* `server` — значение по умолчанию, устанавливает узел k3s в режиме *мастер* (control-plane). В этом режиме узел
будет запускать все компоненты управления Kubernetes: API-сервер, контроллер-менеджер, планировщик (scheduler).
Такой узел отвечает за управление кластером и может также выполнять рабочие нагрузки (workloads), если
не настроены ограничения (taints). Если бы мы указали `agent` — был бы установлен узел k3s в режиме *воркер*-узла.
* `--cluster-init` — добавляет поддержку высокой доступности (HA — High Availability) через встроенный `etcd`. Это
значит, что узел инициализирует новый кластер и готов к тому, чтобы другие мастер-узлы могли к нему подключиться
(для создания HA-конфигурации).
* `--tls-san=192.168.1.27` — добавляет IP 192.168.1.27 в сертификаты API-сервера, чтобы другие узлы и клиенты
могли обращаться к нему по этому адресу.
Проверим, что все k3s запущен:
```bash
sudo service k3s status
```
Увидим что-то типа:
```text
● k3s.service - Lightweight Kubernetes
Loaded: loaded (/etc/systemd/system/k3s.service; enabled; vendor preset: enabled)
Active: active (running) since …
```
## Перемещение конфигурации kubectl
При выполнении команд `kubectl` используется файл конфигурации из переменной `KUBECONFIG`. При установке k3s создает
его в `/etc/rancher/k3s/k3s.yaml` и у него права `600` (только для root). Чтобы использовать `kubectl` от имени
пользователя хоста, нужно скопировать файл конфигурации в домашнюю директорию и изменить права доступа.
```bash
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
```
И, та-да! Увидим одну ноду:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-2 Ready control-plane,etcd,master 31m v1.31.5+k3s1
```
Как видим, узел `opi5plus-2` готов к работе и выполняет роли *control-plane*, *etcd* и *master*.
А что там внутри? Посмотрим на поды:
```bash
kubectl get pod -A
```
Целых семь подов (минималистичная установка k3s):
```text
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-ccb96694c-tfjwj 1/1 Running 0 13m
kube-system helm-install-traefik-crd-bdbgd 0/1 Completed 0 13m
kube-system helm-install-traefik-mlztm 0/1 Completed 1 13m
kube-system local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 13m
kube-system metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 13m
kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0 12m
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 12m
```
* 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)
для *Traefik* — ingress-контроллера, встроенного в k3s. CRD нужны для управления ingress-ресурсами
(маршрутизацией HTTP/HTTPS). Этот под — одноразовая задача (Job), а не постоянный сервис. Он запустился, выполнил
работу (установил CRD) и завершился. Статус "*Completed*" значит, что он больше не работает.
3. `helm-install-traefik` — ещё один Job, который устанавливает сам Traefik через Helm-чарт. Этот под развернул
основной Traefik-под и завершился.
4. `local-path-provisioner` — компонент для автоматического создания локальных Persistent Volumes (PV) на узлах. Он
позволяет подам запрашивать хранилище (например, через PersistentVolumeClaim) без сложной настройки NFS или внешних
хранилищ. В k3s это встроено для простоты.
5. `metrics-server` — собирает данные об использовании ресурсов (CPU, память) подов и узлов. Это нужно для команд
вроде `kubectl top` или для Horizontal Pod Autoscaler (HPA). Установку метрик можно отключить при запуске k3s
флагом `--disable=metrics-server`.
6. `svclb-traefik` - это под для балансировки нагрузки (Service Load Balancer) для Traefik. В k3s нет встроенного
облачного балансировщика (как в AWS/GCP), поэтому *svclb* эмулирует его на уровне узла, перенаправляя трафик
к сервисам типа LoadBalancer. У нас два таких контейнера:
* один для самой логики балансировки;
* другой для мониторинга или дополнительной функциональности (например, *keepalived* или аналога) и это зависит
от реализации в k3s.
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 нашего узла (кластера) отвечает на порту `6443` (порт по умолчанию):
```bash
curl -k https://192.168.1.27:6443
```
Здесь ключ `-k` означает, что мы не проверяем сертификаты (нам важно только, что сервер отвечает). Должны получить
Unauthorized JSON-ответ от API. Что-то вроде:
```json
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
```
ВАЖНО: Надо проверить версию 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`. Это и есть токен, который нужно будет использовать.
Теперь на втором Orange Pi (например, с IP 192.168.1.28) можно запустить второй мастер-узел (вставим токен
из предыдущего шага):
```bash
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 мастер-узла, чтобы наш новый узел мог подключиться к кластеру.
* `--token` — токен аутентификации из предыдущего шага.
* `--tls-san=192.168.1.28` — добавляет IP нашего второго мастера в сертификаты (для будущих подключений).
Проверим какие теперь ноды в кластере:
```bash
sudo k3s kubectl get nodes
```
Теперь увидим две ноды:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-2 Ready control-plane,etcd,master 2h v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 110s v1.31.5+k3s1
```
Проверим поды кластера и посмотрим на каких нодах они запущены:
```bash
sudo k3s kubectl get pods -A -o wide
```
И увидим, что на второй ноде запустились те же поды, что и на первой:
```text
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-ccb96694c-tfjwj 1/1 Running 0 2h 10.42.0.4 opi5plus-2 <none> <none>
kube-system helm-install-traefik-crd-bdbgd 0/1 Completed 0 2h <none> opi5plus-2 <none> <none>
kube-system helm-install-traefik-mlztm 0/1 Completed 1 2h <none> opi5plus-2 <none> <none>
kube-system local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 2h 10.42.0.3 opi5plus-2 <none> <none>
kube-system metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 2h 10.42.0.2 opi5plus-2 <none> <none>
kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0 2h 10.42.0.7 opi5plus-2 <none> <none>
kube-system svclb-traefik-4f8c2580-xzt5d 2/2 Running 0 2m35s 10.42.1.2 opi5plus-3 <none> <none>
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.
Он эмулирует облачный балансировщик нагрузки (типа AWS ELB), которого нет в локальном окружении вроде Orange Pi.
SLB перенаправляет внешний трафик (например, на порты 80/443) к сервисам типа LoadBalancer внутри кластера.
## Подключение третьего узла (воркера)
Добавление третьего узда в качестве воркера (рабочего узла) мы сделаем временно. Во-первых, чтобы показать как это
делается, а во-вторых, чтобы показать как удалять узел и с какими особенностями это связано. И наконец, в-третьих,
объяснить что такое кворум и почему важно, чтобы в кластере было нечетное количество мастер-узлов.
И так, подключение рабочего узла даже проще, чем мастера. Выполним на нашем новом узле:
```bash
curl -sfL https://get.k3s.io | sh -s - agent --server https://192.168.1.10:6443 --token <ТОКЕН>
```
Здесь ключ:
* `agent` — устанавливает узел в режиме воркера (worker). Это значит, что узел будет выполнять рабочие нагрузки
(поды), но не будет управлять кластером (без *control-plane*, *master* и на нем нет реплики *etcd*).
Посмотрим на ноды (команда выполняется на одном из мастер-узлов):
```bash
sudo k3s kubectl get nodes
```
Теперь у нас три ноды, и все они имеют статус *Ready*:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-1 Ready <none> 96s v1.31.5+k3s1
opi5plus-2 Ready control-plane,etcd,master 3h v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 2h v1.31.5+k3s1
```
Новая нода `opi5plus-1` готова к работе и не имеет ролей, а только выполняет рабочие нагрузки (поды).
Посмотрим на поды:
```bash
sudo k3s kubectl get pods -n kube-system -o wide
```
И увидим, что на новом воркере (opi5plus-1) запустился под балансировщика `svclb-traefik`:
```text
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-ccb96694c-tfjwj 1/1 Running 0 3h 10.42.0.4 opi5plus-2 <none> <none>
helm-install-traefik-crd-bdbgd 0/1 Completed 0 3h <none> opi5plus-2 <none> <none>
helm-install-traefik-mlztm 0/1 Completed 1 3h <none> opi5plus-2 <none> <none>
local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 3h 10.42.0.3 opi5plus-2 <none> <none>
metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 3h 10.42.0.2 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-4q7dj 3/3 Running 0 92s 10.42.2.2 opi5plus-1 <none> <none>
svclb-traefik-4f8c2580-h7b9c 3/3 Running 0 2h 10.42.0.9 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-qmzf6 3/3 Running 0 2h 10.42.1.5 opi5plus-3 <none> <none>
traefik-6c979cd89d-98fk8 1/1 Running 0 1h 10.42.1.6 opi5plus-3 <none> <none>
```
Посмотрим состояние сервисов в кластере:
```bash
sudo k3s kubectl get service -n kube-system
```
Увидим, что сервис *traefik* доступен на всех нодах:
```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.43.0.10 <none> 53/UDP,53/TCP,9153/TCP 3d
metrics-server ClusterIP 10.43.248.208 <none> 443/TCP 3d
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
```
Можем так же настроить [web-панель Traefik](../kubernetes/k3s-setting-up-web-access-to-dashboard.md).
![k3s--traefic-dashboard.png](../images/k3s--traefik-dashboard.png)
Что ж, теперь у нас есть кластер k3s с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не
идеальная конфигурация, так как у нас четное количество мастер-узлов.
Попробует отключить один из мастеров (не обязательно выключать питание, достаточно отсоединить сетевой кабель ethernet)
и посмотрим что произойдет.
Само-собой доступ к панели Traefik на "погашенном узле" пропадет, но с обоих работающих узлов (живого мастера
и воркера) сохранится. И еще будет потеряна возможность работать с кластером через `kubectl`. Почему kubectl
не работает на втором мастере? Ошибка на втором мастере после отключения первого говорит о том, что кластер потерял
полную функциональность API-сервера. Как говорилось ранее, k3s с настройкой HA (высокая доступность) используется
встроенный etcd для хранения состояния. Для работы etcd в HA-режиме требуется кворум.
Кворум в etcd — это минимальное количество узлов, которые должны быть доступны для согласования данных и принятия
решений в кластере. Это основа отказоустойчивости распределённой системы. При двух мастерах: **Кворум = N/2 + 1**,
где N — количество мастер-узлов. Для 2 узлов: *кворум = 2/2 + 1 = 2*. Это значит, что оба мастера должны быть живы,
чтобы etcd работал. Если один мастер падает, второй не может достичь кворума (1 < 2) и останавливает работу etcd.
Без etcd API-сервер на втором мастере не может отвечать на запросы kubectl, хотя поды продолжают работать, так как
им не нужен доступ к etcd в реальном времени.
В чем может быть смысл иметь два мастера? Это обеспечивает репликацию данных (второй хранит копию etcd), но не
даёт отказоустойчивости — когда один мастер упал, кластер становится неуправляемым (нет управления через kubectl),
рабочие нагрузки (поды) могут продолжать работать, пока жив хотя бы один узел, но новые изменения (развертывание
подов и обновления) невозможны.
Таким образом, два мастера это не идеальная HA (High Availability), а скорее "полу-HA". Полная HA начинается
с трёх узлов! Три мастера — это стандарт для настоящей отказоустойчивости в Kubernetes (и k3s). При трёх мастерах:
**Кворум = 3/2 + 1 = 2**. Это значит, что кластер остаётся рабочим, если один мастер уме, но живы минимум 2 из 3.
Два оставшихся поддерживают кворум (2 >= 2), и кластер полностью управляем (kubectl работает и можно деплоить поды).
### Удаление узла из кластера
Чтобы снова получить возможность управлять кластером включим погашенный мастер-узел, подождем пока кворум восстановится
и удалим с k3s воркер-узел (opi5plus-1):
```bash
sudo /usr/local/bin/k3s-agent-uninstall.sh
```
Теперь состояние узлов в кластере:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-1 NotReady <none> 147m v1.31.5+k3s1
opi5plus-2 Ready control-plane,etcd,master 3d2h v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 2d v1.31.5+k3s1
```
Нода со статусом `NotReady` с ролью `<none>` — это остатки бывшего воркера. Если запустить на том же хосте масте, узел
может "ожить" и перерегистрироваться с новыми ролями. Но это не обязательно удалит старый объект Node — он может
либо обновиться (если имя совпадает), либо создать дубликат, что приведёт к путанице. Надежнее удалить старый узел из
кластера:
```bash
sudo k3s kubectl delete node opi5plus-1
```
Теперь состояние узлов:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-2 Ready control-plane,etcd,master 3d2h v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 2d v1.31.5+k3s1
```
После удаления узла, проверим состояние подов кластера (правильнее, конечно, было бы проверить поды до удаления узла,
но, допустим, мы имитировали ситуацию "смерти" узла):
```bash
sudo k3s kubectl get pods -n kube-system -o wide
```
Увидим:
```text
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-ccb96694c-tfjwj 1/1 Running 0 4d19h 10.42.0.4 opi5plus-2 <none> <none>
helm-install-traefik-crd-bdbgd 0/1 Completed 0 4d19h <none> opi5plus-2 <none> <none>
helm-install-traefik-mlztm 0/1 Completed 1 4d19h <none> opi5plus-2 <none> <none>
local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 4d19h 10.42.0.3 opi5plus-2 <none> <none>
metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 4d19h 10.42.0.2 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-h7b9c 3/3 Running 0 2d18h 10.42.0.9 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-nhz65 3/3 Running 0 38h 10.42.2.2 opi5plus-1 <none> <none>
svclb-traefik-4f8c2580-qmzf6 3/3 Running 0 2d18h 10.42.1.5 opi5plus-3 <none> <none>
traefik-6c979cd89d-98fk8 1/1 Terminating 0 2d15h 10.42.1.6 opi5plus-3 <none> <none>
traefik-6c979cd89d-t4rhw 1/1 Running 0 38h 10.42.2.3 opi5plus-1 <none> <none>
```
Если бы у нас были рабочие поды на удаленном узле, то они бы перезапустились на других нодах. Но, у нас там был только
`svclb-traefik`, который теперь стал в статусе `Terminating`. Это процесс удаления пода. Kubernetes не сразу удаляет
поды, особенно если они находятся в состоянии "зависания" (например, `Terminating` или `Running`, но стали недоступны).
Так как агент удалён вместе с узлом, то некому сообщить кластеру, что под завершил работу, и он остается "призраком"
в списке. Удалим под `svclb-traefik` вручную (не забудьте заменить `xxxxxxxxx-xxxxx` на реальные значения
`<хеш-ревизии>`и `<суффикс>`):
```bash
sudo k3s kubectl delete pod svclb-traefik-xxxxxxxxx-xxxxx -n kube-system --force --grace-period=0
```
Здесь `--force` и `--grace-period=0` говорят Kubernetes удалить под "форсированно" и "немедленно". Даже если узел
недоступен. Так как это DaemonSet, он не перезапустится на opi5plus-1, потому что узел уже NotReady.
## Добавление третьего мастера
Теперь у нас осталось две мастер-ноды и можно добавить третий мастер. Как это сделать, см выше. Но теперь
при добавлении можно в флаге `--server` указать IP как первого, так и второго мастера. И не забудьте в `--tls-san`
указать IP хоста нового (третьего) мастера.
### Тюнинг kube-dns
После установки можно попробовать отключить один из мастеров и убедиться, что кластер остаётся работоспособным,
а спустя некоторое время (иногда 10-15 минут) поды с погашенного мастера перезапустятся на других нодах. Например:
```text
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-ccb96694c-wzh96 1/1 Running 0 101m 10.42.1.8 opi5plus-3 <none> <none>
local-path-provisioner-5cf85fd84d-s9frj 1/1 Running 0 101m 10.42.1.9 opi5plus-3 <none> <none>
metrics-server-5985cbc9d7-q525g 1/1 Terminating 0 101m 10.42.2.4 opi5plus-1 <none> <none>
metrics-server-5985cbc9d7-v8vlt 1/1 Running 0 29m 10.42.0.12 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-h7b9c 3/3 Running 3 (35m ago) 3d2h 10.42.0.10 opi5plus-2 <none> <none>
svclb-traefik-4f8c2580-nhz65 3/3 Running 0 47h 10.42.2.2 opi5plus-1 <none> <none>
svclb-traefik-4f8c2580-qmzf6 3/3 Running 3 (133m ago) 3d2h 10.42.1.7 opi5plus-3 <none> <none>
traefik-6c979cd89d-t4rhw 1/1 Terminating 0 46h 10.42.2.3 opi5plus-1 <none> <none>
traefik-6c979cd89d-z6wwm 1/1 Running 0 29m 10.42.0.11 opi5plus-2 <none> <none>
```
Хотя, в целом, кластер остается рабочим, и сам чинится при отключении и восстановлении узлов, но если отключается нода
на которой исполняется под с `coredns` — то временно будет затруднен перезапуска и создание новых подов, а значит
и "переезд" подов с погашенного узла, до восстановления `coredns` тоже будет замедлен. Кроме того, если сценарий
приложения(ий) развернутых внутри k3s предполагает переподключение с использованием имен подов или обнаружение подов,
то это тоже перестанет работать.
Решением может быть использование двух реплик `coredns` (вместо одной). Откроем файл конфигурации k3s на редактирование:
```bash
sudo k3s kubectl edit deployment coredns -n kube-system
```
Здесь:
* `kubectl edit` — Открывает редактор (по умолчанию *vim*) для изменения ресурса Kubernetes напрямую в кластере.
Вместо создания локального YAML-файла и применения его через `kubectl apply`, мы сразу редактируем "живой" конфиг.
* `deployment coredns` — Указывает, что редактируем объект типа *deployment* с именем `coredns`. Deployment — это
контроллер, который управляет набором подов (в данном случае coredns), обеспечивая их количество (реплики),
перезапуск и обновления.
* `-n kube-system` — Указывает пространство имён (namespace), где находится *coredns8. В k3s системные компоненты,
к которым относится *coredns(, обычно живут в kube-system.
В открывшемся окне найдем строку `replicas: 1` и заменим её на `replicas: 2`.
```yaml
spec:
progressDeadlineSeconds: 600
replicas: 2
revisionHistoryLimit: 0
```
Сохраним изменения и выйдем из редактора. Изменения сразу применятся, и k3s создаст вторую реплику `coredns`:
```text
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>
```
А затем:
```text
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`.
Он имеет фиксированный *Cluster IP* (внутренний IP-адрес кластера) и балансирует запросы между всеми зарегистрированными
подами `coredns` (у нас теперь две реплики). Каждый под `coredns` регистрируется как endpoint в `kube-dns` при старте.
Посмотеть endpoint'ы сервиса `kube-dns` можно командой:
```bash
sudo k3s kubectl get endpoints kube-dns -n kube-system
```
И увидим, что у `kube-dns` несколько endpoint'ов (IP-адресов подов `coredns`) включая оба новых и старые, которые
гасили при экспериментах с устойчивостью кластера:
```text
NAME ENDPOINTS AGE
kube-dns 10.42.1.8:53,10.42.2.6:53,10.42.1.8:53 + 3 more… 5d23h
```
Каждый под `coredns` — самостоятельный DNS-сервер. Они не взаимодействуют друг с другом и не обмениваются данными. Это
просто экземпляры одного и того же сервиса, работающие параллельно. Они независимы, получают данные из API Kubernetes
и отвечают на запросы параллельно. В каждом поде кластера в качестве DNS настроен `kube-dns` (задаётся в файле
`/etc/resolv.conf` внутри пода). Когда под отправляет DNS-запрос, его получит `kube-dns` и перенаправит запрос
к одному из доступных `coredns`. Балансировка происходит по случайного выбора (Round-Robin). Если один из `coredns`
недоступен (например, узел выключен), `kube-dns` не получит ответа, и направит запросы к живому `coredns`.
### Разные архитектуры на узлах кластера (гетерогенность)
Когда мы подключили узлы (мастеры и воркеры) к кластеру, мы использовали одинаковые Orange Pi 5 Plus. Но, в реальности,
кластеры Kubernetes часто состоят из узлов с разными архитектурами и характеристиками. Например, если подключить к
к кластеру Raspberry Pi 3B увидим примерно такую картину:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-1 Ready control-plane,etcd,master 3d3h v1.31.5+k3s1
opi5plus-2 Ready control-plane,etcd,master 6d3h v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 5d1h v1.31.5+k3s1
rpi3b Ready <none> 27s v1.31.6+k3s1
```
Но надо помнить, что разные архитектуры могут быть оказаться несовместимы с некоторыми приложениями и образами.
Например, Raspberry Pi 3B — это 32-битный ARMv7 (armv7l), а Orange Pi 5 Plus — 64-битный ARMv8 (aarch64). Если в
подах используются бинарные файлы, скомпилированные под определённую архитектуру, то они могут не работать на узлах
с другой архитектурой. Также, некоторые образы Docker могут быть доступны только для определённых архитектур.
В ограниченном объеме можно подключать узлы на других платформах. Например, Windows, может иметь только воркер-узлы на k8s
(начиная с версии 1.14), а в k3s экспериментальная поддержка Windows-воркеров (начиная с с версии 1.24). На macOS нет
официальной поддержки Kubernetes/k3s для узлов на macOS (можно использовать обходные пути с использованием виртуальныех
машин).
> **На всякий случай:**
>
> Если для вашего Kubernetes-кластера требуется блочное хранилище `longhorn` (для обеспечения репликации файлов между узлами кластера и высокой доступности данных), то понадобится модуль `iSCSI` (_Internet Small Computer System Interface_) на уровне системы. В составе Ubuntu 22.04 для Orange Pi 5 этого модуля нет. Потребуется [компиляция ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md).
### Добавление узлов во "внешнем" интернете
В моем проекте (специализированном поисковике) будет нужно парсить и интернет сайты, включая заблокированные сайты.
К сожалению современный интернет имеет взаимные региональные блокировки и просто использовать VPN интернет-соединения
не сработает. Выходом может стать использование воркер-узлов во внешнем интернете. Идея в том, что если какой-нибудь
URL не получится обработать на поде одного узла, то можно попробовать обработать его на другом узле, в другой локации.
#### Настройка Keepalived
Так как узлы k3s взаимодействуют через API на 6443-порте, то для доступа к кластеру из внешнего интернета нужно будет
обеспечить проброс трафика через роутер сети на один из мастер-узлов. НО у нас три мастер-узла, а значит если упадет
узел на который происходит проброс, то удаленный воркер-узел "отвелится" и потеряет доступ к кластеру. Объединить
IP всеx мастер-узлов в один можно с помощью балансировщика нагрузки **Keepalived**. Он создает виртуальный IP-адрес
(VIP), c которого перенапрвляет трафик на один из мастер-узлов, и если этот узел упадет, то трафик перенаправится
на другой и так далее.
Установи `Keepalived` на все мастер-ноды:
```bash
sudo apt update
sudo apt install keepalived
```
Настроим `Keepalived` последовательно на каждом мастере. Для этого отредактируем (создадим) файл конфигурации
`/etc/keepalived/keepalived.conf`:
```bash
sudo nano /etc/keepalived/keepalived.conf
```
На первом мастер-узле (хост — `opi5plus-1`, IP — `192.168.1.26`):
```pycon
vrrp_instance VI_1 {
state MASTER # ЭТО ГЛАВНЫЙ ХОСТ. ПО УМОЛЧАНИЮ ТРАФИК С VIP БУДЕТ ПЕРЕНАПРАВЛЯТЬСЯ НА ЭТОТ ХОСТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
virtual_router_id 51
priority 100 # Самый высокий приоритет
advert_int 1
unicast_src_ip 192.168.1.26 # IP текущего хоста (opi5plus-1)
unicast_peer {
192.168.1.27 # IP второго хоста (opi5plus-2)
192.168.1.28 # IP третьего хоста (opi5plus-3)
}
virtual_ipaddress {
192.168.1.200 # Виртуальный IP (VIP), он должен быть исключен из диапазона DHCP
}
}
```
На втором мастер-узле (хост — `opi5plus-2`, IP — `192.168.1.27`):
```pycon
vrrp_instance VI_1 {
state BACKUP # ЭТО ВТОРОЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ ХОСТ УПАДЕТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
virtual_router_id 51
priority 90 # Меньший приоритет
advert_int 1
unicast_src_ip 192.168.1.27 # IP текущего хоста (opi5plus-2)
unicast_peer {
192.168.1.26 # IP первого хоста (opi5plus-1)
192.168.1.28 # IP третьего хоста (opi5plus-3)
}
virtual_ipaddress {
192.168.1.200 # Виртуальный IP
}
}
```
И, наконец, на третьем мастер-узле (хост — `opi5plus-3`, IP — `192.168.1.28`):
```pycon
vrrp_instance VI_1 {
state BACKUP # ЭТО ТРЕТИЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ- И БЭКАП-ХОСТ УПАДЕТ
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и этот узел подключен по enP4p65s0
virtual_router_id 51
priority 80 # Еще меньший приоритет
advert_int 1
unicast_src_ip 192.168.1.28 # IP текущего хоста (opi5plus-3)
unicast_peer {
192.168.1.27 # IP первого хоста (opi5plus-1)
192.168.1.28 # IP второго хоста (opi5plus-2)
}
virtual_ipaddress {
192.168.1.200 # Виртуальный IP
}
}
```
Добавим `Keepalived` в автозагрузку на всех мастер-узлах и запустим его:
```bash
sudo systemctl enable keepalived
sudo systemctl start keepalived
```
Теперь, если вы на первом мастер-узле (opi5plus-1) проверить доступные IP-адреса:
```bash
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`). Это "точечная" подсеть, содержащая только один адрес, не привязан к основной
подсети интерфейса (/24) и это позволяет VIP "плавать" между узлами, не вызывая конфликтов с основными IP-адресами
и не требуя изменения подсети на каждом узле. VIP рассматривается как уникальный адрес, не требующий маршрутизации,
он просто "привязан" к интерфейсу.
Теперь панель `Traefik` доступна по VIP-адресу `http://192.168.1.200:9000/dashboard/#`, т.к. трафик с этого адреса
будет перенаправлен на один из мастер-узлов.
API Kubernetes тоже теперь доступен по VIP-адресу. Все воркер-узлы, подключенные к кластеру, лучше подключать к
кластеру через VIP-адрес. Сами мастер узлы знают свои IP и взаимодействую через `etcd`, но воркеры подключаясь
через VIP будут более устойчивы к сбоям мастер-узлов. Подсоединить удаленный воркер-узел к кластеру лучше через VIP.
Для этого нужно на роутере сети настроить проброс порта `6443` с внешнего IP роутера, на виртуальный IP-адрес внутри
сети (тоже на `6443` порт). После проверить, что с внешнего хоста API Kubernetes доступно:
```bash
curl -k https://<PUBLIC_IP_ROUTER>:6443
```
Если отклик есть (например, `Unauthorized`), то можно подключить удаленый воркер-узел к кластеру:
```bash
curl -sfL https://get.k3s.io | sh -s - agent --server https://<PUBLIC_IP_ROUTER>:6443 --token <TOKEN>
```
Когда процесс завершится, на любом мастер-узле можно проверить, что воркер-узел подключился:
```bash
sudo k3s kubectl get nodes
```
Получим, например:
```text
NAME STATUS ROLES AGE VERSION
opi5plus-1 Ready control-plane,etcd,master 1d4h v1.31.5+k3s1
opi5plus-2 Ready control-plane,etcd,master 1d v1.31.5+k3s1
opi5plus-3 Ready control-plane,etcd,master 1d2h v1.31.5+k3s1
rpi3b Ready <none> 25h v1.31.6+k3s1
vps-sw-eye Ready <none> 35m v1.31.6+k3s1
```
#### Проблема Flannel для внешних узлов
Узлы во внешнем интернете создаются и управляются через Kubernetes API, используя порт `6443/TCP`. Но, для того чтобы
передавать трафик и данные между узлами k3s использует сетевой плагин **Flannel**. Он встроен в бинарник k3s (в отличие
от k8s, где Flannel работают как поды) и использует overlay-сеть (`10.42.x.x`). Это внутренняя сеть k3s, VXLAN-туннель
между всеми узлами кластера (мастерами и воркерами). Flannel использует порт `8472/UDP`.
К сожалению проброс порта `8472` с внешнего хоста в домашнюю сеть через роутер не поможет, так обмен идёт не через TCP-,
а UDP-порт. Внешний узел отправит пакеты через overlay-сеть Flannel (10.42.x.x) через 8472/UDP к мастеру, но Мастер
отвечает через свой реальный IP (192.168.1.x), который недоступен напрямую из интернета без обратного проброса или VPN.
Проброс <PUBLIC_IP>:8472 → <VIP>>:8472 позволяет трафику от внешнего хоста доходить до домашней сети, но ответный
трафик от мастеров к VPS (например, от <NODE-1>:8472 к <VPS_IP>:8472) не будет проходить, потому что NAT в роутере
"односторонний" — он не знает, как маршрутизировать UDP-ответы от мастеров обратно к VPS через интернет.
Таким образом, для управления удаленным узлом нужно чтобы он имел локальный IP-адрес в домашней сети, а не внешний.
SSH-тоннель с помощью `autossh` и упаковкой UDP-трафика в TCP через `socat` не сработает (а я надеялся). Таким образом
"пробросить" Flannel для полноценного подключения удаленного k3s-узла — VPN-туннель между каждой мастер-нодой на
удаленный узел. Это вполне рабочия вариант, если удаленные узлы — полноценные и произвольные хосты. Но в моём
случае удаленный узел — хост на 1 ядро и 1 ГБ ОЗУ. К тому же он на платформе x86_64, а не ARM, а значит ради одного
узла не стоит заморачиваться с VPN.
Другим вариантом является подключение внутри самих подов на удаленном узле к необходимым сервисам напрямую. Но таким
образом порты базы данных, менеджеров очередей и т.д. будут доступны из интернета, что небезопасно и в целом плохая идея.
-----
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
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,448 @@
# Измерение скорости накопителей (HDD, SSD, USB Flash, NAS и т.д.) под Linux
Способов измерения скорости накопителей под Linux много, и можно подобрать такие, что более точно покажут
производительность именно на ваших задачах.
## Самые простые измерения с помощью `dd`
Официально в Unix/Linux не существует строгого определения, что такое `dd`. Утилита берёт свое название
из команды "Data Definition" в языке описания данных IBM JCL (Job Control Language) для мейнфреймов.
Название намекает на ее основное предназначение — копирование и преобразование данных.
Использование `dd` для измерения скорости чтения и записи диска — не очень точные, т.к. это, по сути,
обычное копирование данных. Не учитываются такие факторы, такие как кэширование операционной системы,
фрагментация диска и т.д.
Тем не менее, `dd` подходит для быстрого теста. Вот, например, как это сделать:
#### Запись (Write)
```shell
dd if=/dev/zero of=/path/to/testfile bs=1G count=1 oflag=direct
```
Где:
* `if=/dev/zero` — указывает, что источник данных — это непрерывный поток нулей;
* `of=/path/to/testfile` — задает файл, в который записываются данные. Убедитесь, что путь указывает на тестируемый диск;
* `bs=1G` — размер блока (здесь 1 гигабайт за раз);
* `count=1` — количество блоков для записи;
* `oflag=direct` — обходит кэш операционной системы, что обеспечивает более точное измерение.
Результат для моего SSD NVMe Kingston SNV2S/2000G в Orange Pi 5 Plus:
```text
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.01244 s, 1.1 GB/s
```
Здесь 1.1 GB/s — это скорость записи. Результат для eMCC памяти Orange Pi 5 Plus:
```text
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 4.76196 s, 225 MB/s
```
Как видим, скорость записи NVMe SSD более чем в 4 раза быстрее, чем у eMMC.
### Чтение (Read)
Теперь у нас есть тестовый файл, который мы можем использовать для измерения скорости чтения:
```shell
dd if=/path/to/testfile of=/dev/null bs=1G iflag=direct
```
Где:
* `if=/path/to/testfile` — файл, созданный в предыдущем тесте.
* `of=/dev/null` — данные не сохраняются, что позволяет измерять только скорость чтения.
* `bs=1G` — размер блока.
* `iflag=direct` — обходит кэш операционной системы.
Результат для SSD NVMe Kingston SNV2S/2000G в Orange Pi 5 Plus:
```text
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.74686 s, 1.4 GB/s
```
Для eMMC Orange Pi 5 Plus:
```text
1+0 records in
1+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 3.8901 s, 276 MB/s
```
Как видим, скорость чтения NVMe SSD более чем в 5 раз выше, чем у eMMC.
### Советы по тестированию с помощью `dd`
* Используйте достаточно большой файл чтобы результаты были более репрезентативными. В идеале размер файла должен превышать объем оперативной памяти (правда это не всегда возможно).
* Очистите кэш перед тестом чтения: `sync; echo 3 > /proc/sys/vm/drop_caches`. Это предотвратит влияние системного кэша на тест.
* Помните: `dd` не моделирует реальное рабочее окружение. Результаты могут отличаться от повседневной нагрузки на диск.
* Не забудьте удалить тестовый файл после теста: `rm /path/to/testfile`
-----
## Измерение скорости операций ввода/вывода c помощью `ioping`
Исходя тз названия, казалось бы, утилита `ioping` проверяет задержки накопителей данных. И это справедливо для NAS. Она похож на ping, но для дисков. Но в реальности `ioping` — это более универсальный инструмент, который может использоваться для измерения скорости операций ввода/вывода (IOPS) и пропускной способности дисков.
Установим ioping:
```shell
sudo apt install ioping
```
Замер:
```shell
ioping -c 10 -s 1m -W .
```
Где:
* `-c 10` — количество замеров;
* `-s 10m` — размер блока данных для тестирования. `10m` — 10 мегабайт. В целом `ioping` — это тест производительности дисковой подсистемы, через измерения задержек при доступе к небольшим блокам данных (обычно 4 KiB). Но отлично работает и на больших блоках, оказывая производительность диска в целом.
* `-W`С записью вместо чтения. Это более безопасно для временного файла в целевом каталоге, и, кроме того, запись ввода-вывода даёт более надежные результаты для систем, где некэшированные чтения не поддерживаются
или происходит искажающее замеры кэширование на каком-то уровне.
* `.` — каталог, в котором будут производиться замеры. `.` — текущий каталог.
Другие ключи и опции можно посмотреть в справке `man ioping` или `ioping --help`.
Результаты теста будут примерно такими:
```text
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=1 time=21.1 ms (warmup)
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=2 time=29.7 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=3 time=23.5 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=4 time=22.3 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=5 time=21.7 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=6 time=23.1 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=7 time=21.7 ms (fast)
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=8 time=31.1 ms (slow)
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=9 time=23.0 ms
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=10 time=22.7 ms
--- . (ext4 /dev/nvme0n1p2 1.79 TiB) ioping statistics ---
9 requests completed in 218.8 ms, 90 MiB written, 41 iops, 411.3 MiB/s
generated 10 requests in 9.04 s, 100 MiB, 1 iops, 11.1 MiB/s
min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms
```
Сверху выводятся данные по индивидуальным запросам:
```text
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=1 time=21.1 ms (warmup)
```
Где:
* `10 MiB` — размер блока данных, к которому идет доступ;
* `<<< .` — текущая директория, на которой проводится тест;
* `(ext4 /dev/nvme0n1p2 1.79 TiB)` — файловая система (ext4) и устройство (/dev/nvme0n1p2) с емкостью 1.79 ТБ, где выполняется тест;
* `request=1` — номер запроса;
* `time=21.1 ms` — время выполнения запроса (в миллисекундах — `ms`, в микросекунды — `us`);
* `(warmup)` — первый запрос может использоваться для "прогрева", его результаты могут быть исключены из финальной статистики, `(fast)` — запрос выполнен быстрее, чем среднее время выполнения, `(slow)` — запрос выполнен медленнее, чем среднее время выполнения.
Снизу выводятся статистические данные по всем запросам:
```text
9 requests completed in 218.8 ms, 90 MiB written, 41 iops, 411.3 MiB/s
generated 10 requests in 9.04 s, 100 MiB, 1 iops, 11.1 MiB/s
min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms
```
Где:
* `9 requests completed in 218.8 ms` — количество запросов, завершенных за время теста и общее время выполнения;
* `90 MiB written` — общий объем данных, записанных на диск;
* `41 iops` — количество операций ввода/вывода в секунду (IOPS);
* `411.3 MiB/s` — скорость записи данных на диск;
* `generated 10 requests in 9.04 s` — количество запросов, сгенерированных за время теста и общее время выполнения;
* `100 MiB` — общий объем данных, сгенерированных тестом;
* `1 iops` — количество операций ввода/вывода в секунду (IOPS) при генерации данных;
* `11.1 MiB/s` — скорость генерации данных;
* `min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms`**минимальное**, **среднее**, **максимальное** задержка выполнения запросов и **стандартное отклонение**, показатель того, насколько задержки разнятся между запросами.
-----
## Тесты накопителей с помощью `fio`
`fio` (Flexible I/O Tester) — мощный инструмент для тестирования производительности систем ввода-вывода (I/O) на
дисках и файловых системах. Он гибко настраивается, поддерживает разные режимы ввода-вывода, размеры блоков,
параллелизм и типы нагрузок. Используется для оценки скорости чтения/записи, задержек и IOPS (число операций
ввода/вывода в секунду).
Установка fio:
```shell
sudo apt install fio
```
Запуск теста (например тест с высокой нагрузкой, эмулирующий многопоточный доступ к диску с небольшими блоками данных):
```shell
fio --name=seq_nvme --size=10G --bs=1M --bs=4k --direct=1 --rw=randrw --rwmixread=70 --numjobs=4 --iodepth=32 --runtime=60
```
Где:
* `--name=seq_nvme` — имя теста.
* `--size=10G` — размер файла для теста (10 ГБ).
* `--bs=1M` — размер блока данных (1 МБ).
* `--bs=4k` — размер блока данных (4 КБ).
* `--direct=1` — обход кэша операционной системы.
* `--rw=randrw` — тип операции (случайное чтение и запись). Также можно установить и другие параметры: `write` — тип операции (последовательная запись). `read` — последовательное чтение, `randwrite` — случайная запись, `randread` — случайное чтение, `rw` — смешанный режим (чтение и запись).
* `--rwmixread=70` — процент операций чтения/запись (70% чтения, 30% записи).
* `--numjobs=4` — количество потоков (заданий), которые одновременно выполняются.
* `--iodepth=32` — количество операций ввода-вывода, которые отправляются одновременно.
* `--runtime=60` — продолжительность теста (60 секунд), для длинных тестов используйте `--time_based` чтобы тест завершался по времени.
И так далее. Еще есть ключ `--output=test.log` для сохранения результатов в log-файл. Подробнее о параметрах можно посмотреть в справке `man fio` или `fio --help`.
Результаты выполнения вышеуказанной команды для моего NVMe SSD (Kingston SNV2S/2000G) были следующие:
```text
seq_nvme: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=32
...
fio-3.28
Starting 4 processes
Jobs: 4 (f=4): [m(4)][100.0%][r=33.4MiB/s,w=15.1MiB/s][r=8550,w=3861 IOPS][eta 00m:00s]
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44678: Fri Nov 29 23:41:45 2024
read: IOPS=2415, BW=9662KiB/s (9894kB/s)(566MiB/60001msec)
clat (usec): min=60, max=41549, avg=298.88, stdev=321.06
lat (usec): min=61, max=41550, avg=300.04, stdev=321.13
clat percentiles (usec):
| 1.00th=[ 76], 5.00th=[ 99], 10.00th=[ 135], 20.00th=[ 165],
| 30.00th=[ 190], 40.00th=[ 225], 50.00th=[ 281], 60.00th=[ 306],
| 70.00th=[ 334], 80.00th=[ 371], 90.00th=[ 420], 95.00th=[ 486],
| 99.00th=[ 1663], 99.50th=[ 1991], 99.90th=[ 2573], 99.95th=[ 3228],
| 99.99th=[ 4490]
bw ( KiB/s): min= 6888, max=12838, per=24.33%, avg=9668.65, stdev=1269.47, samples=119
iops : min= 1722, max= 3209, avg=2417.03, stdev=317.35, samples=119
write: IOPS=1033, BW=4133KiB/s (4232kB/s)(242MiB/60001msec); 0 zone resets
clat (usec): min=19, max=21700, avg=224.76, stdev=261.15
lat (usec): min=19, max=21702, avg=226.79, stdev=261.28
clat percentiles (usec):
| 1.00th=[ 32], 5.00th=[ 33], 10.00th=[ 46], 20.00th=[ 76],
| 30.00th=[ 106], 40.00th=[ 141], 50.00th=[ 174], 60.00th=[ 217],
| 70.00th=[ 293], 80.00th=[ 334], 90.00th=[ 396], 95.00th=[ 453],
| 99.00th=[ 1483], 99.50th=[ 1893], 99.90th=[ 2442], 99.95th=[ 2966],
| 99.99th=[ 4047]
bw ( KiB/s): min= 2784, max= 5516, per=24.23%, avg=4138.13, stdev=561.74, samples=119
iops : min= 696, max= 1379, avg=1034.42, stdev=140.40, samples=119
lat (usec) : 20=0.01%, 50=3.27%, 100=9.03%, 250=38.81%, 500=44.77%
lat (usec) : 750=2.25%, 1000=0.21%
lat (msec) : 2=1.21%, 4=0.44%, 10=0.01%, 20=0.01%, 50=0.01%
cpu : usr=7.75%, sys=19.95%, ctx=206950, majf=0, minf=29
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=144938,61997,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44679: Fri Nov 29 23:41:45 2024
read: IOPS=2390, BW=9562KiB/s (9791kB/s)(560MiB/60001msec)
clat (usec): min=63, max=22486, avg=293.54, stdev=282.59
lat (usec): min=63, max=22487, avg=294.85, stdev=282.65
clat percentiles (usec):
| 1.00th=[ 94], 5.00th=[ 129], 10.00th=[ 147], 20.00th=[ 165],
| 30.00th=[ 188], 40.00th=[ 217], 50.00th=[ 247], 60.00th=[ 297],
| 70.00th=[ 322], 80.00th=[ 363], 90.00th=[ 416], 95.00th=[ 482],
| 99.00th=[ 1663], 99.50th=[ 2008], 99.90th=[ 2606], 99.95th=[ 3195],
| 99.99th=[ 4359]
bw ( KiB/s): min= 6944, max=13312, per=24.09%, avg=9573.84, stdev=1150.52, samples=119
iops : min= 1736, max= 3328, avg=2393.38, stdev=287.64, samples=119
write: IOPS=1027, BW=4110KiB/s (4209kB/s)(241MiB/60001msec); 0 zone resets
clat (usec): min=20, max=41544, avg=236.88, stdev=347.28
lat (usec): min=21, max=41545, avg=239.32, stdev=347.36
clat percentiles (usec):
| 1.00th=[ 32], 5.00th=[ 41], 10.00th=[ 90], 20.00th=[ 115],
| 30.00th=[ 137], 40.00th=[ 159], 50.00th=[ 186], 60.00th=[ 215],
| 70.00th=[ 277], 80.00th=[ 334], 90.00th=[ 392], 95.00th=[ 453],
| 99.00th=[ 1516], 99.50th=[ 1876], 99.90th=[ 2409], 99.95th=[ 3064],
| 99.99th=[ 4113]
bw ( KiB/s): min= 2904, max= 5680, per=24.10%, avg=4116.52, stdev=533.76, samples=119
iops : min= 726, max= 1420, avg=1029.00, stdev=133.43, samples=119
lat (usec) : 50=1.74%, 100=5.26%, 250=48.61%, 500=40.37%, 750=2.13%
lat (usec) : 1000=0.21%
lat (msec) : 2=1.22%, 4=0.45%, 10=0.01%, 20=0.01%, 50=0.01%
cpu : usr=9.60%, sys=22.32%, ctx=205089, majf=0, minf=31
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=143428,61649,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44680: Fri Nov 29 23:41:45 2024
read: IOPS=2596, BW=10.1MiB/s (10.6MB/s)(609MiB/60001msec)
clat (usec): min=65, max=40409, avg=284.56, stdev=285.63
lat (usec): min=65, max=40410, avg=285.50, stdev=285.70
clat percentiles (usec):
| 1.00th=[ 75], 5.00th=[ 96], 10.00th=[ 113], 20.00th=[ 159],
| 30.00th=[ 186], 40.00th=[ 206], 50.00th=[ 262], 60.00th=[ 293],
| 70.00th=[ 322], 80.00th=[ 359], 90.00th=[ 404], 95.00th=[ 474],
| 99.00th=[ 1631], 99.50th=[ 1975], 99.90th=[ 2540], 99.95th=[ 3064],
| 99.99th=[ 4146]
bw ( KiB/s): min= 8015, max=14080, per=26.15%, avg=10392.20, stdev=1341.91, samples=119
iops : min= 2003, max= 3520, avg=2597.87, stdev=335.53, samples=119
write: IOPS=1116, BW=4465KiB/s (4572kB/s)(262MiB/60001msec); 0 zone resets
clat (usec): min=24, max=41585, avg=198.90, stdev=319.52
lat (usec): min=25, max=41587, avg=200.53, stdev=319.59
clat percentiles (usec):
| 1.00th=[ 32], 5.00th=[ 32], 10.00th=[ 34], 20.00th=[ 59],
| 30.00th=[ 75], 40.00th=[ 96], 50.00th=[ 145], 60.00th=[ 190],
| 70.00th=[ 269], 80.00th=[ 314], 90.00th=[ 375], 95.00th=[ 429],
| 99.00th=[ 1369], 99.50th=[ 1795], 99.90th=[ 2376], 99.95th=[ 2999],
| 99.99th=[ 4490]
bw ( KiB/s): min= 3401, max= 5984, per=26.16%, avg=4468.75, stdev=561.75, samples=119
iops : min= 850, max= 1496, avg=1117.05, stdev=140.43, samples=119
lat (usec) : 50=5.17%, 100=12.96%, 250=36.62%, 500=41.68%, 750=1.84%
lat (usec) : 1000=0.19%
lat (msec) : 2=1.12%, 4=0.41%, 10=0.01%, 20=0.01%, 50=0.01%
cpu : usr=6.73%, sys=17.26%, ctx=222857, majf=0, minf=29
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=155819,66981,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44681: Fri Nov 29 23:41:45 2024
read: IOPS=2531, BW=9.89MiB/s (10.4MB/s)(593MiB/60000msec)
clat (usec): min=60, max=41793, avg=282.72, stdev=305.59
lat (usec): min=60, max=41794, avg=283.89, stdev=305.67
clat percentiles (usec):
| 1.00th=[ 76], 5.00th=[ 98], 10.00th=[ 131], 20.00th=[ 159],
| 30.00th=[ 184], 40.00th=[ 200], 50.00th=[ 237], 60.00th=[ 285],
| 70.00th=[ 318], 80.00th=[ 355], 90.00th=[ 408], 95.00th=[ 474],
| 99.00th=[ 1631], 99.50th=[ 1975], 99.90th=[ 2540], 99.95th=[ 3064],
| 99.99th=[ 4178]
bw ( KiB/s): min= 7624, max=14408, per=25.52%, avg=10140.96, stdev=1641.40, samples=119
iops : min= 1906, max= 3602, avg=2535.13, stdev=410.35, samples=119
write: IOPS=1092, BW=4369KiB/s (4474kB/s)(256MiB/60000msec); 0 zone resets
clat (usec): min=19, max=22481, avg=214.85, stdev=280.81
lat (usec): min=19, max=22483, avg=216.97, stdev=280.96
clat percentiles (usec):
| 1.00th=[ 31], 5.00th=[ 32], 10.00th=[ 37], 20.00th=[ 77],
| 30.00th=[ 113], 40.00th=[ 139], 50.00th=[ 165], 60.00th=[ 196],
| 70.00th=[ 251], 80.00th=[ 322], 90.00th=[ 383], 95.00th=[ 445],
| 99.00th=[ 1467], 99.50th=[ 1844], 99.90th=[ 2442], 99.95th=[ 3032],
| 99.99th=[ 4490]
bw ( KiB/s): min= 3192, max= 6072, per=25.61%, avg=4373.76, stdev=743.54, samples=119
iops : min= 798, max= 1518, avg=1093.28, stdev=185.89, samples=119
lat (usec) : 20=0.01%, 50=4.00%, 100=8.72%, 250=45.79%, 500=37.74%
lat (usec) : 750=1.97%, 1000=0.19%
lat (msec) : 2=1.16%, 4=0.41%, 10=0.01%, 20=0.01%, 50=0.01%
cpu : usr=8.77%, sys=20.44%, ctx=217410, majf=0, minf=32
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=151861,65532,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
Run status group 0 (all jobs):
READ: bw=38.8MiB/s (40.7MB/s), 9562KiB/s-10.1MiB/s (9791kB/s-10.6MB/s), io=2328MiB (2441MB), run=60000-60001msec
WRITE: bw=16.7MiB/s (17.5MB/s), 4110KiB/s-4465KiB/s (4209kB/s-4572kB/s), io=1001MiB (1049MB), run=60000-60001msec
Disk stats (read/write):
nvme0n1: ios=594831/255707, merge=0/0, ticks=134992/39009, in_queue=174001, util=100.00%
```
Разберем результаты выполнения вашего fio-теста о основным метрикам и объясним их значение.
#### Общий результат (Run status group 0):
* Чтение (READ):
* Скорость: 38.8 MiB/s (40.7 MB/s) — средняя скорость чтения всех заданий.
* Диапазон: 9562 KiB/s - 10.1 MiB/s — скорости отдельных потоков.
* Объем данных: 2328 MiB (около 2.44 GB).
* Запись (WRITE):
* Скорость: 16.7 MiB/s (17.5 MB/s).
* Диапазон: 4110 KiB/s - 4465 KiB/s.
* Объем данных: 1001 MiB (около 1.049 GB).
#### Метрики на потоки
Для каждого потока представлены следующие данные:
* Чтение (READ)
* IOPS — Средние значения IOPS (операции в секунду) для потоков составляют, например, 2415 IOPS для первого задания. Это число показывает, сколько операций чтения в секунду выполнялось.
* Latency (задержка) — Время отклика операций ввода-вывода (чтение). Например, среднее время отклика (clat) составляет 298.88 микросекунд, а 99-й процентиль (99.00th) — 1663 микросекунды. Это означает, что 99% операций чтения завершаются за это время.
* Bandwidth (пропускная способность) — Средние скорости чтения: 9.66 MiB/s, 9.57 MiB/s и т.д.
* Запись (WRITE)
* IOPS — Средние значения: около 1033-1117 IOPS. Меньше, чем для чтения из-за меньшего процента операций записи.
* Latency (задержка) — Средняя задержка: около 200-230 микросекунд. Процентиль 99%: до 1483-4490 микросекунд.
* Bandwidth (пропускная способность) — Средняя скорость записи: 4.1-4.4 MiB/s.
#### Сводка задержек (Latency Summary)
* Чтение: 50% операций завершались за ~281 микросекунду (медиана). 99.5% операций — за менее чем 1991 микросекунду.
* Запись:- 50% операций завершались за ~174-186 микросекунд. 99% — менее чем за 1467 микросекунд.
#### Нагрузка на диск (Disk Utilization)
* Число операций: 594831 операций чтения и 255707 операций записи. Диск был загружен на 100% времени, что ожидаемо для высокой глубины очереди (iodepth=32).
#### Общие выводы по M.2 SSD Kingston SNV2S/2000G установленном в мой Orange Pi 5 Plus
Для случайных операций на маленьких блоках (4 КБ) Kingston SNV2S/2000G демонстрирует хорошую производительность (средние 40.7 MB/s чтения и 17.5 MB/s записи). IOPS соответствуют значениям для высокопроизводительных NVMe SSD. Средние задержки в районе 200-300 микросекунд — это нормально для NVMe. Процентиль 99% показывает стабильность: отклонения есть, но они незначительны. Полная загрузка диска (util=100%) свидетельствует о том, что диск эффективно обрабатывает запросы в рамках установленной глубины очереди.
### Чистка
Не забудьте удалить файлы `seq_nvme.*` после завершения теста, чтобы освободить место на диске.
```shell
rm seq_nvme.*
```
-----
## Измерение производительности дисков, RAID-массивов и сравнение скорости работы разных файловых систем (ext4, XFS, btrfs и др.) с помощью `bonnie++`
`bonnie++` — это утилита для тестирования производительности файловой системы, которая позволяет оценить скорость чтения/записи, IOPS (число операций ввода/вывода в секунду), задержки и другие параметры. Она позволяет провести тестирование на различных уровнях нагрузки и с разными параметрами.
Установка bonnie++:
```shell
sudo apt install bonnie++
```
Запуск теста (например):
```shell
bonnie++ -s 8192 -r 4096 -u $(whoami)
```
Где:
* `-s 8192` — размер файла для теста (8 ГБ).
* `-r 4096` — размер блока данных (4 КБ).
* `-u $(whoami)` — имя пользователя, от имени которого запускается тест.
Результаты выполнения вышеуказанной команды для моего NVMe SSD (Kingston SNV2S/2000G) в Orange Pi 5 Plus были следующие:
```text
Version 2.00 ------Sequential Output------ --Sequential Input- --Random-
-Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
Name:Size etc /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP
opi5plus-1 8G 958k 99 1.1g 98 1.8g 93 2919k 99 4.4g 99 +++++ +++
Latency 39397us 155us 92us 4420us 87us 641us
Version 2.00 ------Sequential Create------ --------Random Create--------
opi5plus-1 -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
files /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP
16 +++++ +++ +++++ +++ +++++ +++ +++++ +++ +++++ +++ +++++ +++
Latency 2136us 600us 449us 1023us 241us 65us
```
Разберем результаты выполнения вашего bonnie++-теста и объясним основные метрики:
* `------Sequential Output------` — последовательный вывод (запись)
* `-Per Chr-` — посимвольная запись
* `/sec` — скорость записи в байтах в секунду. Например, 958k = 958 КБ/с.
* `%CP` — процент использования процессора 99%.
* `Latency` — задержка записи в микросекундах. Например, 39397 микросекунд.
* `-Block-` — блочная запись
* `/sec` — скорость записи в байтах в секунду. Например, 1.1g = 1.1 ГБ/с.
* `%CP` — процент использования процессора 98%.
* `Latency` — задержка записи в микросекундах 155 микросекунд.
* `-Rewrite-` — перезапись
* `/sec` — скорость записи в байтах в секунду. Например, 1.8g = 1.8 ГБ/с.
* `%CP` — процент использования процессора 93%.
* `Latency` — задержка записи в микросекундах 92 микросекунд.
* `--Sequential Input-` — последовательный ввод (чтение)
* `-Per Chr-` — посимвольное чтение
* `/sec` — скорость чтения в байтах в секунду. Например, 2919k = 2919 КБ/с.
* `%CP` — процент использования процессора 99%.
* `Latency` — задержка чтения в микросекундах 4420 микросекунд.
* `-Block-` — блочное чтение
* `/sec` — скорость чтения в байтах в секунду. Например, 4.4g = 4.4 ГБ/с.
* `%CP` — процент использования процессора 99%.
* `Latency` — задержка чтения в микросекундах 87 микросекунд.
* `--Random-` — случайный доступ.
* 'Latency' — задержка в микросекундах. Например, 641 микросекунда.
* ...
* ...
* И так далее.
Более подробно о параметрах и метриках bonnie++ можно прочитать в справке `man bonnie++`.

View File

@@ -0,0 +1,395 @@
# Orange Pi 5 Plus: установка (перенос) системы на eMMC или SSD NVMe (M.2)
По правде сказать, приведенный ниже рецепт не только для переноса системы с MicroSD (она же MicroSDHC или MicroSDXC) на
eMMC-носитель или SSD-накопитель NVMe. Он отлично сработает и для установки чистой системы на eMMC или SSD. Причем,
установка чистой системы будет намного проще и быстрее.
Процедура немного напоминает магию, но это результат глубокого заныривания в интернет, и проверено срабатывает и для
SSD NVMe, и для 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
| Фото до и после установки NVMe. Более просто в установке. Для надёжности закрепить винтиком. |
|:---------------------------------------------------------------------------------------------------|
| ![Orange Pi 5 Plus без SSD eMMC (m.2)](../images/orange-pi--photo--without-ssd-nvme.webp) |
| ![Orange Pi 5 Plus с установленным SSD eMMC (m.2)](../images/orange-pi--photo--with-ssd-nvme.webp) |
Кстати, производительность (скорость чтения и записи) eMMC-носителей и SSD-накопителей NVMe может сильно отличаться.
Лично мой выбор — SSD-накопитель NVMe. По опыту он в пять-шесть раз быстрее eMMC.
Вот специальная заметка [как замерить производительность накопителя](measuring-performance-storage-devices.md).
После этого включим Orange Pi 5 Plus. И после того как он загрузится, посмотрим какие устройства и тома есть в системе:
```shell
sudo lsblk
```
Увидим что-то подобное:
```text
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
mtdblock0 31:0 0 16M 0 disk
mmcblk1 179:0 0 59.7G 0 disk
├─mmcblk1p1 179:1 0 1G 0 part /boot
└─mmcblk1p2 179:2 0 58G 0 part /var/log.hdd
/
mmcblk0 179:32 0 233G 0 disk
└─mmcblk0p1 179:33 0 230.6G 0 part
mmcblk0boot0 179:64 0 4M 1 disk
mmcblk0boot1 179:96 0 4M 1 disk
zram0 254:0 0 7.7G 0 disk [SWAP]
zram1 254:1 0 200M 0 disk /var/log
```
Важно обратить внимание на объемы накопителей. В данном случае, у нас есть MicroSD-носитель `mmcblk1` (59.7G — это
64Gb флешка) и eMMC-носитель `mmcblk0` (233G — это 256Gb eMMC). У вас могут быть другие объемы и другие имена
устройств. Так же обратите внимание, ещё у нас есть `mtdblock0` — это внутренняя SPI-флеш, которая
используется для загрузки системы. Если у вас есть SSD-накопитель NVMe, он будет иметь имя вроде `nvme0n1`.
На этом этапе, если мы хотим именно перенести систему, самое время сделать образ нашей MicroSD на внешний
носитель (например, на USB-диск или сетевое хранилище). Смонтируем внешний накопитель, например для сетевого
хранилища c Samba:
```shell
mount -t cifs -o username=NAS_USERNAME,password=SECRET //xxx.xxx.xxx.xxx/путь-к-месту-для-сохранения-образа /media/backup/
```
Где:
- `NAS_USERNAME` — имя пользователя для доступа к сетевому хранилищу;
- `SECRET` — пароль для доступа к сетевому хранилищу;
- `xxx.xxx.xxx.xxx` — IP-адрес сетевого хранилища;
- `путь-к-каталогу-для-сохраненияь-обраа` — путь к каталогу на сетевом хранилище, куда будет сохранен образ;
- `/media/backup/` — точка монтирования сетевого хранилища.
Сделаем образ MicroSD в файл `flash-disk.img` на этом внешнем накопителе:
```shell
sudo dd if=/dev/mmcblk1 of=/media/backup/flash-disk.img status=progress
```
Это займет некоторое время (и иногда, в зависимости от скорости внешнего накопителя и размера MicroSD, довольно
продолжительное). После того как образ будет готов, установим в систему `gdisk` — утилиту для работы
с таблицами разделов:
```shell
sudo apt install gdisk
```
## Очистим разделы на SPI-флеш (внутренней флеш-памяти с загрузчиками)
Запустим `gdisk` для работы с заделами на SPI `mtdblock0` (загрузчиками):
```shell
sudo gdisk /dev/mtdblock0
```
Увидим что-то подобное:
```text
GPT fdisk (gdisk) version 1.0.8
Partition table scan:
MBR: protective
BSD: not present
APM: not present
GPT: present
Found valid GPT with protective MBR; using GPT.
Command (? for help):
```
Если введем `?` и нажмем Enter, увидим список команд:
```text
b back up GPT data to a file
c change a partition's name
d delete a partition
i show detailed information on a partition
l list known partition types
n add a new partition
o create a new empty GUID partition table (GPT)
p print the partition table
q quit without saving changes
r recovery and transformation options (experts only)
s sort partitions
t change a partition's type code
v verify disk
w write table to disk and exit
x extra functionality (experts only)
? print this menu
```
Выполним команду `p` и Enter, чтобы увидеть список разделов:
```text
Disk /dev/mtdblock0: 32768 sectors, 16.0 MiB
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 8158
Partitions will be aligned on 64-sector boundaries
Total free space is 1021 sectors (510.5 KiB)
Number Start (sector) End (sector) Size Code Name
1 64 1023 480.0 KiB 8300 idbloader
2 1024 7167 3.0 MiB 8300 uboot
Command (? for help):
```
Как видим, у нас есть два раздела: `idbloader` и `uboot`. Нам нужно удалить их. Для этого выполним команду `d` и Enter.
Увидим:
```text
Partition number (1-2):
```
Введем номер раздела `1` и Enter. Раздел будет удален. Повторим для раздела `2`. Снова выполним команду `d` и Enter.
Теперь нас не спросят номер раздела, оставшийся раздел будет удален без лишних вопросов. Если у вас, вдруг, было
больше двух разделов, надо последовательно удалить их все.
Теперь нам нужно сохранить изменения. Для этого выполним команду `w` и Enter. Увидим:
```text
Warning! Secondary header is placed too early on the disk! Do you want to
correct this problem? (Y/N):
```
Подтверждаем наше намерения перезаписать таблицу разделов. Вводим введя `y` и Enter. Увидим:
```text
Have moved second header and partition table to correct location.
Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!
Do you want to proceed? (Y/N):
```
Еще раз подтверждаем наше намерение перезаписать таблицу разделов. Вводим `y` и Enter. Увидим:
```text
OK; writing new GUID partition table (GPT) to /dev/mtdblock0.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot or after you
run partprobe(8) or kpartx(8)
The operation has completed successfully.
```
## Очистим разделы на целевом eMMC (или SSD NVMe)
Теперь нам нужно очистить разделы на целевом накопителе. Для этого запустим `gdisk` для работы с разделами на eMMC
(в нашем случае это `mmcblk0`):
```shell
sudo gdisk /dev/mmcblk0
```
Проделаем те же операции, что и с SPI-флешем. Не буду повторяться, так как процедура аналогична. Важно помнить, что
нам нужно удалить все(!) разделы.
## Выравняем разделы на eMMC (или SSD NVMe)
Выравнивание секторов eMMC гарантирует правильное распознавание загрузочного диска. Снова запустим `gdisk` для нашего
eMMC:
```shell
sudo gdisk /dev/mmcblk0
```
Дадим команду `p` и Enter, чтобы, чтобы увидеть список разделов, и обратим внимание, на текст над таблицей разделов:
```text
Disk /dev/mmcblk0: 488570880 sectors, 233.0 GiB
Sector size (logical/physical): 512/512 bytes
Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 2048, last usable sector is 488570846
Partitions will be aligned on 2048-sector boundaries
Total free space is 4974559 sectors (2.4 GiB)
```
В данном случае все нормально: как видим выше, основная таблицы разделов начинается с сектора 2 и заканчивается на 33,
а первый используемый сектор — это любое число, кроме 34 (в нашем случае 2048). Можно пропустить следующие шаги. Но
если у вас нет так, то необходимо переформатировать сектора перед записью новой таблицы разделов на диск.
Для этого выполним следующие шаги:
Вводим команду `x` и Enter, чтобы перейти в экспертный режими. В режиме доступны следующие команды:
```text
a set attributes
b byte-swap a partition's name
c change partition GUID
d display the sector alignment value
e relocate backup data structures to the end of the disk
f randomize disk and partition unique GUIDs
g change disk GUID
h recompute CHS values in protective/hybrid MBR
i show detailed information on a partition
j move the main partition table
l set the sector alignment value
m return to main menu
n create a new protective MBR
o print protective MBR data
p print the partition table
q quit without saving changes
r recovery and transformation options (experts only)
s resize partition table
t transpose two partition table entries
u replicate partition table on new device
v verify disk
w write table to disk and exit
z zap (destroy) GPT data structures and exit
? print this menu
```
Переместите основную таблицу разделов. Для этого введите `j` и Enter. Будет предложено задать сектор для расположения
начала основной таблицы разделов:
```text
Currently, main partition table begins at sector 2 and ends at sector 33
Enter new starting location (2 to 61408; default is 2; 1 to abort):
```
Вводим `2` и Enter. Затем сохраняем изменения, выполнив команду `w` и Enter. И пройдя два подтверждения (`y` и Enter)
выходим из `gdisk`.
## Перезаписываем загрузчик
Несколькими попытками проверено, что только такой порядок действий (обновление загрузчика) гарантирует, что
Orange Pi 5 будет загружаться с eMMC. Надо запустить встроенное приложение конфигурации Orange Pi 5:
```shell
sudo orangepi-config
```
Панель orangepi-config на Orange Pi 5 выглядит так:
![Панель orangepi-config на Orange Pi 5 выглядит так](../images/orange--orange-config.gif)
Выбираем пункт '**System: System and security settings**' и заходим в панель '**System Settings**'. Выбираем в ней
пункт '**Install: Install to/update boot loader**':
![В orangepi-config заходим в панель 'System Settings' и выбираем 'Install: Install to/update boot loader](../images/orange--orange-config--system-setting--install-update-boot-loader.png)
Выбираем последний пункт '**Install/Update the bootloader on SPI Flash**':
![Выбираем пункт 'Install/Update the bootloader on SPI Flash'](../images/orange--orange-config--system-setting--install-update-boot-loader--install-update-bootloader-spi-slash.png)
Подтверждаем наше намерение обновить загрузчик:
![orange-pi--003.png](../images/orange--orange-config--system-setting--install-update-boot-loader--warning.png)
Спустя несколько минут мы снова увидим панель '**System Settings**' приложения `orangepi-config`. На этом этапе
можно обновить пакеты системы, выбрав пункт '**Firmware: Run apt update & apt upgrade**'. Но это не обязательно,
можно просто выйти из `orangepi-config`.
Перезагружаем наш Orange Pi 5. Все еще не извлекая MicroSD:
```shell
sudo reboot
```
| Важно! |
|:-------|
| Возможно Orange Pi не загрузится. Просто извлеките MicroSD, перезапишите не неё образ системы (лучше чистой, [с официального сайта производителя](http://www.orangepi.org/html/serviceAndSupport/index.html)), загрузитесь снова и проделайте все вышеперечисленное ещё раз. |
## Записываем образ чистой системы на eMMC
Самый простой, быстрый и проверенный способ — записать на eMMC образ чистой системы, скаченный с официального сайта.
К слову сказать, образы официальных сборок для Orannge Pi лежать на Goolge Drive, так что самое оптимальное скачать
образ на каком-нибудь другом компьютере и перенести его на Orange Pi с помощью USB-накопителя или NAS.
Записываем файл с образом на eMMC:
```shell
sudo dd bs=1M if=Orangepi5plus_1.0.8_ubuntu_jammy_server_linux6.1.43.img of=/dev/mmcblk0 status=progress
```
Все. Можно выключить Orange Pi 5 Plus:
```shell
sudo shutdown 0
```
Извлекаем MicroSD и включаем Orange Pi 5. Он должен загрузиться с eMMC.
## Перенос системы с MicroSD (или с резервной копии) на eMMC
Если установка чистой системы на eMMC не подходит (наприер, если на MicroSD уже настроена и отлажена система), то
можно перенести систему с MicroSD на eMMC. Правда это не сработает, если размер eMMC меньше размера MicroSD (1), а
если сработает (размер eMMC больше размера MicroSD), то на eMMC, после копирования, будут созданы тома и разделы
ровно такого же размера, как на MicroSD (2).
Для копирования раздела с MicroSD на eMMC:
```shell
sudo dd bs=1M if=/dev/mmcblk1 of=/dev/mmcblk0 status=progress
```
а для восстановления из резервной копии (не забываем смонтировать внешний накопитель с резервными копиями) что-то типа:
```shell
sudo dd if=/dev/mmcblk1 of=/media/backup/flash-disk.img status=progress
```
Это займет продолжительное время. Если мы делали восстановление из резервной копии, то можно будет сразу извлечь
MicroSD и перезагрузиться. Но если мы копировали с MicroSD на eMMC, то нам надо будет еще расширить раздел на eMMC
на весь объем накопителя, и расширить файловую систему. Так как она еще не смонтирована, то сделать это не сложно.
Установим в систему пакет утилит `cloud-guest-utils`. Из ее состава нас интересует утилита `growpart`
для автоматического увеличения размеров разделов на диске.
```shell
sudo apt install cloud-guest-utils
```
Увеличиваем второй раздел на eMMC (на первом у нас загрузчик):
```shell
sudo growpart /dev/mmcblk0 2
```
Увидим что-то подобное:
```text
CHANGED: partition=2 start=2158592 old: size=121737216 end=123895808 new: size=486412255 end=488570847
```
Теперь нужно "растянуть" файловую систему на этом разделе. Но для начала проведем проверки файловой системы раздела:
```shell
sudo e2fsck -f /dev/mmcblk0p2
```
Увидим что-то типа:
```text
e2fsck 1.46.5 (30-Dec-2021)
opi_root: recovering journal
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
Free blocks count wrong (13505398, counted=13505373).
Fix<y>? yes
Free inodes count wrong (3730719, counted=3730708).
Fix<y>? yes
opi_root: ***** FILE SYSTEM WAS MODIFIED *****
opi_root: 48812/3779520 files (0.2% non-contiguous), 1711779/15217152 blocks
```
Как видим, нам показали две ошибки и предложили их исправить, и мы согласились (`y`). Теперь, после проверок
и исправлений, "растянем" файловую систему:
```shell
sudo resize2fs /dev/mmcblk0p2
```
Увидим примерно следующее:
```text
resize2fs 1.46.5 (30-Dec-2021)
Resizing the filesystem on /dev/mmcblk0p2 to 60801531 (4k) blocks.
The filesystem on /dev/mmcblk0p2 is now 60801531 (4k) blocks long.
```
Все. Раздел на eMMC увеличен на весь объем накопителя, и файловая система на нем тоже "растянута". Теперь можно
выключить Orange Pi:
```shell
sudo shutdown 0
```
Извлекаем MicroSD и включаем Orange Pi снова. Он должен загрузиться уже с eMMC.
## PS
В составлении этой заметки большую помощь оказала инструкция [Kaveh Kaviani](
https://github.com/kaveh-kaviani/Tutorials/blob/main/content/sbc/orange-pi/orange-pi-5/boot-linux-from-emmc/README.md).
Большое спасибо ему.

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
```