Compare commits
67 Commits
f50b509440
...
master
Author | SHA1 | Date | |
---|---|---|---|
f2583a89cc | |||
948dbe28b0 | |||
be40e3764d | |||
cc4a4add8c | |||
0af69580e7 | |||
fa0eb3188f | |||
adbc6c4072 | |||
c70eb60c8a | |||
f7203aabdc | |||
026a03de3d | |||
54b743e2ad | |||
60e8e24ed7 | |||
dad496edcb | |||
010d85a0ce | |||
d6c747f7c6 | |||
e32b702d7c | |||
08042c99b2 | |||
39a731f369 | |||
af3da4aca3 | |||
356a6d72bb | |||
da9a92f9c6 | |||
f94b1bd679 | |||
b594271e03 | |||
f04d593f53 | |||
75b24bbdab | |||
f94d7e42ed | |||
fc924e196b | |||
af40c9c716 | |||
ce7cea9b0f | |||
c80f4fa469 | |||
88b7e81b84 | |||
8d5b6d1306 | |||
ffb24ad816 | |||
e62371bb52 | |||
76cf200847 | |||
625b6c0b0a | |||
19ba8e07e4 | |||
920cf30bae | |||
18d396991e | |||
92ad28bc58 | |||
1abeffd72c | |||
c3d4600662 | |||
9629b85011 | |||
475a294d61 | |||
7bfc9001e0 | |||
ceb94eb151 | |||
df7777655c | |||
1abefba930 | |||
9f713ba93a | |||
60650b69d2 | |||
2fb12824e2 | |||
93548f786e | |||
e12e8a5b80 | |||
819c122a2b | |||
bf15c4fb7b | |||
effe549f75 | |||
205e8b9c53 | |||
c18a5ffe43 | |||
9e1a414f45 | |||
138bb728fc | |||
dc04658e70 | |||
5560ed2d8e | |||
3b64bc44ac | |||
0b4bc455bf | |||
e2c18fd572 | |||
3bc414ca98 | |||
b6b54e136c |
17
README.md
@@ -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)
|
||||||
|
BIN
images/crowdsec--security-engine-registration.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
images/crowdsec--security-panel.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
images/k3s--chartdb-ui.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
images/k3s--longhorn-ui.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
images/k3s--pods-smokeping.png
Normal file
After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
@@ -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
@@ -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
@@ -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
@@ -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`. и в нем можно будет создавать и редактировать
|
||||||
|
схемы, например:
|
||||||
|
|
||||||
|

|
255
kubernetes/k3s-lets-encrypt-cert-manager.md
Normal 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
@@ -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
|
||||||
|
```
|
||||||
|
|
1022
kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md
Normal file
1251
kubernetes/k3s-postresql.md
Normal 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-сертификат от
|
||||||
|
Let’s 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 (механизма внешней проверки владения доменом
|
||||||
|
со стороны Let’s 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
|
||||||
|
# Позволяет Let’s 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 (Let’s 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-сертификата от Let’s Encrypt
|
||||||
|
# Запрашивает сертификат для <YOU-DOMAIN-NAME> через cert-manager
|
||||||
|
# ВАЖНО: cert-manager должен быть установлен в кластере
|
||||||
|
# ВАЖНО: если манифесты принимаются (apply) последовательно, то манифест с сертификатом должен быть последним для
|
||||||
|
# избежания исчерпания лимитов Let’s 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 для Let’s Encrypt
|
||||||
|
kind: ClusterIssuer
|
||||||
```
|
```
|
||||||
|
|
||||||
Что тут происходит:
|
**ВАЖНО**: В манифесте используется letsencrypt-prod для получения сертификата от Let’s 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
302
kubernetes/k3s-setting-up-web-access-to-dashboard.md
Normal 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Изменение числа реплик 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:
|
||||||
|
|
||||||
|

|
362
kubernetes/k3s-traefik-custom-error-pages.md
Normal 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
@@ -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) и увидите занятные графики (только дождитесь пока данные соберутся, ну часок-другой):
|
||||||
|
|
||||||
|

|
744
raspberry-and-orange-pi/host-protection-with-crowdsec.md
Normal 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 получает блокировки через 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 можно
|
||||||
|
посмотреть ваши локальные блокировки через веб-интерфейсе:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
----
|
||||||
|
#### Управление блокировками
|
||||||
|
|
||||||
|
Можно добавить бан вручную (по умолчанию: `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 из указанных стран при появлении их в логах.
|
||||||
|
И если честно, можно вообще все страны забанить, кроме тех, где бываю в отпуске. Нечего им делать на моем сервере. :)
|
||||||
|
|
@@ -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 с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не
|
Что ж, теперь у нас есть кластер 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|