Compare commits

...

67 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
21 changed files with 5434 additions and 69 deletions

View File

@@ -11,15 +11,23 @@
## Kubernetes (k3s/k8s) ## Kubernetes (k3s/k8s)
* [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md) * [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md)
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) (k3s) * [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) в k3s
* [Под с 3X-UI](kubernetes/k3s-3xui-pod.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) * [Проксирование внешнего хоста через 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
* [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md) * [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
* [Python-скрипт как служба Linux](python/python_as_service.md) * [Python-скрипт как служба Linux](python/python_as_service.md)
## Orange Pi / Raspberry Pi ## Linux (возможно в специфике Orange Pi / Raspberry Pi)
* [Установка (перенос) системы на NVMe или eMMC (для Orange Pi 5 Plus)](raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md) * [Установка (перенос) системы на 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) * [Измерение производительности накопителей](raspberry-and-orange-pi/measuring-performance-storage-devices.md)
* [Установка Docker и Docker Compose](raspberry-and-orange-pi/install-docker-compose.md) * [Установка Docker и Docker Compose](raspberry-and-orange-pi/install-docker-compose.md)
@@ -27,7 +35,8 @@
* [k8s (кubernetes) на Orange Pi (драфт...)](raspberry-and-orange-pi/k8s.md) * [k8s (кubernetes) на Orange Pi (драфт...)](raspberry-and-orange-pi/k8s.md)
* [k3s (кubernetes) на Orange Pi](raspberry-and-orange-pi/k3s.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) * [Перекомпиляция ядра 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) * [Настройка nginx как прямого прокси](nginx/nginx_as_direct_proxy.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

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

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

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

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

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

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

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

1251
kubernetes/k3s-postresql.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +1,358 @@
# Проксирование внешнего хоста через Traefik (Ingress-контроллер) # Проксирование внешнего хоста через Traefik (Ingress-контроллер)
У меня в домашней сети есть хост с сервисом (audiobookshelf), то удобно его прокинуть через Traefik, чтобы не открывать У меня в домашней сети есть хост с web-сервисом (audiobookshelf), а стандартные web-порты (80 для HTTP и 443 для HTTPS)
на внешний IP лишних адресов, портов, использовать домен и включить SSL, и управлять всем этим через из единой на домашнем роутере перенаправлены в кластер k3S (через keepalived). Таким образом, если прокинуть http-трафик этого
точки. хоста через Traefik, то можно будет получить доступ к этому сервису через доменное имя и SSL-сертификат от
Lets Encrypt.
Для удобства я поместил все манифесты в один файл (вы можете оформить из и как отдельные файлы). Так как хотелось бы
описать как делаются универсальные манифесты, которые можно использовать для проксирования любого сервиса, то
я заменил в нем конкретные значения на "заглушки. Можно взять этот манифест и просто заменить в нем значения на
свои:
В моем случае:
* `<PROXIED-HOST>` -- IP-адрес хоста, где работает сервис, который надо проксировать. * `<PROXIED-HOST>` -- IP-адрес хоста, где работает сервис, который надо проксировать.
* `<PROXIED-PORT>` -- порт, с которого отвечает сервис. * `<PROXIED-PORT>` -- порт, с которого отвечает сервис.
* `<YOU-DOMAIN-NAME>` -- доменное имя, на которое будет проксировать сервис. * `<YOU-DOMAIN-NAME>` -- доменное имя, на которое будет проксировать сервис.
* `<NAME-SPACE>` -- пространство имен кластера, в котором будет создан сервис, маршруты, секреты и все необходимое
для проксирования. Пространство имен -- это логическая группа ресурсов в кластере Kubernetes, которая позволяет
организовать и изолировать ресурсы.
* `<SERVICE-NAME>` -- имя сервиса, который будет проксироваться. Это имя, для простоты, будем использоваться
* и в маршрутах, и сертификатах, и в секрете...
## Пространство имен ## Пространство имен
Чтобы все было аккуратно и серисы и поды не путались, создадим пространство имен для проксируемого сервиса. Чтобы все было аккуратно и сервисы и поды не путались, создадим пространство имен для проксирования конкретного хоста.
Например, `ab-shelf`. Мо хост относится к <SERVICE-NAME>, поэтому назову пространство имен `<NAME-SPACE>`
Например, `<NAME-SPACE>`.
```bash ```bash
sudo ubectl create namespace ab-shelf sudo ubectl create namespace <NAME-SPACE>
``` ```
Проверяем, что пространство создано: Проверяем, что пространство создано:
```bash ```bash
sudo kubectl get namespace ab-shelf sudo kubectl get namespace <NAME-SPACE>
``` ```
Увидим, что пространство создано и активно: Увидим, что пространство создано и активно:
```text ```text
NAME STATUS AGE NAME STATUS AGE
ab-shelf Active 54s <NAME-SPACE> Active 54s
``` ```
## Конфигурация IngressRoute, Service и Endpoints ## Конфигурация всего
Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифетст: Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифест:
```bash ```bash
sudo nano ~/k3s/audiobookshelf/audiobookshelf.yaml sudo nano ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
``` ```
И вставляем в него следующее содержимое (не забудь заменить `<PROXIED-HOST>`, `<PROXIED-PORT>` и `<YOU-DOMAIN-NAME>` Он состоит из следующих частей:
на свои значения... пространство имен `ab-shelf` и имя сервиса `audiobookshelf` можно тоже поменять на свое, если * `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 ```yaml
# Endpoints для внешнего хоста # Endpoints для внешнего хоста <SERVICE-NAME>
# Задаёт IP и порт внешнего сервера, так как <SERVICE-NAME> внешний хост для k3s
apiVersion: v1 apiVersion: v1
kind: Endpoints kind: Endpoints
metadata: metadata:
name: audiobookshelf name: <SERVICE-NAME>
namespace: ab-shelf # пространство, например namespace: <NAME-SPACE> # Namespace для <SERVICE-NAME>
subsets: subsets: # Прямо в корне, без spec
- addresses: - addresses:
- ip: <PROXIED-HOST> - ip: <PROXIED-HOST> # IP Synology, где работает <SERVICE-NAME>
ports: ports:
- port: <PROXIED-PORT> - port: <PROXIED-PORT> # Порт Synology (HTTP)
protocol: TCP protocol: TCP
---
# Service для проксируемого хоста (<PROXIED-HOST>:<PROXIED-PORT>) ---
# Service для маршрутизации трафика от Traefik к внешнему хосту
# Связывает IngressRoute с Endpoints
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: audiobookshelf name: <SERVICE-NAME>
namespace: ab-shelf # пространство, например namespace: <NAME-SPACE>
spec: spec:
ports: ports:
- port: <PROXIED-PORT> - port: <PROXIED-PORT> # Порт сервиса, на который Traefik отправляет трафик
targetPort: <PROXIED-PORT> targetPort: <PROXIED-PORT> # Порт на Synology
protocol: TCP protocol: TCP
---
# IngressRoute ---
apiVersion: traefik.containo.us/v1alpha1 # 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 kind: IngressRoute
metadata: metadata:
name: audiobookshelf name: <SERVICE-NAME>-http
namespace: ab-shelf # пространство, например namespace: <NAME-SPACE>
spec: spec:
entryPoints: entryPoints:
- web-custom # ендпоинт, который "слушает" порт 2055 - 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: routes:
- match: Host("<YOU-DOMAIN-NAME>") - match: Host("<YOU-DOMAIN-NAME>")
kind: Rule kind: Rule
services: services:
- name: audiobookshelf - name: <SERVICE-NAME>
port: <PROXIED-PORT> 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. Это нестандартный
* `Endpoints` -- указываем, что сервис будет проксировать запросы на внешний хост `<PROXIED-HOST>` ClusterIssuer cert-manager, создание которого описано в [документации](https://cert-manager.io/docs/usage/ingress/#tls-termination)
и порт `<PROXIED-PORT>`. Ендпоинт -- это конечная точка, к которой будет проксироваться запрос. В данном случае и [отдельной инструкции](k3s-custom-container-deployment.md#создание-clusterissuer)
это IP-адрес и порт, но обычно это имя пода, на который отправляются и который отвечает на запросы. (возможно, вам нужно будет создать его отдельно). Если вы используете другой ClusterIssuer, то замените letsencrypt-prod
* `Service` -- создаем сервис, который будет использоваться для проксирования запросов к `Endpoints`. Сервис -- это на имя вашего ClusterIssuer в секции `issuerRef` в манифесте.
абстракция, которая позволяет упрощать доступ к подам. Он может использоваться для балансировки нагрузки между
несколькими потоками, которые обрабатывают запросы. В данном случае мы создаем сервис, который будет проксировать ## Применяем манифест
запросы к `Endpoints` (внешнему хосту). ```bash
* `IngressRoute` -- создаем маршрут, который будет проксировать запросы на домен `<YOU-DOMAIN-NAME>` sudo kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
к сервису `audiobookshelf` в пространстве `ab-shelf`. Маршрут -- это правило, которое определяет, как обрабатывать ```
запросы, поступающие на определенный адрес. В данном случае мы создаем маршрут, который будет направлять запросы
на домен `<YOU-DOMAIN-NAME>` к сервису `audiobookshelf` в пространстве `ab-shelf`. Внутри маршрута мы указываем, Проверяем, что все ресурсы создались:
что запросы должны обрабатываться сервисом `audiobookshelf` на порту `<PROXIED-PORT>`. ```bash
В данном случае мы используем `web-custom` как точку входа, которая будет слушать порт 2055. 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,362 @@
# Кастомная страница ошибки 404 (и других) в Traefik
Страницы ошибок Traefik по умолчанию выглядят скучно. Это даже не страницы, а просто текстовые сообщения. Например,
404 выглядит как `404 page not found`. Это позволяет Traefik быть лёгким и быстрым.
Если хочется сделать страницы статусоы 4xx и 5xx более привлекательными, то кастомизация страниц ошибок — отличная идея!
Для каждого HTTP-сервиса внутри можно сделать свои страницы ошибок на уровне приложения (как например, на Gitea,
на которой ты сейчас сидишь). И это наиболее правильный способ. Но если http-запрос не привязан ни к какому сервису,
и Traefik не знает куда его отправить, то он выдаёт свои страницы ошибок. Например, при обращении по IP.
Traefik позволяет кастомизировать страницы ошибок через **middleware** типа `errors`, который перенаправляет запросы
с определёнными кодами ошибок (например, 404) на кастомный сервис, возвращающий нужную html-страницу. И все это
излишество в `k3s` нужно настраивать глобально, с помощью `Middleware` и `IngressRoute`, и применять ко всем маршрутам.
Чтобы кастомная страница 404 работала для всех запросов в кластере, нужно:
# Создать сервис, который возвращает кастомную страницу (например, контейнер с Nginx или простой HTTP-сервер).
# Настроить middleware `errors` для перехвата ошибок 404.
# Применить middleware глобально через `IngressRoute` или конфигурацию Traefik.
Самый простой подход — развернуть лёгкий контейнер (например, Nginx) с HTML-файлом для страницы 404 и настроить
Traefik для перенаправления ошибок на этот контейнер. Ну или, как альтернатива, использовать внешний сервис (по URL),
но это сложнее для глобальной настройки, и создаст зависимость от этого URL.
#### 2. План действий
- Создать кастомную страницу 404 (HTML-файл).
- Развернуть контейнер с Nginx, который будет отдавать эту страницу.
- Настроить Traefik middleware `errors` для перехвата 404.
- Применить middleware глобально для всех маршрутов в `k3s`.
- Проверить результат.
---
### Настройка кастомной страницы 404
#### 1. Создать кастомную страницу 404
- Создай HTML-файл для страницы 404 на ноде `opi5`:
```bash
mkdir -p ~/k3s/error-pages
cat > ~/k3s/error-pages/404.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { color: #ff5555; }
p { font-size: 18px; }
</style>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
</body>
</html>
EOF
```
#### 2. Развернуть Nginx для отдачи страницы
- Создай манифест для Nginx, который будет отдавать `404.html`:
```bash
cat > ~/k3s/error-pages/error-pages.yaml <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: error-pages
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: error-pages
namespace: error-pages
spec:
replicas: 1
selector:
matchLabels:
app: error-pages
template:
metadata:
labels:
app: error-pages
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: error-pages
mountPath: /usr/share/nginx/html
volumes:
- name: error-pages
configMap:
name: error-pages
---
apiVersion: v1
kind: Service
metadata:
name: error-pages
namespace: error-pages
spec:
selector:
app: error-pages
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: error-pages
namespace: error-pages
data:
404.html: |
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
h1 { color: #ff5555; }
p { font-size: 18px; }
</style>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
</body>
</html>
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
```
- Проверь поды и сервис:
```bash
kubectl get pod -n error-pages
kubectl get svc -n error-pages
```
#### 3. Настроить Traefik middleware для ошибок
- Создай манифест для middleware `errors`:
```bash
cat > ~/k3s/traefik/error-middleware.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: error-pages
namespace: kube-system
spec:
errors:
status:
- "404"
service:
name: error-pages
namespace: error-pages
port: 80
query: /404.html
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
```
- Проверь:
```bash
kubectl get middleware -n kube-system
```
#### 4. Применить middleware глобально
- В `k3s` дефолтный Traefik обрабатывает маршруты через `IngressRoute` или `Ingress`. Чтобы middleware применялся ко всем маршрутам, нужно либо:
- Добавить middleware к каждому `IngressRoute` вручную.
- Настроить Traefik для глобального применения middleware через `defaultMiddlewares`.
- Для простоты создадим глобальный `IngressRoute` для всех доменов:
```bash
cat > ~/k3s/traefik/global-error-route.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: global-error-route
namespace: kube-system
spec:
entryPoints:
- web
- websecure
routes:
- match: HostRegexp(`{host:.+}`)
kind: Rule
services:
- name: noop
namespace: kube-system
port: 9999
middlewares:
- name: error-pages
namespace: kube-system
EOF
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
- **Примечание**:
- Сервис `noop:9999` — это заглушка, так как `IngressRoute` требует сервис, но middleware `errors` перехватит 404 до обращения к сервису.
- Это обеспечивает, что любой запрос с кодом 404 (для любого домена) будет перенаправлен на `error-pages`.
#### 5. Проверить кастомную страницу 404
- Попробуй открыть несуществующий путь:
```bash
curl -v https://git.cube2.ru/nonexistent
```
- Ожидаемый ответ:
- Код: `404 Not Found`.
- HTML-страница:
```html
<h1>404 - Page Not Found</h1>
<p>Oops! Looks like you're lost in the void.</p>
```
- Проверь для другого домена (например, `Bitwarden`):
```bash
curl -v https://<bitwarden-domain>/nonexistent
```
- Проверь логи Traefik:
```bash
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik | tail -n 20
```
#### 6. (Опционально) Настроить другие ошибки
- Чтобы добавить кастомные страницы для других кодов (например, 403, 500), обнови middleware:
```bash
cat > ~/k3s/traefik/error-middleware.yaml <<EOF
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: error-pages
namespace: kube-system
spec:
errors:
status:
- "403"
- "404"
- "500-503"
service:
name: error-pages
namespace: error-pages
port: 80
query: /{status}.html
EOF
```
- Создай дополнительные файлы (`403.html`, `500.html`) в `ConfigMap`:
```bash
kubectl edit configmap -n error-pages error-pages
```
- Добавь, например:
```yaml
403.html: |
<!DOCTYPE html>
<html>
<head><title>403 Forbidden</title></head>
<body><h1>403 - Forbidden</h1><p>Access denied!</p></body>
</html>
```
- Примени:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
kubectl delete pod -n error-pages -l app=error-pages
```
---
### Ответ на твой вопрос
> По умолчанию Traefik выдаёт скучные текстовые страницы для 404 и других ошибок. Как сделать кастомные 404 для всего `k3s` без привязки к домену? Где они лежат, и есть ли простой способ их переопределить?
- **Где лежат дефолтные страницы**:
- Они встроены в бинарник Traefik и генерируются как текст (не HTML), например: `404 page not found`.
- Физически их нет в виде файлов в кластере.
- **Почему не HTML**:
- Traefik использует текстовые ответы для минимизации ресурсов.
- **Как переопределить**:
- Использовать middleware `errors`, который перенаправляет ошибки (например, 404) на кастомный сервис с HTML-страницей.
- Развернуть контейнер (например, Nginx) с кастомной страницей.
- Настроить глобальный `IngressRoute` для применения middleware ко всем доменам.
- **Простой способ**:
1. Создать `ConfigMap` с HTML-файлом (`404.html`).
2. Развернуть Nginx в namespace `error-pages` для отдачи страницы.
3. Настроить middleware `errors` для перехвата 404.
4. Применить middleware через глобальный `IngressRoute`.
- **Для всего `k3s`**:
- Глобальный `IngressRoute` с `HostRegexp` перехватывает все запросы и применяет middleware `errors` для ошибок 404.
---
### Рекомендации
1. Создать и применить страницу 404:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
```
2. Настроить middleware:
```bash
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
```
3. Применить глобальный маршрут:
```bash
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
4. Проверить:
```bash
curl -v https://git.cube2.ru/nonexistent
curl -v https://<bitwarden-domain>/nonexistent
```
5. Проверить логи:
```bash
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik | tail -n 20
```
6. (Опционально) Добавить другие ошибки:
- Обновить `ConfigMap` и middleware для 403, 500 и т.д.
---
### Итог
Дефолтные страницы ошибок Traefik — это встроенные текстовые ответы, которые можно переопределить с помощью middleware `errors` и кастомного сервиса (например, Nginx с HTML-страницей). Для глобальной настройки в `k3s` мы развернули контейнер с `404.html`, настроили middleware для перехвата ошибок 404, и применили его ко всем доменам через `IngressRoute` с `HostRegexp`. Это простой и универсальный способ сделать страницы ошибок яркими и весёлыми! 😄 Теперь твои 404 будут выглядеть стильно, и ты можешь добавить такие же для других ошибок.
**Действия**:
1. Применить:
```bash
kubectl apply -f ~/k3s/error-pages/error-pages.yaml
kubectl apply -f ~/k3s/traefik/error-middleware.yaml
kubectl apply -f ~/k3s/traefik/global-error-route.yaml
```
2. Проверить:
```bash
curl -v https://git.cube2.ru/nonexistent
```
**Напиши**:
1. Получилась ли кастомная страница 404? (`curl -v https://git.cube2.ru/nonexistent`)
2. Работает ли для других доменов? (`curl -v https://<bitwarden-domain>/nonexistent`)
3. Хочешь настроить страницы для других ошибок (403, 500)?
Теперь можно расслабиться и наслаждаться яркими страницами ошибок! 🚀

208
kubernetes/k3s_smokeping.md Normal file
View File

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

View File

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

View File

@@ -41,7 +41,7 @@ sudo /usr/local/bin/k3s-agent-uninstall.sh # На воркере
Некоторые требования к узлам: Некоторые требования к узлам:
* На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04). * На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04).
* Статические IP-адреса узлов (или зрезервированные под MAC-адреса IP в DHCP). * Статические IP-адреса узлов (или зарезервированные под MAC-адреса IP в DHCP).
* На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet). * На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet).
@@ -75,9 +75,59 @@ sudo service k3s status
``` ```
Посмотрим сколько нод в кластере: ## Перемещение конфигурации kubectl
При выполнении команд `kubectl` используется файл конфигурации из переменной `KUBECONFIG`. При установке k3s создает
его в `/etc/rancher/k3s/k3s.yaml` и у него права `600` (только для root). Чтобы использовать `kubectl` от имени
пользователя хоста, нужно скопировать файл конфигурации в домашнюю директорию и изменить права доступа.
```bash ```bash
sudo kubectl get nodes mkdir -p ~/.kube/config
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
```
Дадим права:
```bash
sudo chown opi:opi ~/.kube
chmod 755 ~/.kube
sudo chown opi:opi ~/.kube/config
chmod 755 ~/.kube/config
sudo chown opi:opi ~/.kube/config/k3s.yaml
chmod 755 ~/.kube/config/k3s.yaml
```
Проверим:
```bash
ls -ld ~/.kube ~/.kube/config ~/.kube/config/k3s.yaml
```
Должны увидеть что-то вроде:
```text
drwxr-xr-x 4 opi opi 4096 Apr 26 11:32 /home/opi/.kube
drwxr-xr-x 2 opi opi 4096 Apr 26 11:09 /home/opi/.kube/config
-rwxr-xr-x 1 opi opi 2961 Apr 27 16:31 /home/opi/.kube/config/k3s.yaml
```
Установить переменную KUBECONFIG:
```bash
export KUBECONFIG=~/.kube/config/k3s.yaml
```
И добавь её в `~/.bashrc` чтобы не вводить каждый раз после входа в систему или перезагрузки:
```bash
echo 'export KUBECONFIG=~/.kube/config/k3s.yaml' >> ~/.bashrc
```
Проверим, что переменная установлена:
```bash
echo $KUBECONFIG
```
## Проверим установку k3s
Посмотрим сколько нод в кластере (теперь можно не использовать `sudo`):
```bash
kubectl get nodes
``` ```
И, та-да! Увидим одну ноду: И, та-да! Увидим одну ноду:
@@ -91,7 +141,7 @@ opi5plus-2 Ready control-plane,etcd,master 31m v1.31.5+k3s1
А что там внутри? Посмотрим на поды: А что там внутри? Посмотрим на поды:
```bash ```bash
sudo kubectl get pods -A kubectl get pod -A
``` ```
Целых семь подов (минималистичная установка k3s): Целых семь подов (минималистичная установка k3s):
@@ -106,9 +156,17 @@ kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 12m kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 12m
``` ```
Тут статус X/Y в выводе kubectl get pods показывает: * READY `X/Y` в выводе `kubectl get pod` показывает:
* Y — сколько контейнеров должно быть в поде (по спецификации). * Y — сколько контейнеров должно быть в поде (по спецификации).
* X — сколько из них сейчас работает (running). * X — сколько из них сейчас работает (running).
* STATUS показывает состояние пода. Основные статусы которые могут быть:
* `Running` — под работает и все контейнеры в нем запущены.
* `Completed` — под завершил свою работу (например, Job или CronJob).
* `Error` — под завершился с ошибкой.
* `ContainerCreating` — под в процессе создания, контейнеры в нем еще не запущены.
* `CrashLoopBackOff` — под перезапускается из-за ошибки.
* `ImagePullBackOff` — не удалось загрузить образ контейнера.
* `Pending` — под ожидает ресурсов (например, памяти или CPU).
Представлены следующие поды: Представлены следующие поды:
1. `coredns` — это DNS-сервер для кластера. Он отвечает за разрешение имен внутри Kubernetes (например, чтобы поды 1. `coredns` — это DNS-сервер для кластера. Он отвечает за разрешение имен внутри Kubernetes (например, чтобы поды
@@ -141,9 +199,9 @@ kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0
Структура имени — `<имя-приложения>-<хеш-ревизии>-<случайный-суффикс>`. Впрочем, `<хеш-ревизии>` может отсутствовать, Структура имени — `<имя-приложения>-<хеш-ревизии>-<случайный-суффикс>`. Впрочем, `<хеш-ревизии>` может отсутствовать,
если под не имеет контроллера репликации (например, Job или CronJob). если под не имеет контроллера репликации (например, Job или CronJob).
Можно проверить, что API нашего узла (кластера) отвечает: Можно проверить, что API нашего узла (кластера) отвечает на порту `6443` (порт по умолчанию):
```bash ```bash
curl -k https://192.168.1.27 curl -k https://192.168.1.27:6443
``` ```
Здесь ключ `-k` означает, что мы не проверяем сертификаты (нам важно только, что сервер отвечает). Должны получить Здесь ключ `-k` означает, что мы не проверяем сертификаты (нам важно только, что сервер отвечает). Должны получить
@@ -160,8 +218,98 @@ Unauthorized JSON-ответ от API. Что-то вроде:
} }
``` ```
## Подключение второго узла (мастер) ВАЖНО: Надо проверить версию Traefik, которая установилась. Например версия 3.3.2 отличается от 3.3.6 значением
`apiVersion` в которые надо указывать в манифесте:
* `traefik.io/v1alpha1` -- для Traefik v3.3.6
* `traefik.containo.us/v1alpha1` -- для Traefik v3.3.2
Проверить версию можно так (подставьте вместо `<хеш-ревизии>-<случайный-суффикс>` свой, из вывода `kubectl get pod`):
```bash
kubectl get pod -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -o jsonpath='{.spec.containers[0].image}'
```
Увидим что-то вроде:
```text
rancher/mirrkubectl exec -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -- traefik versionik-67bfb46dcb-prvjd -- traefik version
Version: 3.3.2
Codename: saintnectaire
Go version: go1.23.4
Built: 2025-01-14T15:52:27Z
OS/Arch: linux/arm64
```
## Установка блочного хранилища (PVC -- Persistent Volume Claim) Longhorn
Longhorn -- это блочное хранилище k3s, которое позволяет создавать и управлять блочными томами в кластере
для обеспечения высокой доступности и отказоустойчивости. Если узел, на котором находится том, выходит из строя,
Longhorn автоматически перемещает том на другой узел и контейнер продолжает работу с томом, как будто ничего
не произошло (с некоторой задержкой, конечно).
Если установить Longhorn сразу, то при добавлении новых узлов в кластер Longhorn автоматически будет устанавливаться
на них. Но если вы хотите установить Longhorn позже, то нужно будет вручную установить его на новых узлах.
| **ВАЖНО** |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Longhorn требует наличия iSCSI на узлах. Убедитесь, что молуль iSCSI включен или собран в ядро Linux на вашем хосте. Для Orange Pi 5 Plus (Ubuntu на ядре 6.1.43-rockchip-rk3588) [потребует перекомпиляцию ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md), на Orange Pi 5 (Ubuntu ядро 5.10.160-rockchip-rk3588) iSCSI разрешен к подключению как модуль. |
Установим Longhorn на первый узел (мастер):
```bash
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
```
Проверим, что все поды Longhorn запустились:
```bash
kubectl get pod -n longhorn-system
```
Увидим что-то вроде:
```text
NAME READY STATUS RESTARTS AGE
csi-attacher-5d68b48d9-6wsff 1/1 Running 0 9m18s
csi-attacher-5d68b48d9-t9wht 1/1 Running 0 9m19s
csi-attacher-5d68b48d9-w772m 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-bd26m 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-vg9g9 1/1 Running 0 9m18s
csi-provisioner-6fcc6478db-xc8ks 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-47pw9 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-n5ndj 1/1 Running 0 9m18s
csi-resizer-6c558c9fbc-xjflz 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-2cbn8 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-5x9fd 1/1 Running 0 9m18s
csi-snapshotter-874b9f887-79dgn 1/1 Running 0 9m18s
engine-image-ei-b907910b-2jgjr 1/1 Running 0 10m
instance-manager-fba73d00a7ff718b4ddabef450cfe759 1/1 Running 0 9m34s
longhorn-csi-plugin-h92s8 3/3 Running 0 9m18s
longhorn-driver-deployer-5f44b4dc59-z7tlc 1/1 Running 0 9m34s
longhorn-manager-k2gtm 2/2 Running 0 10m
longhorn-ui-f7ff9c74-7bbsw 1/1 Running 0 10m
longhorn-ui-f7ff9c74-b4svq 1/1 Running 0 10m
```
Что это за поды? Longhorn состоит из нескольких компонентов, каждый из которых отвечает за свою задачу:
* CSI-компоненты (attacher, provisioner, resizer, snapshotter) интегрируют Longhorn с Kubernetes для работы
с Persistent Volumes (PV) и Persistent Volume Claims (PVC).
* `csi-attacher` -- (3 пода) для присоединения (attaches) тома Longhorn к подам Kubernetes, когда PVC монтируется.
Три реплики CSI -- для отказоустойчивости. На одном узле достаточно одной, но Longhorn по умолчанию разворачивает 3.
* `csi-provisioner` -- (3 пода) для сздания новыех PV (томов) при запросе PVC. Отвечает за динамическое
выделение хранилища.
* `csi-resizer` -- (3 пода) позволяет изменять размер томов (можно только увеличивать PVC) без остановки приложений.
* `csi-snapshotter` -- (3 пода): управля.т созданием и восстановлением снапшотов томов.
* `engine-image` -- хранит бинарники движка Longhorn, используемые для работы томов (чтение/запись данных).
Один под на узел, содержит образ движка для всех томов на этом узле и запускается на каждом узле.
* `instance-manager` -- (1 под) Управляет движками и репликами томов на узле (например, запускает процессы для
чтения/записи данных). Один под на узел для локального управления томами.
* `longhorn-csi-plugin` -- (1 под, 3 контейнера) интерфейс между Kubernetes CSI и Longhorn. Обрабатывает
монтирование/управление томами на узле.
* `longhorn-driver-deployer` -- (1 под) устанавливает CSI-драйвер и регистрирует его в Kubernetes.
Нужен для инициализации CSI-интеграции и он обычно один в кластере.
* `longhorn-manager` -- (1 под, 2 контейнера) Основной компонент Longhorn. Управляет узлами, томами, репликами,
снапшотами и синхронизацией данных. Один под на узел, 2 контейнера (основной + kube-rbac-proxy для авторизации).
* `longhorn-ui` -- (2 реплики) Предоставляют веб-интерфейс для управления Longhorn (графики, настройка томов,
* мониторинг). Две реплики для отказоустойчивости, хотя на одном узле это избыточно.
## Подключение второго узла (мастер)
Для начала, на первой ноде получим токен для подключения нового узла к кластеру: Для начала, на первой ноде получим токен для подключения нового узла к кластеру:
```bash ```bash
@@ -276,10 +424,9 @@ metrics-server ClusterIP 10.43.248.208 <none>
traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.168.1.28 80:31941/TCP,443:30329/TCP,9000:32185/TCP 3d traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.168.1.28 80:31941/TCP,443:30329/TCP,9000:32185/TCP 3d
``` ```
Можем проверить доступность панели `Traefik` через браузер через IP-адрес нового узла и (в нашем случае `http://192.168.1.26:9000/dashboard/#/`) Можем так же настроить [web-панель Traefik](../kubernetes/k3s-setting-up-web-access-to-dashboard.md).
и увидим, что балаансировщик работает и перенаправляет трафик и с ноды воркера.
![k3s--traefic-dashboard.png](../images/k3s--traefic-dashboard.png) ![k3s--traefic-dashboard.png](../images/k3s--traefik-dashboard.png)
Что ж, теперь у нас есть кластер k3s с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не Что ж, теперь у нас есть кластер k3s с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не
идеальная конфигурация, так как у нас четное количество мастер-узлов. идеальная конфигурация, так как у нас четное количество мастер-узлов.
@@ -664,5 +811,30 @@ SSH-тоннель с помощью `autossh` и упаковкой UDP-тра
-----
Cертификаты
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
PVC Longhorn
sudo kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
Установить Helm (менеджер пакетов для Kubernetes):
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Добавить репозиторий Traefik в Helm:
helm repo add traefik https://helm.traefik.io/traefik
Обновить репозитории:
helm repo update
Установить Traefik:
```bash
helm install traefik traefik/traefik -n kube-system --set service.type=LoadBalancer
```