Compare commits
178 Commits
e9405ec41f
...
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 | |||
f50b509440 | |||
3a71c76f89 | |||
20fa88fa19 | |||
c54ab2ebc1 | |||
f3a296f9e4 | |||
09d32c8cb1 | |||
3a1ce1370b | |||
558653d7e9 | |||
433a3b102f | |||
1571e7118d | |||
ed9159335e | |||
c7a5241d33 | |||
0a90d4a444 | |||
81fbf2ffa6 | |||
a49d432419 | |||
84a6ba4c92 | |||
b470dcdd5d | |||
86c51d33ce | |||
cbf3ae56bb | |||
7b0853da9c | |||
0a6c1591ab | |||
1437724e55 | |||
84a10dbd90 | |||
4ac540eaa9 | |||
e9443f8086 | |||
56f512ebf3 | |||
03fc03bc6a | |||
92a9fd2bc6 | |||
3ea61d918c | |||
8634efc350 | |||
a86d2d5ed7 | |||
ea88f3065a | |||
5d2e53d313 | |||
242f6ca5fb | |||
857d0dd7e1 | |||
ef04ea4e26 | |||
bbaf3c4014 | |||
374f356435 | |||
dc2dca04e1 | |||
def4abb094 | |||
1a72d6ea9b | |||
22538dcde5 | |||
a64615386d | |||
9de84306d8 | |||
91fafb96cf | |||
9064bad6d1 | |||
2b9a7e7c7c | |||
6470803172 | |||
a149cab8c5 | |||
bef2817577 | |||
c45661a2b8 | |||
8148cc2e65 | |||
21bf903d45 | |||
a62f875e60 | |||
1a974ed53b | |||
9b646dfa28 | |||
e6d139fb0e | |||
efd085e1e9 | |||
6132c774c3 | |||
02c8e7d9da | |||
7b45aa7762 | |||
195b6928d5 | |||
e55d4362d4 | |||
c31ff0395a | |||
1f96d09fc6 | |||
14b0f403fa | |||
488b47eb17 | |||
54cf07242b | |||
5c1a2dbf04 | |||
8ff5cd3dd7 | |||
b30ebec857 | |||
e7d272c7d8 | |||
edaba86d24 | |||
f17b5a62b7 | |||
75a4c49d36 | |||
33125bac4f | |||
9d610804bc | |||
937d7e3167 | |||
4850f8d151 | |||
3c342007e7 | |||
3ee178b1e6 | |||
32b50a347e | |||
d3b6bb8476 | |||
74f74f091e | |||
1d11a62504 | |||
934ed4e017 | |||
d86651d1d5 | |||
888ce8a686 | |||
293bbd5de7 | |||
7e16b04f0a | |||
545040792a | |||
cb77d20583 | |||
8860ee5299 | |||
06f4298e63 | |||
11c91eb3b0 | |||
980d3ffdb5 | |||
4243679bc5 | |||
4675399af6 | |||
fb491e4c36 | |||
16b84a68ef | |||
397d01d94e | |||
7a62738c59 | |||
db379ecb6b | |||
57862884c8 | |||
fef7a45268 | |||
90be311f72 | |||
4f8ac49905 | |||
d80ad3d09c | |||
d90b9e1fd0 | |||
8dde0f5c75 | |||
be86f430c6 |
2
.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
.idea
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
|
36
README.md
@@ -2,20 +2,46 @@
|
||||
|
||||
## Docker
|
||||
* [Расположение образов Docker](docker/docker-adjasting.md)
|
||||
* [Установка GPG-ключа для репозиториев Docker](docker/docker-trusted-gpg.md)
|
||||
* [Контейнер MariaDB/MySQL](docker/docker-mariadb.md)
|
||||
* [Nginx и letsencrypt в контейнерах](docker/docker-nginx-w-certbot.md) на примере Portainer
|
||||
* [Контейнер MySQL под Windows 10](docker/docker-mysql-in-windows10.md)
|
||||
* [Развертывание VPN-сервера на базе MS SSTP](docker/docker-sstp-vpn.md)
|
||||
* [Развертывание прокси базе Shadowsocks (сервер и клиент)](docker/docker-shadowsocks.md)
|
||||
|
||||
## Kubernetes (k3s/k8s)
|
||||
* [Установка k3s на Orange Pi 5 Plus](raspberry-and-orange-pi/k3s.md)
|
||||
* [Под с Shadowsocks-клиент](kubernetes/k3s-shadowsocks-client.md) в k3s
|
||||
* [Подключение менеджера сертификатов (cert-manager) Let's Encrypt](kubernetes/k3s-lets-encrypt-cert-manager.md) к k3s
|
||||
* [Под с 3X-UI](kubernetes/k3s-3xui-pod.md) в k3s
|
||||
* [Проксирование внешнего хоста через Traefik (Ingress-контроллер)](kubernetes/k3s-proxy.md)
|
||||
* [Перенос контейнера Docker в k3s](kubernetes/k3s-migrating-container-from-docker-to-kubernetes.md) (на примере Gitea)
|
||||
* [Резервное копирование k3s](kubernetes/k3s-backup.md)
|
||||
* [Настройка доступа к панелям управления](kubernetes/k3s-setting-up-web-access-to-dashboard.md) Longhorn и Traefik
|
||||
* [Под с SmokePing](kubernetes/k3s_smokeping.md) для мониторинга доступности хостов
|
||||
* [PostgeSQL в K3s](kubernetes/k3s-postresql.md)
|
||||
* [ChartDB в K3s](kubernetes/k3s-chartdb.md) — графический редактор схем баз данных
|
||||
* [Развертывание Metabase в K3s](kubernetes/k3s-metabase.md) — BI-платформа для анализа данных
|
||||
|
||||
## Python
|
||||
* [Устранение проблем при установке mysqlclient (подключение к MySQL/MariaDB)](python/python-mysql.md)
|
||||
|
||||
## Raspberry Pi / Orange Pi
|
||||
* [Установка Docker и Docker Compose](raspberry-pi/install-docker-compose.md)
|
||||
* [Резервное копирование и восстановление](raspberry-pi/backup-restore.md)
|
||||
* [Устранение проблем при установке Python-коннектора mysqlclient (MySQL/MariaDB)](python/python-mysql.md)
|
||||
* [Python-скрипт как служба Linux](python/python_as_service.md)
|
||||
|
||||
## Linux (возможно в специфике Orange Pi / Raspberry Pi)
|
||||
* [Установка (перенос) системы на NVMe или eMMC (для Orange Pi 5 Plus)](raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md)
|
||||
* [Измерение производительности накопителей](raspberry-and-orange-pi/measuring-performance-storage-devices.md)
|
||||
* [Установка Docker и Docker Compose](raspberry-and-orange-pi/install-docker-compose.md)
|
||||
* [Резервное копирование и восстановление](raspberry-and-orange-pi/backup-restore.md)
|
||||
* [k8s (кubernetes) на Orange Pi (драфт...)](raspberry-and-orange-pi/k8s.md)
|
||||
* [k3s (кubernetes) на Orange Pi](raspberry-and-orange-pi/k3s.md)
|
||||
* [Перекомпиляция ядра Linux (включение пподдержки iSCSI в Orange Pi 5 Plus](raspberry-and-orange-pi/opi5plus-rebuilding-linux-kernel-for-iscsi.md)
|
||||
* [Защита хоста с помощью CrowdSec](raspberry-and-orange-pi/host-protection-with-crowdsec.md), включая GeoIP блокировки
|
||||
*
|
||||
## Nginx / Apache
|
||||
* [Ограничение доступа по User-Agent (на примере GPTBot)](nginx/nginx-ban-user-agent.md)
|
||||
* [Настройка nginx как прямого прокси](nginx/nginx_as_direct_proxy.md)
|
||||
|
||||
## Разное
|
||||
* [Настройка RU-локали в Ubuntu/Debian](misc/set-locale-ru.md)
|
||||
* [Развертывание Django-приложения (сайта) на VDS-хостинге](misc/deploying-django-site-to-dvs-hosting.md)
|
||||
* [Сплиттер для разделения логов](misc/splitter-for-logs.md)
|
663
docker/docker-nginx-w-certbot.md
Normal file
@@ -0,0 +1,663 @@
|
||||
# Веб-сервер Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker
|
||||
|
||||
Для удобного переноса сайтов или веб-приложений между серверами, а также для упрощения обновления и обслуживания
|
||||
веб-сервера nginx, удобно держать его в контейнере Docker. В данной инструкции рассмотрено развертывание веб-сервера
|
||||
Nginx с SSL-сертификатами Let's Encrypt в контейнерах Docker. В качестве примера сайта используется контейнер Portainer --
|
||||
отличный инструмент для управления Docker-контейнерами через веб-интерфейс.
|
||||
|
||||
### Соглашения:
|
||||
| | |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------|
|
||||
| `web` | Пользователь от имени которого мы работаем |
|
||||
| `/home/web` | Домашний каталог. |
|
||||
| `/home/web/docker-data` | Каталог для хранения данных Docker-контейнеров (место куда монтируют тома контейнеров) |
|
||||
| `portainer.you.domain.name` | Домен, по которому будет доступен Portainer |
|
||||
| `email@you.domain.name` | Email для сертификатов Let's Encrypt | |
|
||||
|
||||
Чтобы не случилось путаницы во время копи-паста рекомендуется сразу произвести все замены в тексте.
|
||||
|
||||
## Portainer + nginx в контейнерах Docker
|
||||
|
||||
И так, для начала создадим каталог для хранения данных Portainer (можно опустить если вам не нужен Portainer):
|
||||
```bash
|
||||
mkdir -p /home/web/docker-data/portainer
|
||||
```
|
||||
|
||||
Теперь создадим каталог для хранения конфигурационных файлов Nginx. Сам Nginx будет сидеть в контейнере, но
|
||||
конфигурационные файлы, которые он будет использовать, находятся на хосте в каталоге `/home/web/docker-data/nginx/conf.d`:
|
||||
```bash
|
||||
mkdir -p /home/web/docker-data/nginx/conf.d
|
||||
```
|
||||
|
||||
Теперь создадим файл конфигурации Nginx, который будет использоваться для проксирования запросов к контейнеру Portainer:
|
||||
```bash
|
||||
nano /home/web/docker-data/nginx/conf.d/portainer.conf
|
||||
```
|
||||
|
||||
Вставьте в файл следующее содержимое:
|
||||
```nginx configuration
|
||||
server {
|
||||
listen 80;
|
||||
server_name portainer.you.domain.name;
|
||||
|
||||
location / {
|
||||
proxy_pass http://portainer:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Что происходит в этом файле конфигурации:
|
||||
- `listen 80;` — слушаем порт 80 (обычный HTTP-трафик);
|
||||
- `server_name portainer.you.domain.name;` — имя хоста, по которому будет доступен Portainer (замените `you.domain.name`
|
||||
на ваш домен);
|
||||
- `proxy_pass http://portainer:9000;` — проксируем запросы в контейнер Portainer, который будет доступен по хосту (имени
|
||||
контейнера `portainer`) и порту 9000;
|
||||
- `proxy_set_header Host $host;` — передаем заголовок `Host` в запросе;
|
||||
- `proxy_set_header X-Real-IP $remote_addr;` — передаем заголовок `X-Real-IP` в запросе;
|
||||
- `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` — передаем заголовок `X-Forwarded-For` в запросе;
|
||||
|
||||
Сохраните файл и выйдите из редактора (`Ctrl + X`, затем `Y` для подтверждения).
|
||||
|
||||
Теперь создадим файл `docker-compose.yml` для развертывания контейнеров Nginx и Portainer:
|
||||
```bash
|
||||
nano /home/web/docker-data/docker-compose.yml
|
||||
```
|
||||
|
||||
Вставьте в файл следующее содержимое:
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ce:latest
|
||||
container_name: portainer
|
||||
# Гасим порт 9000, чтобы он не светил на хост, а был доступен только во внутри-контейнерной сети
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /home/web/docker-data/portainer:/data
|
||||
restart: always
|
||||
networks:
|
||||
- web
|
||||
# Можно закомментировать строку выше и раскомментировать строки ниже если зачем-то нужен закрепленный IP-адрес
|
||||
# web:
|
||||
# ipv4_address: 172.20.0.10
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
|
||||
restart: always
|
||||
networks:
|
||||
- web
|
||||
|
||||
networks:
|
||||
web:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/24 # Подсеть для пользовательской сети
|
||||
```
|
||||
|
||||
Что у нас настроено в этом `docker-compose.yml`:
|
||||
- `portainer` (у вас его может и не быть, или быть какой-то другой сервис, который вы будете производить):
|
||||
- `image: portainer/portainer-ce:latest` — используем образ Portainer Community Edition;
|
||||
- `container_name: portainer` — имя контейнера `portainer`;
|
||||
- `volumes: ...` — монтируем файлы или тома изнутри контейнера на хост. В данном случае маппим: сокет —
|
||||
чтобы изнутри контейнера Portainer можно было через Docker API управлять Docker самого хоста (вот так хитро) и
|
||||
каталог `/home/web/docker-data/portainer` — чтобы сохранять настройки и данные Portainer между перезапусками;
|
||||
- `restart: always` — автоматически перезапускаем контейнер при его остановке или перезагрузке хоста;
|
||||
- `networks: ...` — подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
|
||||
- `nginx`:
|
||||
- `image: nginx:latest` — используем образ Nginx;
|
||||
- `container_name: nginx` — имя контейнера `nginx`;
|
||||
- `ports: ...` — пробрасываем порты 80 и 443 из контейнера на хост;
|
||||
- `volumes: ...` — монтируем каталог с конфигурационными файлами Nginx;
|
||||
- `restart: always` — автоматически перезапускаем контейнер при его остановке или перезагрузке хоста;
|
||||
- `networks: ...` — подключаем контейнер к пользовательской (внутри-контейнерной) сети `web`.
|
||||
- `networks: ...`:
|
||||
- `web`:
|
||||
- `driver: bridge` — используем драйвер сети `bridge` (по умолчанию);
|
||||
- `ipam: ...` — настраиваем IP-адреса для контейнеров внутри сети `web`. В данном случае используем подсеть
|
||||
|
||||
Сохраняем файл `docker-compose.yml` и выходим из редактора (`Ctrl + X`, затем `Y` для подтверждения).
|
||||
|
||||
Теперь развернем контейнеры Nginx и Portainer:
|
||||
```bash
|
||||
cd /home/web/docker-data
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
После того как контейнеры запустятся, можно зайти в веб-интерфейс Portainer по адресу `http://portainer.you.domain.name`.
|
||||
|
||||
## Добавляем контейнер Let's Encrypt
|
||||
|
||||
Создадим каталог для хранения ключей сертификатов Let's Encrypt и временных файлов для проверки владения доменом:
|
||||
```bash
|
||||
mkdir -p /home/web/docker-data/letsencrypt
|
||||
mkdir -p /home/web/docker-data/letsencrypt/_cert
|
||||
mkdir -p /home/web/docker-data/letsencrypt/_ownership_check
|
||||
```
|
||||
|
||||
Добавим в `docker-compose.yml` Certbot-контейнер (для получения сертификатов Let's Encrypt):
|
||||
|
||||
```yaml
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: letsencrypt-certbot
|
||||
volumes:
|
||||
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt # Для хранения сертификатов
|
||||
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html # Для временных файлов Let's Encrypt
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Для доступа к Docker API и контейнерам хоста
|
||||
networks:
|
||||
- web
|
||||
entrypoint: "/bin/sh -c 'apk add --no-cache curl && trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done'"
|
||||
```
|
||||
|
||||
*Что тут происходит и зачем нам такие мапинги томов?*
|
||||
|
||||
1. Когда certbot запрашивает сертификат у Let's Encrypt, то тот требует подтверждения владения доменом. При работе
|
||||
certbot в контейнере самый популярный (и лучший при работе в контейнере) способ подтверждения — это HTTP-проверка.
|
||||
Certbot создает временные файлы в каталоге `/var/www/html/letsencrypt` (мы будем указывать этот каталог при инициализации
|
||||
сертификата). Сервер же Let's Encrypt перед выдачей сертификата делает HTTP-запрос к этому временному файлу по URL
|
||||
(например, в нашем случае по `http://portainer.you.domain.namey/.well-known/acme-challenge/`) и если файл доступен,
|
||||
владение доменом считается подтвержденным, и сертификат выдается.
|
||||
|
||||
Таким образом, маппинг `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/html` позволяет certbot создавать
|
||||
временные файлы в каталоге `/home/web/docker-data/letsencrypt/_ownership_check` хоста, и эти файлы nginx сможет
|
||||
"отдать" при проверке со стороны Let's Encrypt.
|
||||
|
||||
Конечно, нам еще придется настроить nginx, чтобы он мог отдавать эти временные файлы. Но об этом чуть позже.
|
||||
|
||||
2. Мапинг `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` позволяет certbot сохранять получаемые
|
||||
и обновляемые сертификаты Let's Encrypt в каталоге хоста `/home/web/docker-data/letsencrypt/_cert`, чтобы они не
|
||||
пропадали при перезапуске контейнера certbot.
|
||||
|
||||
3. Мапинг `/var/run/docker.sock:/var/run/docker.sock` позволяет certbot управлять контейнерами Docker, чтобы он мог
|
||||
перезапускать контейнеры (в нашем случае контейнер nginx) в случае обновления сертификатов. В принципе, этот мапинг
|
||||
можно не делать, и обойтись хуками certbot, но это сложнее.
|
||||
|
||||
4. `entrypoint: ...`:
|
||||
- `apk add --no-cache curl` — устанавливаем пакет `curl`, который потребуется для работы с Docker API через сокет;
|
||||
- `trap exit TERM;` — устанавливаем обработчик сигнала `TERM`, чтобы контейнер certbot корректно завершал работу;
|
||||
- `while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done`
|
||||
— это скрипт, который запускается при старте контейнера certbot. Он запускает certbot в режиме `renew` каждые 12 часов.
|
||||
Таким образом, сертификаты будут автоматически провереться каждые 12 часов и обновляться, если это необходимо.
|
||||
|
||||
Также нам нужно добавить маппинг тома для сертификатов Let's Encrypt в контейнере `nginx`. Теперь описание этого контейнера
|
||||
в `docker-compose.yml` будет выглядеть так:
|
||||
```yaml
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
|
||||
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt # <- этот маппинг для сертификатов
|
||||
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt # <- этот маппинг для временных файлов
|
||||
restart: always
|
||||
networks:
|
||||
- web
|
||||
```
|
||||
|
||||
*Тут происходит очень похожий маппинг тома для сертификатов Let's Encrypt и временных файлов для проверки
|
||||
владения, но теперь в контейнер с nginx*:
|
||||
|
||||
- `volumes: ...`:
|
||||
- `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` — маппинг тома для сертификатов Let's Encrypt, чтобы их
|
||||
можно было использовать в контейнере nginx;
|
||||
- `/home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt` — маппинг тома временных файлов для
|
||||
проверки владения доменом со стороны Let's Encrypt.
|
||||
|
||||
Сохраняем файл `docker-compose.yml`.
|
||||
|
||||
Теперь нам нужно настроить nginx, чтобы он мог отдавать временные файлы Certbot. Для этого изменим конфигурационный файл
|
||||
`/home/web/docker-data/nginx/conf.d/portainer.conf` на следующий:
|
||||
```nginx configuration
|
||||
server {
|
||||
listen 80;
|
||||
server_name portainer.you.domain.name;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
# Или так, с помощью alias:
|
||||
# alias /var/www/letsencrypt/.well-known/acme-challenge/;
|
||||
# try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://portainer:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Как видно, мы добавили новый *location*-блок, который отдает временные файлы Certbot из каталога `/var/www/letsencrypt/`
|
||||
(а это каталог контейнера `nginx`, который мы ранее замаппили в каталог хоста `/home/web/docker-data/letsencrypt/_ownership_check`).
|
||||
Он явно указывает, что запросы к `/.well-known/acme-challenge/` не должны идти в прокси, а должны обслуживаться локально.
|
||||
Используем директиву `location ^~` — она приоритетнее `location /` и такой *location* будет срабатывать даже при
|
||||
включённом proxy_pass.
|
||||
|
||||
**Важно!** После того как сертификаты Let's Encrypt будут получены, не надо удалять этот `location ^~` из конфигурации!
|
||||
Он нужен для автоматического обновления сертификатов: certbot будет снова создавать временные файлы в каталоге
|
||||
`/var/www/letsencrypt/`, а сервера Let's Encrypt "дергать" их и тем самым проверять права владения. Если nginx
|
||||
не сможет отдать эти файлы, то и обновление сертификатов не произойдет.
|
||||
|
||||
Теперь, если все контейнеры docker-compose, и `nginx`, и `letsencrypt-certbot`, запущены можно инициализировать
|
||||
получение сертификатов Let's Encrypt:
|
||||
```bash
|
||||
docker exec -it letsencrypt-certbot certbot certonly \
|
||||
--webroot -w /var/www/html \
|
||||
-d ortainer.you.domain.name \
|
||||
--email email@you.domain.name \
|
||||
--agree-tos --no-eff-email --force-renewal
|
||||
```
|
||||
|
||||
Или можно поднять только nginx (чтобы он отдавал временные файлы для проверки владения доменом):
|
||||
```bash
|
||||
cd /home/web/docker-data
|
||||
docker-compose up -d nginx
|
||||
```
|
||||
|
||||
И запусть контейнер `letsencrypt-certbot` с инициализацией получения сертификатов:
|
||||
```bash
|
||||
docker run --rm --name letsencrypt-certbot \
|
||||
-v /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html \
|
||||
-v /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt \
|
||||
certbot/certbot certonly --webroot \
|
||||
-w /var/www/html \
|
||||
-d portainer.you.domain.name \
|
||||
--email email@you.domain.name \
|
||||
--agree-tos --no-eff-email --force-renewal
|
||||
```
|
||||
|
||||
Если все пройдет успешно (должно пройти) мы увидим примерно такой вывод:
|
||||
```text
|
||||
certbot certonly --webroot -w /var/www/html -d portainer.you.domain.name --email email@you.domain.name --agree-tos --no-eff-email --force-renewal
|
||||
Saving debug log to /var/log/letsencrypt/letsencrypt.log
|
||||
Requesting a certificate for portainer.you.domain.name
|
||||
|
||||
Successfully received certificate.
|
||||
Certificate is saved at: /etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem
|
||||
Key is saved at: /etc/letsencrypt/live/portainer.you.domain.name/privkey.pem
|
||||
This certificate expires on 2025-05-14.
|
||||
These files will be updated when the certificate renews.
|
||||
NEXT STEPS:
|
||||
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
If you like Certbot, please consider supporting our work by:
|
||||
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
|
||||
* Donating to EFF: https://eff.org/donate-le
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
```
|
||||
|
||||
Если у вас что-то пойдёт не так, то скорее всего напутано в маппингах томов или в конфигурации nginx. Вы можете
|
||||
посмотреть логи `certbot` (возможно вам придется замаппить каталог логов из контейнера certbot на хост).
|
||||
|
||||
Уже хочется проверить, что все работает? Рано! Нам нужно добавить в конфигурацию nginx SSL-сертификаты и настроить
|
||||
перенаправление с HTTP на HTTPS. Отредактируем конфиг nginx, теперь он будет выглядеть так:
|
||||
```nginx configuration
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name portainer.you.domain.name;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/portainer.you.domain.name/privkey.pem;
|
||||
|
||||
# Рекомендуемые SSL настройки
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/letsencrypt;
|
||||
# Или так, с помощью alias:
|
||||
# alias /var/www/letsencrypt/.well-known/acme-challenge/;
|
||||
# try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://portainer:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Перенаправление с HTTP на HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name portainer.you.domain.name;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
Что изменилось в конфигурации:
|
||||
- Добавлены директивы для SSL для сервера в котором живет прокси на Portainer:
|
||||
- `listen 443 ssl;` — теперь сервер слушает порт 443 (HTTPS) и использует SSL;
|
||||
- `ssl_certificate ...` и `ssl_certificate_key ...` — указываем пути к сертификату и ключу сертификата Let's Encrypt;
|
||||
- `ssl_protocols TLSv1.2 TLSv1.3;` — указываем протоколы SSL/TLS, которые будут использоваться;
|
||||
- `ssl_ciphers HIGH:!aNULL:!MD5;` — указываем шифры, которые будут использоваться (`HIGH` → Разрешает только сильные
|
||||
шифры, например, AES256, `!aNULL` → Запрещает анонимные шифры, которые не используют аутентификацию и уязвимы
|
||||
к MITM-атакам, `!MD5` → Запрещает использование хэша MD5, так как он давно признан небезопасным;
|
||||
- `ssl_prefer_server_ciphers on;` — указываем, что сервер предпочтет использовать свои шифры, а не клиентские.
|
||||
- Добавлен блок `server` для перенаправления с HTTP на HTTPS:
|
||||
- `return 301 https://$host$request_uri;` — перенаправляем все запросы с порта 80 на порт 443 c кодом 301 (перемещено
|
||||
навсегда).
|
||||
|
||||
Сохраним файл и, наконец запустим все контейнеры:
|
||||
```bash
|
||||
cd /home/web/docker-data
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Теперь можно проверить, что все работает, и зайдите в веб-интерфейс Portainer по адресу `https://portainer.you.domain.name`.
|
||||
|
||||
Осталось проверить, что перевыпуск сертификатов будет происходить и мы всё ещё ничего не сломали в мапингах:
|
||||
```bash
|
||||
docker exec -it letsencrypt-certbot certbot renew --dry-run -v
|
||||
```
|
||||
|
||||
Должны увидеть примерно такой вывод:
|
||||
```text
|
||||
Saving debug log to /var/log/letsencrypt/letsencrypt.log
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Processing /etc/letsencrypt/renewal/portainer.you.domain.name.conf
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Certificate not due for renewal, but simulating renewal for dry run
|
||||
Plugins selected: Authenticator webroot, Installer None
|
||||
Simulating renewal of an existing certificate for portainer.you.domain.name
|
||||
Performing the following challenges:
|
||||
http-01 challenge for portainer.you.domain.name
|
||||
Using the webroot path /var/www/html for all unmatched domains.
|
||||
Waiting for verification...
|
||||
Cleaning up challenges
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Congratulations, all simulated renewals succeeded:
|
||||
/etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem (success)
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
```
|
||||
|
||||
И наконец, при обновлении сертификатов нужно перезапускать контейнер `nginx` чтобы он перподключил новые сертификаты.
|
||||
Изнутри контейнера `letsencrypt-certbot` мы не можем управлять контейнерами Docker (и даже ничего не знаем о них),
|
||||
но можно добавить **хук** в `letsencrypt-certbot`, который будет перезапускать контейнер `nginx` сразу после успешного
|
||||
обновления сертификатов (и только если обновление прошло успешно)!
|
||||
|
||||
Что такое **хук**? Это скрипт, который выполняется в определенный момент жизненного цикла `certbot`. Certbot автоматически
|
||||
ищет и выполняет скрипты в специальных каталогах, которые и называют **хуками**:
|
||||
- `/etc/letsencrypt/renewal-hooks/deploy/` – скрипты выполняются после успешного обновления;
|
||||
- `/etc/letsencrypt/renewal-hooks/pre/` – выполняются до начала обновления;
|
||||
- `/etc/letsencrypt/renewal-hooks/post/` – выполняются после любой попытки обновления (даже если оно не удалось);
|
||||
|
||||
Таким образом наш маппинг `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` обеспечит нам исполнение хуков
|
||||
внутри контейнера certbot, хотя сами хуки будут лежать на хосте. Добавим скрипт
|
||||
`/home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh`:
|
||||
```bash
|
||||
mkdir -p /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy
|
||||
sudo nano /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh
|
||||
```
|
||||
|
||||
Обратите внимание, что хук мы создаем через `sudo`. Все содержимое `/home/web/docker-data/letsencrypt/_cert` было создано
|
||||
изнутри контейнера `certbot`, и поэтому принадлежит пользователю `root` (`root` из контейнера, но на хосте он превратился
|
||||
в `root` хоста). Поэтому нам нужно использовать `sudo` чтобы редактировать файл.
|
||||
|
||||
Вставим в скрипт следующее содержимое:
|
||||
```bash
|
||||
#!/bin/sh
|
||||
echo "СРАБОТАЛ ХУК \"deploy/restart-nginx.sh\": перезапускаем контейнер nginx"
|
||||
curl -s -o /dev/null --unix-socket /var/run/docker.sock -X POST http:/v1.41/containers/nginx/restart
|
||||
```
|
||||
|
||||
Что тут важно? Несмотря на то, что скрипт лежит на хосте, он все равно будет исполняться внутри контейнера `letsencrypt-certbot`.
|
||||
Потому просто так перезапустить контейнер `nginx` не получится. Нам нужно использовать Docker API, чтобы сделать это.
|
||||
Именно поэтому мы в нашем `docker-compose.yml` замаппили сокет Docker в контейнер `letsencrypt-certbot` и устанавливали `curl`.
|
||||
|
||||
Сохраним файл и сделаем его исполняемым:
|
||||
```bash
|
||||
sudo chmod +x /home/web/docker-data/letsencrypt/_cert/renewal-hooks/deploy/restart-nginx.sh
|
||||
```
|
||||
|
||||
Теперь можно проверить как все это сработает. Посмотрим на наши контейнеры:
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
И увидим наши три контейнера:
|
||||
```text
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
8be62353e563 nginx:latest "/docker-entrypoint.…" 13 minutes ago Up 9 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp nginx
|
||||
6b805c7df486 portainer/portainer-ce:latest "/portainer" 13 minutes ago Up 13 minutes 8000/tcp, 9000/tcp, 9443/tcp portainer
|
||||
dd0b7a683dde certbot/certbot:latest "/bin/sh -c 'apk add…" 13 minutes ago Up 13 minutes 80/tcp, 443/tcp letsencrypt-certbot
|
||||
```
|
||||
|
||||
Дадим команду в контейнер `letsencrypt-certbot` на принудительное обновление сертификатов:
|
||||
```bash
|
||||
docker exec -it letsencrypt-certbot certbot renew --force-renewal
|
||||
```
|
||||
|
||||
Увидим как certbot обновляет сертификаты:
|
||||
```text
|
||||
aving debug log to /var/log/letsencrypt/letsencrypt.log
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Processing /etc/letsencrypt/renewal/portainer.you.domain.name.conf
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Renewing an existing certificate for portainer.you.domain.name
|
||||
Hook 'deploy-hook' ran with output:
|
||||
СРАБОТАЛ ХУК "deploy/restart-nginx.sh": перезапускаем контейнер nginx
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Congratulations, all renewals succeeded:
|
||||
/etc/letsencrypt/live/portainer.you.domain.name/fullchain.pem (success)
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
```
|
||||
|
||||
Теперь можно убедиться, что контейнер nginx перезапустился:
|
||||
```bash
|
||||
docker ps
|
||||
```
|
||||
|
||||
И увидим, что STATUS контейнера nginx изменился (uptime сброшен):
|
||||
```text
|
||||
8be62353e563 nginx:latest "/docker-entrypoint.…" 2 seconds ago Up 24 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp nginx
|
||||
6b805c7df486 portainer/portainer-ce:latest "/portainer" 13 minutes ago Up 13 minutes 8000/tcp, 9000/tcp, 9443/tcp portainer
|
||||
dd0b7a683dde certbot/certbot:latest "/bin/sh -c 'apk add…" 13 minutes ago Up 13 minutes 80/tcp, 443/tcp
|
||||
```
|
||||
|
||||
**Все!** Теперь у нас полностью контейнеризированное решение, без лишних зависимостей на хосте, и при переносе каталога
|
||||
`~/docker-data` на другой сервер (с Docker + docker-compose) всё должно точно также запуститься.
|
||||
|
||||
## Зависимости и "проверки здоровья"
|
||||
|
||||
В нашем случае контейнеры не особо зависят друг от друга, но если стремиться к идеальной контейнеризации, то можно
|
||||
рассмотреть "кто на ком стоит", какие зависимости и какой порядок запуска контейнеров.
|
||||
|
||||
Самый независимый контейнер — это `portainer`. Он просто запускается и работает. Начали его проксировать через nginx
|
||||
или нет — ему важно.
|
||||
|
||||
Контейнер `letsencrypt-certbot` зависит от контейнера `nginx`, так как он без него не пройдет валидация домена.
|
||||
Решение установить `depends_on` от контейнера `nginx` не идеально, т.к. старт контейнера `nginx` не означает, что он
|
||||
готов принимать запросы. Избежать проблем можно сделав **healthcheck** для Nginx.
|
||||
|
||||
В docker-compose.yml добавим:
|
||||
```yaml
|
||||
nginx:
|
||||
...
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
...
|
||||
...
|
||||
|
||||
certbot:
|
||||
...
|
||||
depends_on:
|
||||
nginx:
|
||||
condition: service_healthy
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
Что тут происходит:
|
||||
- `healthcheck: ...` — добавляем блок в контейнере `nginx` для проверки его здоровья. В данном случае проверяем
|
||||
доступность Nginx выполняя команду `curl -f http://localhost`. Если Nginx отвечает, то контейнер считается здоровым;
|
||||
- `interval: 10s` — проверка состояния каждые 10 секунд;
|
||||
- `timeout: 3s` — ожиданий проверки 3 секунды (когда curl зависнет дольше 3 секунд, то это считается ошибкой);
|
||||
- `retries: 3` — количество попыток проверки (если команда завершается с ошибкой (код ≠ 0) — Docker попробует ещё
|
||||
раз... и так до 3-х раз);
|
||||
- `start_period: 10s` — время ожидания перед началом проверок (первая проверка будет выполнена через 10 секунд после
|
||||
старта контейнера);
|
||||
- `depends_on: ...` — добавляем зависимость контейнера `certbot` от контейнера `nginx`. Но не просто так, а с условием
|
||||
`service_healthy`. Это означает, что контейнер `certbot` не будет запущен, пока контейнер `nginx` не будет здоров.
|
||||
|
||||
Теперь при запуске контейнера `certbot` он будет ждать, пока контейнер `nginx` не станет здоровым (а еще задержку
|
||||
в 10 секунд будет заметно даже на глаз).
|
||||
|
||||
К сожалению, если `nginx` упадет в процессе, то `certbot` не будет перезапущен. Но это уже другая история,
|
||||
мир не идеален.
|
||||
|
||||
### Погасить проверку здоровья после первого успешного запуска (раздел для параноиков)
|
||||
|
||||
Бдительный читатель может заметить, что проверка `curl -f http://localhost` будет порождать бесполезную нагрузку на
|
||||
сервере (ведь кажется, что по locahost будет отвечать `portainer`, а не `nginx`). Но это не так. Прокси на `portainer`
|
||||
отвечать там не будет (ведь он настроен на домен `portainer.you.domain.name`), а будет отдавать дефолтную страницу
|
||||
`nginx`. У нас она даже не настроена, и будет 301, а это вообще очень-очень мало не потребляет...
|
||||
|
||||
Тем не менее есть способ погасить проверку здоровья, как только контейнер стал `healthy` (допустим, чтобы "не следить"
|
||||
в логах). У нас есть мапп `/home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt` внутри контейнера `nginx`. Если мы
|
||||
создадим скрипт в каталоге `/home/web/docker-data/letsencrypt/_cert` он будет доступен внутри контейнера в каталоге
|
||||
`/etc/letsencrypt`. Создадим скрипт `healthcheck-nginx.sh`:
|
||||
```bash
|
||||
nano /home/web/docker-data/letsencrypt/_cert/healthcheck-nginx.sh
|
||||
```
|
||||
|
||||
И поместим в него следующее содержимое:
|
||||
```bash
|
||||
#!/bin/sh
|
||||
HEALTH_FILE_FLAG="/tmp/nginx_healthy"
|
||||
|
||||
if [ -f "HEALTH_FILE_FLAG" ]; then
|
||||
exit 0 # Уже healthy, больше не проверяем
|
||||
fi
|
||||
|
||||
if curl -fs http://localhost > /dev/null; then
|
||||
touch "HEALTH_FILE_FLAG" # Создаем файл-флаг
|
||||
exit 0 # Успешная проверка
|
||||
else
|
||||
exit 1 # Nginx еще не поднялся, пусть проверяют еще
|
||||
fi
|
||||
```
|
||||
|
||||
Что будет происходить. При старте контейнера `nginx` через *healthcheck* будем запускать этот скрипт.
|
||||
При первом запуске он проверит есть ли файл-флаг `/tmp/nginx_healthy` внутри контейнера. Сразу после старта контейнера
|
||||
этого фал-флага нет (контейнер запускается "чистым"). Затем скрипт попытается выполнить `curl -fs http://localhost`.
|
||||
Если Nginx еще не поднялся, то **curl** возвращает ошибку и контейнер остается *unhealthy*. Но если Nginx уже работает,
|
||||
то **touch** создается файл-флаг `/tmp/nginx_healthy` то **exit 0** ответит, что контейнер *healthy*.⃣ При всех
|
||||
следующих *healthcheck* проверка файл-флага пройдет успешно, сразу будет получен `exit 0` и проверка *healthy* будет
|
||||
завершена без вызова `curl`.
|
||||
|
||||
Осталось добавить этот скрипт в `docker-compose.yml`:
|
||||
```yaml
|
||||
nginx:
|
||||
...
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "/etc/letsencrypt/healthcheck-nginx.sh"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
### Зависимости и healthcheck для `portainer` (или параноики могут сломать бизнес-логику)
|
||||
|
||||
А что с `nginx` и `portainer`? Они зависят друг от друга? Кажется нет. Если `nginx` упадет, то `portainer` просто
|
||||
станет недоступен (ничего не будет доступно вообще). Если же `portainer` упадет, то `nginx` будет отдавать ошибку 502.
|
||||
Ошибка, в данном случае, это тоже информация. Как минимум мы увидим что nginx работает, а упало приложение.
|
||||
|
||||
Тем не менее можно добавить `healthcheck` в `portainer`, проверять, что он отвечает внутри себя по порту 9000,
|
||||
а затем установить зависимость `nginx` от здоровья `portainer`.
|
||||
|
||||
Но, кажется, это уже перебор.
|
||||
|
||||
## Итоговый docker-compose.yml
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
portainer:
|
||||
image: portainer/portainer-ce:latest
|
||||
container_name: portainer
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /home/web/docker-data/portainer:/data
|
||||
restart: always
|
||||
networks:
|
||||
- web
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /home/web/docker-data/nginx/conf.d:/etc/nginx/conf.d
|
||||
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt
|
||||
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/letsencrypt
|
||||
restart: always
|
||||
healthcheck:
|
||||
# test: ["CMD", "curl", "-f", "http://localhost/.well-known/acme-challenge/"]
|
||||
# test: ["CMD", "sh", "-c", "netstat -tln | grep -q ':80'"]
|
||||
test: ["CMD", "sh", "/etc/letsencrypt/healthcheck-nginx.sh"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- web
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: letsencrypt-certbot
|
||||
volumes:
|
||||
- /home/web/docker-data/letsencrypt/_ownership_check:/var/www/html
|
||||
- /home/web/docker-data/letsencrypt/_cert:/etc/letsencrypt
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
nginx:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- web
|
||||
# entrypoint: "/bin/sh -c 'trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew; done'"
|
||||
entrypoint: "/bin/sh -c 'apk add --no-cache curl && trap exit TERM; while :; do sleep 12h & wait $${!}; certbot renew --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/restart-nginx.sh; done'"
|
||||
|
||||
networks:
|
||||
web:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/24 # Подсеть для пользовательской сети
|
||||
```
|
79
docker/docker-trusted-gpg.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Установка GPG-ключа для репозиториев Docker
|
||||
|
||||
Иногда, при установке Docker, возникает ошибка с ключом GPG. Например, при установке Docker на Ubuntu 20.04. Тогда
|
||||
при обновлении списка пакета при обновлении командой `sudo apt-get update` выдается сообщение (или подобное):
|
||||
|
||||
> W: https://mirrors.aliyun.com/docker-ce/linux/ubuntu/dists/jammy/InRelease: Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg), see the DEPRECATION section in apt-key(8) for details. ??
|
||||
|
||||
Это связано с устаревшей практикой хранения ключей репозиториев в общем файле `/etc/apt/trusted.gpg`, а предупреждение
|
||||
означает, что ключ GPG для репозитория Docker CE хранится в старом формате, который в будущем будет удален из
|
||||
APT (Advanced Package Tool). Ранее APT использовал общий файл /etc/apt/trusted.gpg для хранения **всех** ключей
|
||||
репозиториев. Это устарело из-за соображений безопасности. Новая практика заключается в том, чтобы хранить ключи
|
||||
в отдельных файлах в директории `/etc/apt/trusted.gpg.d/`.
|
||||
|
||||
## Как исправить
|
||||
|
||||
Найдём ключ для репозитория:
|
||||
```shell
|
||||
apt-key list
|
||||
```
|
||||
|
||||
Ключ, связанный с репозиторием Docker может выглядеть, например, так (и радом будет предупреждение):
|
||||
```text
|
||||
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).
|
||||
/etc/apt/trusted.gpg
|
||||
--------------------
|
||||
pub rsa4096 2017-02-22 [SCEA]
|
||||
9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
|
||||
uid [ unknown] Docker Release (CE deb) <docker@docker.com>
|
||||
sub rsa4096 2017-02-22 [S]
|
||||
```
|
||||
|
||||
Удалим ключ из старой связки ключей (keyring):
|
||||
```shell
|
||||
sudo apt-key del 0EBFCD88
|
||||
```
|
||||
|
||||
На всякий случай, проверим, что установлены покеты для работы с HTTPS, curl для загрузки ключей по интернет,
|
||||
ca-certificates для проверки сертификатов и gpg для работы с ключами. Просто установим их (если они не установлены,
|
||||
то ничего не произойдет):
|
||||
```shell
|
||||
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
|
||||
```
|
||||
|
||||
На Orange Pi 5 Plus у меня не получилось установить GPG-ключ для Docker нормальным образом через команду (у вас, может, и получится):
|
||||
```shell
|
||||
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/docker-archive-keyring.gpg --keyserver keyserver.ubuntu.com --recv-keys 7EA0A9C3F273FCD8
|
||||
```
|
||||
|
||||
И потому я пошел другим путем. Скачал ключ с сайта Docker и установил его вручную:
|
||||
```shell
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
```
|
||||
|
||||
Проверим, что ключ сохранен:
|
||||
```shell
|
||||
ls -l /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
```
|
||||
|
||||
Исправим список репозиториев для Docker. Отроем на редактирование _docker.list_ командой:
|
||||
```shell
|
||||
sudo nano /etc/apt/sources.list.d/docker.list
|
||||
```
|
||||
|
||||
У меня рабочий вариант на Orange Pi 5 (с процессором ARM64) он выглядел так:
|
||||
```text
|
||||
deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu jammy stable
|
||||
```
|
||||
|
||||
А на стареньком Raspberry Pi 3 (с процессором 32-битным ARMv7) он выглядел так:
|
||||
```text
|
||||
deb [arch=armhf signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable
|
||||
```
|
||||
|
||||
Обновим список пакетов:
|
||||
```shell
|
||||
sudo apt-get update
|
||||
```
|
||||
|
||||
Теперь при обновлении списка пакетов не будет предупреждения о старом ключе GPG!
|
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--3x-ui-welcome.png
Normal file
After Width: | Height: | Size: 58 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 |
BIN
images/k3s--traefik-dashboard.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
images/orange--orange-config--avahi-installing.gif
Normal file
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 16 KiB |
BIN
images/orange--orange-config--system-settings--avahi-01.gif
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
images/orange--orange-config--system-settings--avahi-02.gif
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
images/orange--orange-config.gif
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
images/orange-pi--photo--with-emmc.webp
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
images/orange-pi--photo--with-ssd-nvme.webp
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
images/orange-pi--photo--without-emmc.webp
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
images/orange-pi--photo--without-ssd-nvme.webp
Normal file
After Width: | Height: | Size: 44 KiB |
687
kubernetes/k3s-3xui-pod.md
Normal file
@@ -0,0 +1,687 @@
|
||||
# Установка 3X-UI как под в K3s (Kubernetes)
|
||||
|
||||
3x-ui — это симпатичный веб-интерфейс для управления VPN-серверами, такими как WireGuard, Shadowsocks, Xray, V2Ray
|
||||
и тому подобное. Он позволяет настраивать и мониторить VPN-соединения и клиентов через браузер. Мы будем запускать
|
||||
его как контейнер (под) внутри K3s кластера на Orange Pi 5.
|
||||
|
||||
Мне нужен 3x-ui, для безопасного доступа к домашней сети из любой точки мира, а также для безопасного доступа
|
||||
к интернету через домашний сервер.
|
||||
|
||||
### Создание namespace (не обязательно)
|
||||
|
||||
Для удобства организации рекомендую создать отдельное пространство имён (`namespace`) для 3x-ui. Пространство имен --
|
||||
это способ организовать ресурсы в кластере. Оно работает как виртуальная "папка", которая помогает разделять
|
||||
(изолировать) и управлять объектами, такими как поды, сервисы, конфигурации и т.д. Объекты в одном _namespace_ не видят
|
||||
объекты из другого namespace (если не настроено обратное), что помогает избежать путаницы. Несколько приложений
|
||||
с одинаковыми именами могут без проблем существовать в разных пространствах имен. Кроме того, можно настроить
|
||||
права доступа (RBAC) отдельно для каждого namespace.
|
||||
|
||||
Выполним в терминале:
|
||||
```bash
|
||||
sudo kubectl create namespace x-ui
|
||||
```
|
||||
|
||||
Проверим, что пространство имён создано:
|
||||
```bash
|
||||
kubectl get namespaces
|
||||
```
|
||||
|
||||
Увидим x-ui в списке:
|
||||
```text
|
||||
NAME STATUS AGE
|
||||
... ... ...
|
||||
... ... ...
|
||||
x-ui Active 6s
|
||||
```
|
||||
|
||||
## Простое развёртывание 3X-UI в поде
|
||||
|
||||
Cоздадим манифест развертывания пода (этого YAML-файл с инструкциями для K3s, что и как запустить). Мы будем
|
||||
использовать SQLite как внутреннюю базу данных 3x-ui, и пока эта бызы будет храниться внутри пода. Позже сможем
|
||||
переключиться на `Longhorn` (опционально).
|
||||
|
||||
Создадим `deployment.yaml` в каталоге `~/k3s/vpn/x-ui/` (см. [структуру каталогов для хранения конфигураций и манифестов](k3s-shadowsocks-client.md)
|
||||
принятую в моем проекте):
|
||||
```bash
|
||||
mkdir -p ~/k3s/vpn/x-ui
|
||||
nano ~/k3s/vpn/x-ui/deployment.yaml
|
||||
```
|
||||
|
||||
Вставим в него следующий код:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: x-ui # имя развертывания (имя пода)
|
||||
namespace: x-ui # пространство имён, в котором будет создан под
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: x-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: x-ui
|
||||
spec:
|
||||
hostNetwork: true # использовать сетевой стек хоста
|
||||
containers:
|
||||
- name: x-ui # имя контейнера
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
# image: enwaiax/x-ui:latest # альтернативный облегчённый: меньше способов шифрования и китайский интерфейс
|
||||
```
|
||||
В этом манифесте примечательно следующее:
|
||||
- `hostNetwork: true` — позволяет контейнеру использовать сетевой стек хоста и значит работать
|
||||
с сетевыми интерфейсами и портами хоста напрямую. Это полезно для приложений, которые требуют прямого доступа
|
||||
к сети, например, VPN-серверы.
|
||||
- `spec.replicas: 1` — количество реплик (экземпляров) пода, которые будут запущены. В данном случае -- оин под.
|
||||
- `spec.selector` — селектор, который используется для выбора подов, которые будут управляться этим
|
||||
развертыванием. Он определяет, какие поды будут обновлены или удалены при изменении конфигурации развертывания.
|
||||
- `matchLabels` — метки, которые должны совпадать с метками подов, чтобы они были выбраны селектором.
|
||||
В данном случае мы используем метку `app: x-ui`, чтобы выбрать поды, которые относятся к приложению x-ui.
|
||||
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/vpn/x-ui/deployment.yaml
|
||||
```
|
||||
|
||||
Проверим, что под запустился:
|
||||
```bash
|
||||
sudo k3s kubectl get pods -n x-ui -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
x-ui-bb97f6894-h7zj8 1/1 Running 0 11s 10.42.1.50 opi5plus-3 <none> <none>
|
||||
|
||||
Видим, что нода на которой запустился 3x-ui это `opi5plus-3`, а имя пода `x-ui-bb97f6894-h7zj8`. Проверим логи пода,
|
||||
используя его имя:
|
||||
```bash
|
||||
sudo kubectl logs -n x-ui x-ui-bb97f6894-h7zj8
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
Server ready
|
||||
(0x291e4e8,0x40001657b0)
|
||||
2025/03/28 13:28:34 Starting x-ui 2.5.6
|
||||
(0x291e4e8,0x40001658e0)
|
||||
INFO - Web server running HTTP on [::]:2053
|
||||
INFO - XRAY: infra/conf/serial: Reading config: &{Name:bin/config.json Format:json}
|
||||
WARNING - XRAY: core: Xray 25.3.6 started
|
||||
```
|
||||
|
||||
Теперь мы знаем порт, на котором работает 3x-ui (`2053`), и значит можем получить доступ к веб-интерфейсу через браузер
|
||||
по адресу `http://opi5plus-3:2053` или `http://<IP_адрес_вашего_узла>:2053`.
|
||||
|
||||
<img src="../images/k3s--3x-ui-welcome.png" alt="3x-ui welcome page (RU)" width="70%" />
|
||||
|
||||
После первого логирования (по умолчанию логин и пароль `admin`/`admin`) можно настаивать VPN-подключения, создавать
|
||||
пользователей, менять логин и пароль на вход и т.д. Веб-интерфейс 3x-ui интуитивно понятен, так что разбираться
|
||||
не составит труда.
|
||||
|
||||
## Развертывание Kubernetes пода 3x-ui с постоянным хранилищем (PVC)
|
||||
|
||||
Есть, конечно, у 3x-ui под k3s минусы. В частности, внутри пода (`sudo kubectl exec -it -n x-ui x-ui-... -- /bin/sh`)
|
||||
не будет работать командный интерфейс 3x-ui (`x-ui admin`). Поды k3s работают на **Alpine**, а там некоторые команды
|
||||
отличаются (например, нет `bash`, а только `ash`). Но web-панель работает как положено, и всё управление удобнее
|
||||
делать через веб-интерфейс, так что лезть в консоль подов не обязательно.
|
||||
|
||||
Но есть ещё другой минус, более критичный. При рестарте пода, все настройки будут сброшены, так как они хранятся
|
||||
во внутреннем хранилище пода, а при остановке пода хранилище удаляется.
|
||||
|
||||
Чтобы этого избежать нужно использовать постоянное хранилище (Persistent Volume). Для его работы требуется установить
|
||||
`Longhorn` (или другой менеджер хранилищ). K3s поддерживает `Longhorn` из коробки, так как в операционной системе на
|
||||
Orange Pi 5 нет поддержки `iSCSI`, включение его потребует компиляции ядра (если вы этого еще не сделали, [смотрите
|
||||
инструкцию](../raspberry-and-orange-pi/opi5plus-rebuilding-linux-kernel-for-iscsi.md).
|
||||
|
||||
Если `Longhorn` уже установлен, создадим не его базе постоянное хранилище для -- _PersistentVolumeClaim_ (**PVC**).
|
||||
Манифест PVC создадим в каталоге `~/k3s/vpn/x-ui/`, рядом с `deployment.yaml`:
|
||||
```bash
|
||||
nano ~/k3s/vpn/x-ui/pvc-db.yaml
|
||||
```
|
||||
|
||||
Вставим в него следующий код:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: x-ui-db-pvc
|
||||
namespace: x-ui
|
||||
spec:
|
||||
storageClassName: longhorn # Указываем Longhorn как класс хранилища
|
||||
accessModes:
|
||||
- ReadWriteOnce # Доступ для чтения и записи одним подом
|
||||
resources:
|
||||
requests:
|
||||
storage: 512Mi # Запрашиваемое хранилище, размер можно увеличить, если нужно
|
||||
```
|
||||
|
||||
Обратите внимание:
|
||||
- `metadata.name` и `metadata.namespace` — имя хранилища (и это имя мы должны использовать в манифесте
|
||||
развертывания пода, чтобы указать, какое хранилище использовать) и пространство имён, в котором оно будет создано.
|
||||
- `spec.storageClassName` — класс хранилища, который будет использоваться для создания постоянного хранилища.
|
||||
В данном случае -- `longhorn`.
|
||||
- `spec.accessModes` — режим доступа к хранилищу. `ReadWriteOnce` означает, что хранилище может быть смонтировано
|
||||
только одним подом для чтения и записи. У нас один под и база на SQLite, так что этого достаточно.
|
||||
- `spec.resources.requests.storage` — запрашиваемый размер хранилища. Мы запрашиваем 1 ГБ и не означает, что
|
||||
хранилище будет занимать 1 ГБ на диске. Это предельный размер, который сможет занять хранилище.
|
||||
|
||||
Применим pvc-манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/vpn/x-ui/pvc-db.yaml
|
||||
```
|
||||
|
||||
После этого Longhorn создаст том, который будет привязан к этому PVC.
|
||||
|
||||
Теперь нам нужно изменить манифест развертывания пода, и подключить к нему созданный PVC. Теперь наш
|
||||
`~/k3s/vpn/x-ui/deployment.yaml` будет выглядеть так:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: x-ui
|
||||
namespace: x-ui
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: x-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: x-ui
|
||||
spec:
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: x-ui
|
||||
image: ghcr.io/mhsanaei/3x-ui:latest
|
||||
# image: enwaiax/x-ui:latest # альтернативный облегчённый: меньше способов шифрования и китайский интерфейс
|
||||
volumeMounts:
|
||||
- name: db-storage # Имя тома, в который будет смонтирован...
|
||||
mountPath: /etc/x-ui # ...в путь к базе данных внутри контейнера
|
||||
volumes:
|
||||
- name: db-storage # Имя тома, которое...
|
||||
persistentVolumeClaim: # ...должно быть постоянным хранилищем
|
||||
claimName: x-ui-db-pvc # ...и размещаться в PVC с именем 'x-ui-db-pvc'
|
||||
```
|
||||
|
||||
Применим обновлённый манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/vpn/x-ui/deployment.yaml
|
||||
```
|
||||
|
||||
Под перезапустится, и теперь база данных будет храниться в постоянном хранилище Longhorn. При перезапуске пода или его
|
||||
"переезде" на другой узел, база данных останется доступной и не потеряется. Следует отметить, что при сбое узла
|
||||
процесс перемещения пода занимает некоторое время. В кластере на Orange Pi 5, где проверки связности не очень
|
||||
агрессивные, это может занять до 5 минут. В общем, это нормально.
|
||||
|
||||
## Единая точка входа VPN-соединений через под 3x-ui
|
||||
|
||||
Под с 3x-ui может быть запущен k3s на произвольном узле, и может быть произвольно перемещён в кластере на другой узел.
|
||||
Таким образом, если мы хотим предоставить доступ к VPN-соединениям из интернета, нам нужно настроить доступ через
|
||||
единый IP-адрес. Это можно сделать несколькими способами.
|
||||
|
||||
### Доступ через VIP (виртуальный IP) c перенаправлял трафика через Keepalived на узел с подом с 3x-ui
|
||||
|
||||
При [развертывании k3s](../raspberry-and-orange-pi/k3s.md) на Orange Pi 5 Plus мы уже настраивали Keepalived. Теперь
|
||||
надо настроить его так, чтобы узел с подом 3x-ui получал больший приоритет в Keepalived, и тогда виртуальный IP
|
||||
будет получать трафик с этого узла.
|
||||
|
||||
**Лучшим решением будет динамическая настройка приоритета в Keepalived.**
|
||||
|
||||
Лучший способ — настроить так, чтобы приоритет Keepalived ноды автоматически повышался, если под 3x-ui запущен на ней.
|
||||
Это можно сделать с помощью механизма `track_script`, который будет проверять наличие пода и динамически менять
|
||||
приоритет. Такой подход сохранит текущую работу K3s API и [подов Shadowsocks](k3s-shadowsocks-client.md), добавив
|
||||
поддержку 3x-ui.
|
||||
|
||||
Создадим проверочный скрипт (на каждом узле), который будет проверять наличие пода 3x-ui. Скрипт будет
|
||||
расположен в `~/scripts/check_xui.sh`:
|
||||
```bash
|
||||
mkdir -p ~/scripts
|
||||
nano ~/scripts/check_xui.sh
|
||||
```
|
||||
|
||||
И вставим в него следующий код (на каждой ноде):
|
||||
```bash
|
||||
#!/usr/bin/bash
|
||||
NODE_NAME=$(hostname) # Получаем имя текущей ноды
|
||||
POD_NAME=$(kubectl get pods -n x-ui -o jsonpath="{.items[?(@.spec.nodeName=='$NODE_NAME')].metadata.name}")
|
||||
if [ -n "$POD_NAME" ]; then
|
||||
exit 0 # Под есть на этой ноде
|
||||
else
|
||||
exit 1 # Пода нет
|
||||
fi
|
||||
```
|
||||
|
||||
Скрипт использует `kubectl`, чтобы проверить, есть ли под `3x-ui` в _namespace_ `x-ui` на текущей ноде. Использование
|
||||
`sudo` не требуется, так как скрипт будет запускаться `keepalived`, который работает от `root`.
|
||||
Убедись, что kubectl доступен на всех нодах и настроен для работы с кластером (например, через kubeconfig).
|
||||
|
||||
Сделаем скрипт исполняемым (на каждой ноде):
|
||||
```bash
|
||||
sudo chmod +x ~/scripts/check_xui.sh
|
||||
```
|
||||
|
||||
Обновим конфиг `reepalived`, добавив `vrrp_script` и привязку к нему через `track_script`. Теперь мы переведем все
|
||||
ноды в **BACKUP** (чтобы избежать конфликтов), а приоритет будет динамически меняться в зависимости от наличия пода.
|
||||
|
||||
На перовой мастер-ноде:
|
||||
```bash
|
||||
sudo nano /etc/keepalived/keepalived.conf
|
||||
```
|
||||
|
||||
И теперь там будет вот такой конфиг (не забудь указать правильное имя пользователя `<user>` в пути к скрипту):
|
||||
```pycon
|
||||
vrrp_script check_xui {
|
||||
script "/home/<user>/scripts/check_xui.sh"
|
||||
interval 2 # Проверять каждые 2 секунды
|
||||
weight 50 # Добавить 50 к приоритету, если под есть
|
||||
}
|
||||
|
||||
vrrp_instance VI_1 {
|
||||
# state MASTER
|
||||
state BACKUP # Все ноды стартуют как BACKUP
|
||||
interface enP4p65s0
|
||||
virtual_router_id 51
|
||||
priority 100 # Базовый приоритет
|
||||
advert_int 1
|
||||
unicast_src_ip 192.168.1.26
|
||||
unicast_peer {
|
||||
192.168.1.27
|
||||
192.168.1.28
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.1.200
|
||||
}
|
||||
track_script {
|
||||
check_xui # Привязка к скрипту
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Перезапустим Keepalived:
|
||||
```bash
|
||||
sudo service keepalived restart
|
||||
```
|
||||
|
||||
Аналогичным образом настроим конфиги на других узлах (добавить блок `vrrp_script` сверху, и добавить `track_script` в
|
||||
`vrrp_instance`). Не забудь указать проверить `unicast_src_ip` для каждой ноды и перезапустить Keepalived на всех узлах.
|
||||
|
||||
Теперь на каждой ноде cкрипт `~/scripts/check_xui.sh` проверяет наличие пода `x-ui` каждые 2 секунды. Если под есть,
|
||||
Keepalived добавляет 50 к базовому приоритету ноды (например, 100 → 150). Если пода нет, приоритет остаётся базовым
|
||||
(100, 90 или 80). Нода с наивысшим приоритетом становится MASTER и получает виртуальный IP. Таким образом, VIP всегда
|
||||
будет указывать на ноду с подом 3x-ui.
|
||||
|
||||
Теперь панель 3x-ui будет доступна с виртуального IP (192.168.1.200). Все VPN-соединения будут работать через него.
|
||||
Так что если на домашнем роутере настроить перенаправление портов (для 2053-порта веб-панели 3x-ui, и портов которые
|
||||
будем выбирать для VPN-соединений), то можно будет подключаться к 3x-ui и VPN-соединениям из любой точки мира.
|
||||
|
||||
|
||||
|
||||
### Доступ через Ingress Controller по имени домена (http).
|
||||
|
||||
Сейчас web-панель 3x-ui доступна через VIP по порту `2053` по http. _В принципе, так можно и оставить_. Но если мы хотим
|
||||
иметь доступ по https, да еще чтобы это работало через доменное имя, и чтобы k3s автоматически получал и обновлял
|
||||
сертификаты, то можно использовать Ingress-контроллер. Он будет брать трафик с порта VIP, по порту `2055`, через
|
||||
балансировщик svclb-traefik направлять его на Ingress-контроллер Traefik и перенаправлять его на под с 3x-ui (тоже
|
||||
через VIP но уже по порту `2053`). Дополнительно, есть [заметка про настройку Traefik в качестве прокси](k3s-proxy.md).
|
||||
|
||||
#### Манифест для Ingress-контроллера Traefik
|
||||
|
||||
По умолчанию Ingress-контроллер Traefik в k3s слушает на портах 80 и 443 (HTTP и HTTPS) и перенаправляет трафик
|
||||
на соответствующие поды. В моем случае порты 80 и 443 на моем роутере уже перенаправляются на другой хост.
|
||||
В будущем я это, возможно, изменю, и сейчас я не могу перенаправить эти порты на VIP. Поэтому мне нужно настроить
|
||||
Traefik так, чтобы он слушал http/https на другом порту (например, 2055, и порт, обратите внимание, стандартный
|
||||
443-порт от продолжит слушать как и раньше) и перенаправлял трафик на под с 3x-ui (это только для http/https, то есть
|
||||
для доступа в веб-интерфейсу 3x-ui, а не для VPN-соединений). Этот манифест задаёт глобальную конфигурацию Traefik
|
||||
для всего кластера, а не только к 3x-ui, и потому лучше положить его в "общую" папку для Traefik, например:
|
||||
`~/k3s/traefik/traefik-config.yaml`:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/k3s/traefik
|
||||
nano ~/k3s/traefik/traefik-config.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующий код:
|
||||
```yaml
|
||||
apiVersion: helm.cattle.io/v1
|
||||
kind: HelmChartConfig
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: kube-system
|
||||
spec:
|
||||
valuesContent: |-
|
||||
additionalArguments:
|
||||
- --entrypoints.web-custom.address=:2055 # Слушаем HTTP на 2055
|
||||
- --log.level=DEBUG
|
||||
```
|
||||
Что тут происходит: Для изменения настройки Traefik, создаётся HelmChartConfig (этот такой аналог пакетного менеджера
|
||||
для Kubernetes, который позволяет управлять приложениями и сервисами в кластере). Этот манифест указывает Traefik,
|
||||
в пространство имён `kube-system`, а аргумент `--entrypoints.web-custom.address=:2055` в конфигурацию -- инструкция:
|
||||
_Слушай порт 2055 и назови эту точку входа **web-custom**_). После применения Traefik начнёт принимать запросы на порту
|
||||
2055. Поскольку мой роутер пробрасывает 2055 на VIP-адрес (тоже 2055 порт), Traefik на ноде с VIP увидит этот трафик.
|
||||
|
||||
Применим манифест:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/traefik/traefik-config.yaml
|
||||
```
|
||||
|
||||
Теперь Traefik будет слушать http еще и на порту 2055.
|
||||
|
||||
#### Манифест для маршрутизации трафика на под с 3x-ui через Ingress-контроллер
|
||||
|
||||
Теперь нужно сказать Traefik, что запросы на домен `v.home.cube2.ru` через порт `2055` — это HTTP, и их надо
|
||||
перенаправить на порт 2053, где работает 3x-ui. Для этого в каталоге с манифестами 3x-ui `~/k3s/vpn/x-ui/`
|
||||
(ведь это касается подa с 3x-ui) создадим манифест IngressRoute:
|
||||
```bash
|
||||
nano ~/k3s/vpn/x-ui/ingressroute.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующий код (не забудь указать свой домен):
|
||||
```yaml
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: x-ui-ingress
|
||||
namespace: x-ui
|
||||
spec:
|
||||
entryPoints:
|
||||
- web-custom # ендпоинт, который "слушает" порт 2055
|
||||
routes:
|
||||
- match: Host("v.home.cube2.ru")
|
||||
kind: Rule
|
||||
services:
|
||||
- name: x-ui-external # имя сервиса, на который будет перенаправлен трафик
|
||||
port: 2053 # порт, на который будет перенаправлен трафик
|
||||
```
|
||||
|
||||
Что тут происходит? Мы создаём объект `IngressRoute`, который определяет маршрут для входящего трафика. Параметры:
|
||||
- `kind` — тип объекта, который мы создаём. В данном случае это `IngressRoute`, который используется для
|
||||
маршрутизации трафика в Traefik.
|
||||
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-ingress` и
|
||||
пространство имён `x-ui`, в котором будет создан объект (то же пространство, что и у пода с 3x-ui).
|
||||
- `entryPoints` — точка входа, которая будет использоваться для маршрутизации трафика. В данном случае это `web-custom`,
|
||||
который мы настроили в предыдущем шаге.
|
||||
- `routes` — определяет правила маршрутизации. В данном случае мы указываем, что если запрос приходит на домен
|
||||
`v.home.cube2.ru` (`match` — условие, которое должно быть выполнено для маршрутизации), то он будет перенаправлен
|
||||
на сервис `x-ui-external` (который мы создадим ниже) на порт `2053`.
|
||||
|
||||
Теперь создадим сервис `x-ui-external`, который будет использоваться для маршрутизации трафика на под с 3x-ui.
|
||||
|
||||
Создадим манифест сервиса в каталоге `~/k3s/vpn/x-ui/`:
|
||||
```bash
|
||||
nano ~/k3s/vpn/x-ui/x-ui-service.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующий код:
|
||||
```yaml
|
||||
# Service для 3x-ui с hostNetwork: true, использующего VIP 192.168.1.200
|
||||
apiVersion: v1
|
||||
kind: Service # Тип объекта, который мы создаём. В данном случае это Service
|
||||
metadata:
|
||||
name: x-ui-external
|
||||
namespace: x-ui
|
||||
spec:
|
||||
ports:
|
||||
- port: 2053
|
||||
targetPort: 2053
|
||||
protocol: TCP
|
||||
---
|
||||
# Endpoints указывает на VIP, так как под не в сетевом пространстве Kubernetes
|
||||
apiVersion: v1
|
||||
kind: Endpoints # Тип объекта, который мы создаём. В данном случае это Endpoints
|
||||
metadata:
|
||||
name: x-ui-external
|
||||
namespace: x-ui
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.1.200 # IP-адрес (VIP), на который будет перенаправлен трафик
|
||||
ports:
|
||||
- port: 2053
|
||||
protocol: TCP
|
||||
```
|
||||
|
||||
Что тут происходит? Мы создаём два объекта: `Service` и `Endpoints`. `Service` — это абстракция, которая предоставляет
|
||||
единый IP-адрес и DNS-имя для доступа к группе подов. `Endpoints` — это объект, который указывает конечные точки
|
||||
для перенаправления трафика. В нашем случае это VIP:2053, так как под 3x-ui использует `hostNetwork: true`
|
||||
и недоступен через внутренние IP Kubernetes. Но обычно `Endpoints` указывают на имена подов, на которые отправляется
|
||||
трафик.
|
||||
|
||||
Для `Service` мы указываем:
|
||||
- `kind` — тип объекта, который мы создаём. В данном случае это `Service`.
|
||||
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-external` и
|
||||
пространство имён `x-ui`, в котором будет создан объект (то же пространство, что и у пода с 3x-ui).
|
||||
- `spec` — спецификация объекта, которая определяет его поведение. Мы указываем, что сервис будет слушать внешний трафик
|
||||
на порту `2053` и перенаправлять на тот же порт внутри кластера.
|
||||
- `ports` — определяет порты, на которых будет слушать сервис. Мы указываем, что сервис будет слушать
|
||||
на порту `2053` и перенаправлять трафик на тот же порт внутри кластера, и будем использоваться TCP.
|
||||
|
||||
Для `Endpoints` мы указываем:
|
||||
- `kind` — тип объекта, который мы создаём. В данном случае это `Endpoints`.
|
||||
- `metadata` — метаданные объекта, такие как имя и пространство имён. Мы указываем имя `x-ui-external` (то же,
|
||||
что и у сервиса) и пространство имён `x-ui` (то же, что и у пода с 3x-ui).
|
||||
- `subsets` — подмножество конечных точек, которые будут использоваться для маршрутизации трафика. Мы указываем, что
|
||||
в подмножестве есть одна конечная точка с IP 192.168.1.200 и портом 2053 (TCP).
|
||||
|
||||
Применим манифесты:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/vpn/x-ui/ingressroute.yaml
|
||||
sudo kubectl apply -f ~/k3s/vpn/x-ui/x-ui-service.yaml
|
||||
```
|
||||
|
||||
Перезагрузим Traefik, чтобы он увидел изменения:
|
||||
```bash
|
||||
kubectl rollout restart deployment traefik -n kube-system
|
||||
```
|
||||
|
||||
Или для надёжности вовсе удалим поды с traefik и svclb-traefik, тогда они должны создастся заново, и гарантированно
|
||||
примут новые настройки:
|
||||
```bash
|
||||
kubectl delete pod -n kube-system -l app.kubernetes.io/name=traefik
|
||||
kubectl delete pod -n kube-system -l svccontroller.k3s.cattle.io/svcname=traefik
|
||||
```
|
||||
|
||||
Проверим, что поды создались и запустились:
|
||||
```bash
|
||||
sudo kubectl get pods -n kube-system -o wide | grep traefik
|
||||
```
|
||||
|
||||
Увидим что-то вроде (поды стартовали недавно):
|
||||
```text
|
||||
helm-install-traefik-c4vlp 0/1 Completed 0 148m 10.42.0.84 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-8pfdg 4/4 Running 0 4m 10.42.2.62 opi5plus-1 <none> <none>
|
||||
svclb-traefik-4f8c2580-9tldj 4/4 Running 0 4m 10.42.1.93 opi5plus-3 <none> <none>
|
||||
svclb-traefik-4f8c2580-pmbqj 4/4 Running 0 4m 10.42.0.83 opi5plus-2 <none> <none>
|
||||
traefik-5db7d4fd45-45gj6 1/1 Running 0 4m 10.42.0.82 opi5plus-2 <none> <none>
|
||||
```
|
||||
|
||||
Проверим, что сервисы создались и запустились:
|
||||
```bash
|
||||
sudo kubectl get svc -n kube-system -o wide | grep traefik
|
||||
```
|
||||
|
||||
Увидим что-то вроде (есть обработка через порт 2055:ххххх):
|
||||
```text
|
||||
traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.168.1.28 80:31941/TCP,443:30329/TCP,9000:32185/TCP,2055:32627/TCP 53d app.kubernetes.io/instance=traefik-kube-system,app.kubernetes.io/name=traefik
|
||||
```
|
||||
|
||||
Проверим, что созданный сервис `x-ui-external` доступен:
|
||||
```bash
|
||||
sudo kubectl get svc -n x-ui -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде (сервис создан и слушает на порту 2053):
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
|
||||
x-ui-external ClusterIP 10.43.73.106 <none> 2053/TCP 2h <none>
|
||||
```
|
||||
|
||||
Проверим, что созданный IngressRoute доступен:
|
||||
```bash
|
||||
sudo kubectl get ingressroutes -n x-ui -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде (IngressRoute создан):
|
||||
```text
|
||||
NAME AGE
|
||||
x-ui-ingress 14h
|
||||
```
|
||||
|
||||
Проверим логи Traefik (не зря же мы включали отладку в манифесте)
|
||||
```bash
|
||||
kubectl get pods -n kube-system | grep traefik
|
||||
sudo kubectl logs -n kube-system traefik-<hash> --since=5m
|
||||
```
|
||||
Ищем: `"web-custom": {"address": ":2055"}` и маршрут `x-ui-x-ui-ingress` с `Host("v.home.cube2.ru")`,
|
||||
|
||||
И наконец, проверим, что под с 3x-ui доступен по нашему доменному на порту 2055 через VIP-адрес (возможно, придется
|
||||
сделать запись в `/etc/hosts`, если ваш роутер не может разрешить внешний домен внутрь домашней сети, и поставить
|
||||
в соответствие домен и VIP):
|
||||
```bash
|
||||
curl -v http://v.home.cube2.ru:2055
|
||||
```
|
||||
|
||||
**Все заработало**, мы видим, что запросы на домен `v.home.cube2.ru` через порт `2055` перенаправляются на под с 3x-ui
|
||||
|
||||
Если не получилось, то можно дополнительно проверить, что с сервисом `traefik` всё в порядке. Посмотрим его текущие
|
||||
настройки:
|
||||
```bash
|
||||
sudo kubectl get service -n kube-system traefik -o yaml
|
||||
```
|
||||
|
||||
Мы должны увидеть в блоке `spec:ports` что-то типа:
|
||||
```yaml
|
||||
- name: web-custom
|
||||
nodePort: тут-будет-номер-порта-внутри-балансировщика
|
||||
port: 2055
|
||||
protocol: TCP
|
||||
targetPort: 2055
|
||||
```
|
||||
|
||||
Если блока нет, добавьте его через редактор (по умолчанию откроется `vim`, используйте `:wq` для сохранения и выхода):
|
||||
```bash
|
||||
sudo kubectl edit service -n kube-system traefik -o yaml
|
||||
```
|
||||
|
||||
Найти в `spec:ports` блок:
|
||||
```yaml
|
||||
- name: web
|
||||
nodePort: 31941
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: web
|
||||
- name: websecure
|
||||
nodePort: 30329
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: websecure
|
||||
```
|
||||
|
||||
И добавить под ним новый блок:
|
||||
```yaml
|
||||
- name: web-custom
|
||||
port: 2055
|
||||
protocol: TCP
|
||||
targetPort: 2055
|
||||
```
|
||||
|
||||
После сохранения изменений и выхода из редактора, сервис будет обновлён автоматически. Можно проверить, что ему присвоен
|
||||
новый номер порта внутри балансировщика (см. выше) и возможно все заработает. Но скорее всего придется удалить манифесты
|
||||
`ingressroute.yaml`, `x-ui-service.yaml` и все настраивать заново, проверять логи и т.д.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Доступ через Ingress Controller c https и перенаправлением трафика на узел с подом с 3x-ui
|
||||
|
||||
Установим Cert-Manager для автоматического получения сертификатов Let's Encrypt. Это позволит нам использовать
|
||||
HTTPS для доступа к 3x-ui (и другим подам). Cert-Manager автоматически обновляет сертификаты, когда они истекают.
|
||||
```bash
|
||||
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
|
||||
```
|
||||
|
||||
В результате у нас появится три новых пода в пространстве имён `cert-manager`:
|
||||
```bash
|
||||
sudo k3s kubectl get pods -n cert-manager -o wide
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
cert-manager-64478b89d5-p4msl 1/1 Running 0 8m36s 10.42.1.55 opi5plus-3 <none> <none>
|
||||
cert-manager-cainjector-65559df4ff-t7rj4 1/1 Running 0 8m36s 10.42.1.54 opi5plus-3 <none> <none>
|
||||
cert-manager-webhook-544c988c49-zxdxc 1/1 Running 0 8m36s 10.42.1.56 opi5plus-3 <none> <none>
|
||||
```
|
||||
|
||||
Cert-Manager состоит из трёх основных компонентов, каждый из которых запускается в своём поде:
|
||||
* `cert-manager` -- основной контроллер. Он следит за ресурсами вроде Certificate и Issuer, запрашивает сертификаты
|
||||
у провайдеров (например, Let’s Encrypt) и обновляет их при необходимости.
|
||||
* `cert-manager-cainjector` -- внедряет CA (Certificate Authority) в вебхуки и другие ресурсы Kubernetes, чтобы
|
||||
они могли доверять сертификатам, выданным Cert-Manager.
|
||||
* `cert-manager-webhook` -- отвечает за валидацию и мутацию запросов на создание или обновление ресурсов, связанных
|
||||
с сертификатами. Он проверяет их на соответствие правилам.
|
||||
|
||||
#### Манифест для ClusterIssuer
|
||||
|
||||
Создадим манифест ClusterIssuer (эмитент кластера) для Cert-Manager и относится ко всему кластеру. В нем описываются
|
||||
правила для получения сертификатов от внешнего поставщика (в нашем случае Let's Encrypt) и укажем твм адрес сервера
|
||||
ACME, email для уведомлений, способы подтверждения владения доменом (например, через HTTP-01 или DNS-01).
|
||||
|
||||
```bash
|
||||
mkdir ~/k3s/cert-manager
|
||||
nano ~/k3s/cert-manager/clusterissuer.yaml
|
||||
```
|
||||
|
||||
И вставим в него следующий код (не забудь указать свой email):
|
||||
```yaml
|
||||
piVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: ваш@емейл.где-то
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik # Ingress-контроллер, например, traefik
|
||||
```
|
||||
|
||||
Применим манифест, чтобы cert-manager принял конфигурацию:
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/cert-manager/clusterissuer.yaml
|
||||
```
|
||||
|
||||
Это будет работать для всего кластера и для всех подов (текущих и будущих). Cert-Manager будет автоматически
|
||||
запрашивать и обновлять сертификаты. Let's Encrypt при проверке прав владения доменом посредством правила HTTP-01
|
||||
использует http (порт 80) и нужно настроить в роутере перенаправление трафика на кластер (лучше через VIP) для этого
|
||||
порта.
|
||||
|
||||
|
||||
|
||||
--------
|
||||
У меня развернут k3s-кластер из трех узлов (Orange Pi 5 Plus). Там есть под 3x-ui для внешнего доступа к VPN-соединениям.
|
||||
Под настроен как hostNetwork: true и использует `Longhorn` для хранения базы данных. На всех узлах настроен keepalived так,
|
||||
чтобы виртуальный IP-адрес (VIP) всегда указывал на узел с подом 3x-ui. Это позволяет поду ловить VPN-соединения из интернета
|
||||
(благодаря пробросу портов на роутере). Веб-интерфейс 3x-ui по порту 2053 тоже доступен по VIP-адресу (192.168.1.200) благодаря
|
||||
`hostNetwork: true` и виден из интернета по адресу `http://v.home.cube2.ru:2053`.
|
||||
|
||||
Я хочу настроить доступ к веб-интерфейсу 3x-ui через svclb-traefik по порту 2055 так, чтобы он стал доступен через VIP
|
||||
и по 2055 порту и будущем можно было настроить доступ по https.
|
||||
|
||||
|
||||
Кроме того у меня еще есть внешний хост с audiobookshelf по адресу http://192.168.1.15:8000 и я хотелбы настроить доступ
|
||||
к нему через svclb-traefik по адресу http://zvuk.cube2.ru:2055.
|
||||
|
||||
Как это сделать? Какие конфиги тебе нужны чтобы ты смог мне помочь?
|
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`. и в нем можно будет создавать и редактировать
|
||||
схемы, например:
|
||||
|
||||

|
343
kubernetes/k3s-custom-container-deployment.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# Развертывание пользовательского контейнера в k3s
|
||||
|
||||
|
||||
|
||||
|
||||
Теперь нам нужно передать образ контейнера на другие ноды кластера. Для этого сначала извлечём образ из k3s в tar-файл:
|
||||
```bash
|
||||
sudo docker save shadowsocks-with-tools:latest -o /tmp/shadowsocks.tar
|
||||
```
|
||||
|
||||
Затем передадим на вторую ноду, в нашем случае на хосте `192.168.1.28`, затем на третью (придется копировать через
|
||||
`sudo`, т.к. при создании образа в tar-файл мы тоже использовали `sudo`):
|
||||
```bash
|
||||
sudo scp /tmp/shadowsocks.tar opi@192.168.1.28:/tmp/
|
||||
```
|
||||
|
||||
Импортируем на второй ноде этот образ в k3s. Небезопасное использование пароля удаленного хоста в команде, требуется т.к.
|
||||
в команде через `ssh` не предоставляется интерактивный терминал, а SSH-ключи решают аутентификацию только для входа, но
|
||||
не для выполнения `sudo` на удалённой машине... c помощью ключа `-S` мы заставляем `sudo` га уделенном хсте
|
||||
читать пароль из `stdin`, и потому при введении пароля на текущем хосте он будет виден на экране... это выглядит опасно,
|
||||
но менее безопасно чем передать его с помощью `echo` в команде `echo 'your_password' |` добавив перед `sido -S k3s ...`.
|
||||
Так пароль, по крайне мере, не будет сохранён в `bash_history`, что было бы совсем, совсем не безопасно (а экран мы
|
||||
можем очистить после, выполнив команду `clear`):
|
||||
```bash
|
||||
ssh opi@192.168.1.28 "echo 'your_password' | sudo -S k3s ctr images import /tmp/shadowsocks.tar"
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
unpacking docker.io/library/shadowsocks-with-tools:latest (sha256:ae615618ce9d2aac7d3764ef735108452adf3fc30bb65f23f28c345798880c80)...done
|
||||
```
|
||||
|
||||
Удалим временный файл на второй ноде:
|
||||
```bash
|
||||
ssh opi@192.168.1.28 "rm /tmp/shadowsocks.tar"
|
||||
```
|
||||
|
||||
И убедимся, что образ появился в k3s на второй ноде (снова используем ключ `-S`):
|
||||
```bash
|
||||
ssh opi@192.168.1.28 "sudo -S k3s ctr images ls | grep shadowsocks"
|
||||
```
|
||||
|
||||
**Повторяем операцию для третьей ноды**. И можем удалить временный файл на первой ноде:
|
||||
```bash
|
||||
sudo rm /tmp/shadowsocks.tar
|
||||
```
|
||||
|
||||
|
||||
----
|
||||
Отлично, что всё получилось! Рад, что мы дошли до этого момента вместе. Твой вопрос про "секретные" контейнеры и необходимость публиковать их в Docker Hub — очень важный и актуальный, особенно для больших кластеров или чувствительных данных. Давай разберём это подробно, чтобы ты понял все варианты и мог выбрать тот, который подходит тебе.
|
||||
|
||||
Нужно ли выкладывать "секретный" контейнер в Docker Hub?
|
||||
Ты прав: если твой контейнер содержит секреты (например, ключи, пароли, защищённые алгоритмы), выкладывать его в публичный реестр вроде Docker Hub — не лучшая идея. Даже если сделать репозиторий приватным на Docker Hub, это всё равно означает передачу твоего образа третьей стороне, что может быть нежелательно с точки зрения безопасности или лицензирования.
|
||||
|
||||
Ответ: Нет, тебе не обязательно публиковать контейнер в Docker Hub. Есть несколько способов хранить и распространять образы "у себя", внутри твоей сети, без выхода в интернет. Давай разберём варианты.
|
||||
|
||||
Варианты хранения и использования контейнеров
|
||||
1. Локальное использование (как сейчас)
|
||||
Как это работает:
|
||||
Ты уже сделал это: собрал образ shadowsocks-with-tools локально и импортировал его в containerd на каждом узле через k3s ctr images import.
|
||||
В deployment.yaml указал imagePullPolicy: Never, чтобы k3s использовал только локальный образ.
|
||||
Плюсы:
|
||||
Никаких внешних реестров — всё остаётся на твоих машинах.
|
||||
Полный контроль над секретами.
|
||||
Минусы:
|
||||
Не масштабируется: нужно вручную копировать образ на каждый узел.
|
||||
Если узлов сотни, это становится непрактичным.
|
||||
Вывод: Подходит для небольшого кластера (как твои три мастера), но не для больших систем.
|
||||
|
||||
2. Приватный реестр Docker Hub
|
||||
Как это работает:
|
||||
Создаёшь приватный репозиторий на Docker Hub (бесплатно одно приватное репо, дальше платно).
|
||||
Выкладываешь образ:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo docker tag shadowsocks-with-tools yourusername/shadowsocks-with-tools:latest
|
||||
sudo docker push yourusername/shadowsocks-with-tools:latest
|
||||
В deployment.yaml:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
image: yourusername/shadowsocks-with-tools:latest
|
||||
Настраиваешь доступ через imagePullSecrets в k3s:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: dockerhub-credentials
|
||||
namespace: kube-system
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: <base64-encoded-docker-config>
|
||||
Где <base64-encoded-docker-config> — это закодированный JSON с твоими логином и паролем от Docker Hub:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
echo -n '{"auths":{"https://index.docker.io/v1/":{"username":"yourusername","password":"yourpassword"}}}' | base64
|
||||
Добавь в Deployment:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: dockerhub-credentials
|
||||
Плюсы:
|
||||
k3s сам скачивает образ на всех узлах.
|
||||
Репозиторий приватный, доступ только по ключам.
|
||||
Минусы:
|
||||
Образ всё равно уходит в облако.
|
||||
Ограничения бесплатного плана (1 приватное репо).
|
||||
Вывод: Подходит, если доверяешь Docker Hub, но не идеально для "секретов".
|
||||
|
||||
3. Локальный приватный реестр в твоей сети
|
||||
Как это работает:
|
||||
Разворачиваешь свой собственный реестр на одном из серверов (например, opi5plus-3, 192.168.1.26).
|
||||
Используешь официальный образ registry:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2
|
||||
Тегни и выложи свой образ:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo docker tag shadowsocks-with-tools:latest 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
sudo docker push 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
В deployment.yaml:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
image: 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
Безопасность (опционально):
|
||||
Добавь TLS и аутентификацию:
|
||||
Сгенерируй сертификаты:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
mkdir -p /etc/docker/certs
|
||||
openssl req -newkey rsa:4096 -nodes -sha256 -keyout /etc/docker/certs/registry.key -x509 -days 365 -out /etc/docker/certs/registry.crt
|
||||
Запусти реестр с TLS:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo docker run -d -p 5000:5000 --restart=always --name registry \
|
||||
-v /etc/docker/certs:/certs \
|
||||
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
|
||||
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
|
||||
registry:2
|
||||
Настрой аутентификацию через htpasswd:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo apt install apache2-utils
|
||||
mkdir -p /etc/docker/auth
|
||||
htpasswd -Bc /etc/docker/auth/htpasswd username password
|
||||
docker run -d -p 5000:5000 --restart=always --name registry \
|
||||
-v /etc/docker/certs:/certs \
|
||||
-v /etc/docker/auth:/auth \
|
||||
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \
|
||||
-e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \
|
||||
-e REGISTRY_AUTH=htpasswd \
|
||||
-e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
|
||||
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
|
||||
registry:2
|
||||
Создай Secret для k3s:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: registry-credentials
|
||||
namespace: kube-system
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: <base64-encoded-config>
|
||||
Где <base64-encoded-config>:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
echo -n '{"auths":{"192.168.1.26:5000":{"username":"username","password":"password"}}}' | base64
|
||||
В deployment.yaml:
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-credentials
|
||||
Плюсы:
|
||||
Образ остаётся в твоей сети.
|
||||
Полный контроль над доступом.
|
||||
k3s сам скачивает образ на узлах.
|
||||
Минусы:
|
||||
Нужно настроить реестр и поддерживать его.
|
||||
Вывод: Идеально для "секретных" контейнеров в большом кластере.
|
||||
|
||||
4. Другие приватные реестры (Harbor, Nexus, etc.)
|
||||
Если кластер большой, можешь развернуть более мощный реестр, например:
|
||||
Harbor: Open-source, с UI, ролями и интеграцией с Kubernetes.
|
||||
Nexus Repository: Поддерживает Docker и другие форматы.
|
||||
Установка сложнее, но для сотен узлов это оправдано.
|
||||
Ответ на твой вопрос
|
||||
Можно ли хранить контейнеры у себя? Да, можно! Локальный реестр (Вариант 3) — это то, что тебе нужно. Ты разворачиваешь его на своём сервере (например, 192.168.1.26:5000), загружаешь туда образы, и k3s тянет их оттуда. Никакого Docker Hub, всё под твоим контролем.
|
||||
Секреты: Они остаются в твоей сети, и доступ ограничивается твоими правилами (TLS, пароли).
|
||||
Для твоего случая
|
||||
Сейчас у тебя 3 узла, и локальный импорт работает. Но если кластер вырастет, настрой локальный реестр:
|
||||
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo docker run -d -p 5000:5000 --restart=always --name registry registry:2
|
||||
sudo docker tag shadowsocks-with-tools:latest 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
sudo docker push 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
В deployment.yaml:
|
||||
|
||||
yaml
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
image: 192.168.1.26:5000/shadowsocks-with-tools:latest
|
||||
Проверка Shadowsocks
|
||||
Теперь, когда под работает, давай проверим Shadowsocks:
|
||||
|
||||
Зайди в под:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo k3s kubectl exec -it -n kube-system shadowsocks-client-stockholm-<hash> -- sh
|
||||
TCP:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
curl --socks5 127.0.0.1:1081 http://ifconfig.me
|
||||
Ожидаемый результат: <VPS_IP>.
|
||||
UDP:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
nc -u -l 12345
|
||||
С VPS:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
echo "test" | nc -u v-panel.sw.cube2.ru 12345
|
||||
Логи:
|
||||
bash
|
||||
|
||||
Collapse
|
||||
|
||||
Wrap
|
||||
|
||||
Copy
|
||||
sudo k3s kubectl logs -n kube-system -l app=shadowsocks-client-stockholm
|
||||
Итог
|
||||
"Секретные" контейнеры не нужно выкладывать в Docker Hub — храни их в локальном реестре.
|
||||
Для проверки Shadowsocks используй команды выше.
|
||||
Напиши:
|
||||
|
||||
<PUBLIC_IP>, <VPS_IP>, имя пользователя.
|
||||
Результат проверки (curl, nc, логи).
|
||||
Ты круто всё освоил — теперь ты готов к любым кластерам! Я с тобой!
|
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
358
kubernetes/k3s-proxy.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Проксирование внешнего хоста через Traefik (Ingress-контроллер)
|
||||
|
||||
У меня в домашней сети есть хост с web-сервисом (audiobookshelf), а стандартные web-порты (80 для HTTP и 443 для HTTPS)
|
||||
на домашнем роутере перенаправлены в кластер k3S (через keepalived). Таким образом, если прокинуть http-трафик этого
|
||||
хоста через Traefik, то можно будет получить доступ к этому сервису через доменное имя и SSL-сертификат от
|
||||
Let’s Encrypt.
|
||||
|
||||
Для удобства я поместил все манифесты в один файл (вы можете оформить из и как отдельные файлы). Так как хотелось бы
|
||||
описать как делаются универсальные манифесты, которые можно использовать для проксирования любого сервиса, то
|
||||
я заменил в нем конкретные значения на "заглушки. Можно взять этот манифест и просто заменить в нем значения на
|
||||
свои:
|
||||
|
||||
* `<PROXIED-HOST>` -- IP-адрес хоста, где работает сервис, который надо проксировать.
|
||||
* `<PROXIED-PORT>` -- порт, с которого отвечает сервис.
|
||||
* `<YOU-DOMAIN-NAME>` -- доменное имя, на которое будет проксировать сервис.
|
||||
* `<NAME-SPACE>` -- пространство имен кластера, в котором будет создан сервис, маршруты, секреты и все необходимое
|
||||
для проксирования. Пространство имен -- это логическая группа ресурсов в кластере Kubernetes, которая позволяет
|
||||
организовать и изолировать ресурсы.
|
||||
* `<SERVICE-NAME>` -- имя сервиса, который будет проксироваться. Это имя, для простоты, будем использоваться
|
||||
* и в маршрутах, и сертификатах, и в секрете...
|
||||
|
||||
## Пространство имен
|
||||
|
||||
Чтобы все было аккуратно и сервисы и поды не путались, создадим пространство имен для проксирования конкретного хоста.
|
||||
Мо хост относится к <SERVICE-NAME>, поэтому назову пространство имен `<NAME-SPACE>`
|
||||
Например, `<NAME-SPACE>`.
|
||||
```bash
|
||||
sudo ubectl create namespace <NAME-SPACE>
|
||||
```
|
||||
|
||||
Проверяем, что пространство создано:
|
||||
```bash
|
||||
sudo kubectl get namespace <NAME-SPACE>
|
||||
```
|
||||
|
||||
Увидим, что пространство создано и активно:
|
||||
```text
|
||||
NAME STATUS AGE
|
||||
<NAME-SPACE> Active 54s
|
||||
```
|
||||
|
||||
## Конфигурация всего
|
||||
|
||||
Для удобства я объединил манифесты в один файл (но можно и по отдельности). Создаем единый манифест:
|
||||
```bash
|
||||
sudo nano ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
|
||||
```
|
||||
|
||||
Он состоит из следующих частей:
|
||||
* `Endpoints` -- указывает на конечную точку, в которую будет проксироваться трафик. В данном случае это
|
||||
IP-адрес и порт, на который будет проксироваться запрос.
|
||||
* `Service` -- создает сервис, который будет использоваться для проксирования запросов к `Endpoints`.
|
||||
* `Certificate` -- создает сертификат для домена `<YOU-DOMAIN-NAME>`, который будет использоваться для
|
||||
шифрования трафика. Сертификат запрашивается через `cert-manager`, который автоматически обновляет его по мере
|
||||
необходимости.
|
||||
* `Middleware` -- создает промежуточное ПО, которое будет использоваться для обработки запросов. В данном случае
|
||||
это редирект с HTTP на HTTPS и исключение редиректа для ACME challenge (механизма внешней проверки владения доменом
|
||||
со стороны 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
|
||||
# Endpoints для внешнего хоста <SERVICE-NAME>
|
||||
# Задаёт IP и порт внешнего сервера, так как <SERVICE-NAME> внешний хост для k3s
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: <SERVICE-NAME>
|
||||
namespace: <NAME-SPACE> # Namespace для <SERVICE-NAME>
|
||||
subsets: # Прямо в корне, без spec
|
||||
- addresses:
|
||||
- ip: <PROXIED-HOST> # IP Synology, где работает <SERVICE-NAME>
|
||||
ports:
|
||||
- port: <PROXIED-PORT> # Порт Synology (HTTP)
|
||||
protocol: TCP
|
||||
|
||||
---
|
||||
# Service для маршрутизации трафика от Traefik к внешнему хосту
|
||||
# Связывает IngressRoute с Endpoints
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: <SERVICE-NAME>
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
ports:
|
||||
- port: <PROXIED-PORT> # Порт сервиса, на который Traefik отправляет трафик
|
||||
targetPort: <PROXIED-PORT> # Порт на Synology
|
||||
protocol: TCP
|
||||
|
||||
---
|
||||
# Middleware для редиректа HTTP → HTTPS
|
||||
# Применяется к HTTP-запросам для перенаправления на HTTPS
|
||||
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
|
||||
# apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: https-redirect
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
redirectScheme:
|
||||
scheme: https # Перенаправлять на HTTPS
|
||||
permanent: true # Код 301 (постоянный редирект)
|
||||
|
||||
---
|
||||
# Middleware для исключения редиректа на ACME challenge
|
||||
# Позволяет 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
|
||||
metadata:
|
||||
name: <SERVICE-NAME>-http
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
entryPoints:
|
||||
- web # Порт 80 (стандартный HTTP)
|
||||
routes:
|
||||
- match: Host("<YOU-DOMAIN-NAME>") # Запросы к <YOU-DOMAIN-NAME>
|
||||
kind: Rule
|
||||
services:
|
||||
- name: <SERVICE-NAME> # Сервис <SERVICE-NAME>
|
||||
port: <PROXIED-PORT> # Порт сервиса
|
||||
middlewares:
|
||||
- name: https-redirect # Редирект на HTTPS
|
||||
|
||||
---
|
||||
# IngressRoute для HTTPS (порт 443)
|
||||
# Обрабатывает HTTPS-запросы с TLS
|
||||
apiVersion: traefik.io/v1alpha1 # версия Traefik v34.2.1+up34.2.0 (Traefik v3.3.6)
|
||||
# apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: <SERVICE-NAME>-https
|
||||
namespace: <NAME-SPACE>
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure # Порт 443 (HTTPS)
|
||||
routes:
|
||||
- match: Host("<YOU-DOMAIN-NAME>")
|
||||
kind: Rule
|
||||
services:
|
||||
- name: <SERVICE-NAME>
|
||||
port: <PROXIED-PORT>
|
||||
tls:
|
||||
secretName: <SERVICE-NAME>-tls # Сертификат от cert-manager
|
||||
|
||||
---
|
||||
# IngressRoute для HTTP-01 challenge (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. Это нестандартный
|
||||
ClusterIssuer cert-manager, создание которого описано в [документации](https://cert-manager.io/docs/usage/ingress/#tls-termination)
|
||||
и [отдельной инструкции](k3s-custom-container-deployment.md#создание-clusterissuer)
|
||||
(возможно, вам нужно будет создать его отдельно). Если вы используете другой ClusterIssuer, то замените letsencrypt-prod
|
||||
на имя вашего ClusterIssuer в секции `issuerRef` в манифесте.
|
||||
|
||||
## Применяем манифест
|
||||
```bash
|
||||
sudo kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
|
||||
```
|
||||
|
||||
Проверяем, что все ресурсы создались:
|
||||
```bash
|
||||
sudo kubectl get all -n <NAME-SPACE>
|
||||
sudo kubectl get ingressroute -n <NAME-SPACE>
|
||||
sudo kubectl get middleware -n <NAME-SPACE>
|
||||
sudo kubectl get service -n <NAME-SPACE>
|
||||
sudo kubectl get endpoints -n <NAME-SPACE>
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
service/<SERVICE-NAME> ClusterIP 10.43.152.59 <none> <PROXIED-PORT>/TCP 3h
|
||||
```
|
||||
|
||||
Для IngressRoute:
|
||||
```text
|
||||
NAME AGE
|
||||
<SERVICE-NAME> 3h
|
||||
<SERVICE-NAME>-acme 1h
|
||||
<SERVICE-NAME>-http 1h
|
||||
<SERVICE-NAME>-https 1h
|
||||
```
|
||||
|
||||
Для Middleware:
|
||||
```text
|
||||
NAME AGE
|
||||
https-redirect 1h
|
||||
no-https-redirect 1h
|
||||
```
|
||||
|
||||
Для Service:
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
<SERVICE-NAME> ClusterIP 10.43.152.59 <none> <PROXIED-PORT>/TCP 3h
|
||||
```
|
||||
|
||||
Для Endpoints:
|
||||
```text
|
||||
NAME ENDPOINTS AGE
|
||||
<SERVICE-NAME> <PROXIED-HOST>:<PROXIED-PORT> 3h
|
||||
```
|
||||
|
||||
Проверяем, что сертификат создан:
|
||||
```bash
|
||||
sudo kubectl describe certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
sudo kubectl get secret -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
```
|
||||
|
||||
## UPD
|
||||
|
||||
Почему-то SSL-сертификат Let's Encrypt через положенный срок не перевыпустился. Для начала выяснилось, что на
|
||||
моем роутере перестал работать NAT-loopback, и внутрення проверка Certbot перестала работать. Похоже роутер получил
|
||||
обновление прошивки, и все сломалось. В результате DNS кластера (coredns) разрешал домен во внешний IP-адрес роутера,
|
||||
а не во внутренний IP-адрес кластера (VIP через keepalived), а без NAT-loopback запросы к домену не доходили до
|
||||
кластера.
|
||||
|
||||
Как чинил:
|
||||
|
||||
Во-первых, руками поменял на всех узлах кластера (включая роутер) в файле `/etc/resolv.conf` IP-адрес DNS.
|
||||
Обновление испортило и DHCP-сервер на роутере, и он стал раздавать свой IP-адреса как DNS-сервер, вместо моего
|
||||
домшнего DNS-сервера (работат на Synology NAS).
|
||||
|
||||
Во-вторых, в кластере k3s обновил конфигурацию coredns:
|
||||
```bash
|
||||
kubectl edit configmap -n kube-system coredns
|
||||
```
|
||||
|
||||
Отчего-то там было навернуты хосты 'hosts /etc/coredns/NodeHosts` внутри `.:53 ` блока, и это мешало. В результате
|
||||
получалось следующее:
|
||||
```yaml
|
||||
# Please edit the object below. Lines beginning with a '#' will be ignored,
|
||||
# and an empty file will abort the edit. If an error occurs while saving this file will be
|
||||
# reopened with the relevant failures.
|
||||
#
|
||||
apiVersion: v1
|
||||
data:
|
||||
Corefile: |
|
||||
.:53 {
|
||||
errors
|
||||
health
|
||||
ready
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
}
|
||||
prometheus :9153
|
||||
forward . /etc/resolv.conf
|
||||
cache 30
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
import /etc/coredns/custom/*.override
|
||||
}
|
||||
import /etc/coredns/custom/*.server
|
||||
...
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
После сохранения конфигурации манифет coredns (т.к. его редактируем пряма в кластере) применяется автоматически.
|
||||
После пеускаем coredns:
|
||||
```bash
|
||||
kubectl rollout restart deployment -n kube-system coredns
|
||||
```
|
||||
|
||||
Проверяем логи:
|
||||
```bash
|
||||
kubectl logs -n kube-system -l k8s-app=kube-dns
|
||||
```
|
||||
|
||||
Проверяем статусы подов CoreDNS:
|
||||
```shell
|
||||
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide
|
||||
```
|
||||
|
||||
Поды должны быть в статусе `Running`:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-646d9c4784-wsbbg 1/1 Running 0 58m 10.42.2.127 opi5plus-1 <none> <none>
|
||||
coredns-646d9c4784-z5zqv 1/1 Running 0 58m 10.42.0.125 opi5 <none> <none>
|
||||
```
|
||||
|
||||
И наконец, в-третьих, обновил сертификат Let's Encrypt. Сначала удалил старый сертификат и секрет:
|
||||
```bash
|
||||
kubectl delete certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
kubectl delete secret -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
```
|
||||
|
||||
И снова применил манифест с сертификатом:
|
||||
```bash
|
||||
kubectl apply -f ~/k3s/<SERVICE-NAME>/<SERVICE-NAME>.yaml
|
||||
```
|
||||
|
||||
Проверяем, что сертификат обновился:
|
||||
```bash
|
||||
kubectl describe certificate -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
kubectl get secret -n <NAME-SPACE> <SERVICE-NAME>-tls
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
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:
|
||||
|
||||

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

|
1564
misc/deploying-django-site-to-dvs-hosting.md
Normal file
67
misc/set-locale-ru.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Установка RU-локали в Ubuntu
|
||||
|
||||
Чтобы добавить русскую локаль `ru_RU.UTF-8` в Ubuntu, выполните следующие шаги:
|
||||
|
||||
Проверим, доступна ли локаль ru_RU.UTF-8 в системе:
|
||||
```shell
|
||||
locale -a | grep ru_RU
|
||||
```
|
||||
|
||||
Если локаль ru_RU.UTF-8 уже есть в списке, значит, она уже установлена. Если её нет, cгенерируем локаль `ru_RU.UTF-8`
|
||||
и добавим её в систему:
|
||||
```shell
|
||||
sudo locale-gen ru_RU.UTF-8
|
||||
```
|
||||
|
||||
Обновим настройку локалей:
|
||||
```shell
|
||||
sudo update-locale
|
||||
```
|
||||
|
||||
## Установим локаль как системную по умолчанию. :
|
||||
|
||||
Чтобы локаль `ru_RU.UTF-8` была установлена по умолчанию для всей системы, отредактируем файл `/etc/default/locale`:
|
||||
```shell
|
||||
sudo nano /etc/default/locale
|
||||
```
|
||||
|
||||
Добавим (или изменим) строки на следующие:
|
||||
```text
|
||||
LANG=ru_RU.UTF-8
|
||||
LC_ALL=ru_RU.UTF-8
|
||||
```
|
||||
|
||||
Сохраним файл и выйдем из редактора (`Ctrl + X`, затем `Y` для подтверждения).
|
||||
|
||||
Чтобы изменения вступили в силу, перезагрузим систему или выполним:
|
||||
```shell
|
||||
source /etc/default/locale
|
||||
```
|
||||
|
||||
Проверим текущую локаль:
|
||||
```shell
|
||||
locale
|
||||
```
|
||||
|
||||
Увидим что-то типа:
|
||||
```text
|
||||
LANG=ru_RU.UTF-8
|
||||
LANGUAGE=
|
||||
LC_CTYPE="ru_RU.UTF-8"
|
||||
LC_NUMERIC="ru_RU.UTF-8"
|
||||
LC_TIME="ru_RU.UTF-8"
|
||||
LC_COLLATE="ru_RU.UTF-8"
|
||||
LC_MONETARY="ru_RU.UTF-8"
|
||||
LC_MESSAGES="ru_RU.UTF-8"
|
||||
LC_PAPER="ru_RU.UTF-8"
|
||||
LC_NAME="ru_RU.UTF-8"
|
||||
LC_ADDRESS="ru_RU.UTF-8"
|
||||
LC_TELEPHONE="ru_RU.UTF-8"
|
||||
LC_MEASUREMENT="ru_RU.UTF-8"
|
||||
LC_IDENTIFICATION="ru_RU.UTF-8"
|
||||
LC_ALL=ru_RU.UTF-8
|
||||
```
|
||||
|
||||
Локаль `ru_RU.UTF-8` установлена корректно.
|
||||
|
||||
|
85
nginx/nginx_as_direct_proxy.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Настройка nginx как прямого прокси
|
||||
|
||||
Собственно, прямой прокси — это прокси, который просто перенаправляет запросы на другой сервер. Очень полезно,
|
||||
когда у вас внутри сети есть один компьютер, который виден из интернет (DMZ или через проброс портов), и мы хотим
|
||||
перенаправить внешние запросы на другие сервера внутри сети. Заодно можно настроить SSL-терминацию.
|
||||
|
||||
На примере AudioBookShelf, который должен быть доступен снаружи по адресу `some.you.site` у нас будет вот такой конфиг:
|
||||
```nginx configuration
|
||||
# config for AudioBookShelf [some.you.site]
|
||||
|
||||
server {
|
||||
server_name [some.you.site]; # доменное имя сайта
|
||||
charset utf-8; # кодировка по умолчанию
|
||||
|
||||
access_log /home/orangepi/web-data/audiobookshelf/logs/audiobookshelf-access.log; # логи с доступом
|
||||
error_log /home/orangepi/web-data/audiobookshelf/logs/audiobookshelf-error.log; # логи с ошибками
|
||||
|
||||
client_max_body_size 512M; # максимальный объем файла для загрузки на сайт (max upload size)
|
||||
# listen 80; # managed by Certbot
|
||||
listen 443 ssl http2; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/[some.you.site]/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/[some.you.site]/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
location /favicon.ico { root /home/orangepi/web-data/audiobookshelf/html; } # Расположение favicon.ico
|
||||
location /favicon.png { root /home/orangepi/web-data/audiobookshelf/html; } # Расположение favicon.png
|
||||
location /robots.txt { root /home/orangepi/web-data/audiobookshelf/html; } # robots.txt (dissalow all)
|
||||
|
||||
location / {
|
||||
proxy_pass http://[какой-то-ip]:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Nginx-Proxy true;
|
||||
# proxy_redirect off;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
|
||||
}
|
||||
# location / {
|
||||
# index index.html;
|
||||
# }
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = [some.you.site]) { return 301 https://$host$request_uri; } # managed by Certbot
|
||||
server_name [some.you.site];
|
||||
listen 80;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
```
|
||||
|
||||
## Проксирование на host если nginx находится внутри Docker
|
||||
|
||||
Если [nginx находится внутри Docker или Docker Compose](../docker/docker-nginx-w-certbot.md), то он сможет увидеть
|
||||
только свои контейнерные IP-адреса и хосты. Если таким nginx нужно проксировать на сам хост, то в конфиге nginx
|
||||
нужно указать:
|
||||
```nginx configuration
|
||||
proxy_pass http://host.docker.internal:xxxx;
|
||||
```
|
||||
|
||||
* `host.docker.internal` -- это специальный DNS-имя, которое указывает на хост, на котором запущен Docker.
|
||||
|
||||
Начиная с Docker 20.10.0+ сам контейнер надо запускать с дополнительным параметром `--add-host=host.docker.internal:host-gateway`.
|
||||
```shell
|
||||
docker run --add-host=host.docker.internal:host-gateway ...
|
||||
```
|
||||
|
||||
Или добавить дополнительную инструкцию `extra_hosts: "host.docker.internal:host-gateway"` в `docker-compose.yml` при
|
||||
использовании Docker Compose:
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
ports:
|
||||
- "80:80"
|
||||
...
|
||||
...
|
||||
```
|
44
python/python_as_service.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Python-скрипт как служба Linux
|
||||
|
||||
| ВАЖНО |
|
||||
|:------|
|
||||
| Python-скрипт работающий как служба Linux не должен быть блокирующим. В противном случае, он может привести к зависанию системы. Чтобы избежать блокировок в нем долны быть пердусмотрены механизмы асинхронного выполнения. Самое простое решение -- использовангие `sleep()` в вечных циклах. |
|
||||
|
||||
Чтобы превратить Python-скрипт в службу Linux, можно использовать systemd (систему инициализации, используемую в большинстве дистрибутивов Linux). Нам потребуется создать файл службы systemd, который будет запускать ваш Python-скрипт:
|
||||
|
||||
1. Создадим файл службы systemd. Этот файл обычно находится в каталоге `/etc/systemd/system/`. Назовем его, например, `my_python_service.service`.
|
||||
|
||||
2. В этом файле укажем следующее:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description = My_Python_Service
|
||||
After = network.target
|
||||
|
||||
[Service]
|
||||
ExecStart = /usr/bin/python3 /path/to/your/python/script.py
|
||||
Restart = always
|
||||
User = username
|
||||
Group = groupname
|
||||
Environment = NAME=value
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Где:
|
||||
|
||||
- `Description` -- описание службы.
|
||||
- `After` -- указание systemd запускать службу после того, как сеть будет доступна.
|
||||
- `ExecStart` -- команда, которую systemd выполнит для запуска вашей службы. Соответственно в ней указание какой Python запускать (и где он находится) и полный путь к нашему Python-скрипту.
|
||||
- `Restart` -- указание systemd перезапустить службу, если она упадет.
|
||||
- `User` и `Group` -- имя пользователя и группы, от имени которых будет запущена служба.
|
||||
- `Environment` -- переменные окружения, которые будут установлены при запуске службы.
|
||||
- `WantedBy` -- это указание systemd, когда должна быть запущена служба.
|
||||
|
||||
|
||||
3. После создания файла службы systemd, надо обновить systemd, чтобы он знал о новой службе. Можно сделать это с помощью команды `systemctl daemon-reload`.
|
||||
|
||||
4. Теперь можно запустить службу с помощью команды `systemctl start my_python_service`.
|
||||
|
||||
5. Если необходимо, чтобы ваша служба автоматически запускалась при загрузке системы, то включаем её в автозапуск с помощью команды `systemctl enable my_python_service`.
|
@@ -170,7 +170,7 @@ sudo chown -R 777 /media/backup/
|
||||
sudo chown -R [user]:[user] /media/backup/
|
||||
```
|
||||
|
||||
### Скрипт резервного копирования в SAMBA-папку внутри домашней сети
|
||||
### Скрипт резервного копирования в SAMBA-папку внутри домашней сети (на NAS)
|
||||
|
||||
Скрипт резервного копирования (*не забудьте заменить `[ip]`, `[user]` и `[login]` -- ip-адрес NAS
|
||||
в домашней сети, NAS-логин и NAS-пароль*) сохранит zip-архивы образа flash-накопителя и домашний
|
||||
@@ -270,3 +270,63 @@ sudo crontab -e
|
||||
|
||||
Скрипт будет запускаться каждый понедельник в 00:05. Таким образом в каждый момент времени
|
||||
в облаке будет храниться две последних резервных копий (за две предыдущих недели).
|
||||
|
||||
-----
|
||||
|
||||
## Восстановление из резервной копии
|
||||
|
||||
### Восстановление загрузочного образа flash-накопителя
|
||||
|
||||
Для восстановления загрузочного образа flash-накопителя на Orange Pi, нужно извлечь его из
|
||||
устройства (raspberry pi или orange pi) и, предварительно распокавав нужный `img` из zip-архива
|
||||
просто перезаписать заново флешку. Под windows надо использовать [rufus](https://rufus.ie/ru/)
|
||||
(но это работает хреново, иногда вообще все USB-порты компьютера перестают работать до перезагрузки)
|
||||
а под MacOS -- [balenaEtcher](https://www.balena.io/etcher/) (все работает идеально).
|
||||
|
||||
Так же под MacOS и Linux можно восстановить образ с помощью команды `dd`. Сначала узнаем имя устройства
|
||||
в которое вставлена флешка:
|
||||
```shell
|
||||
diskutil list
|
||||
```
|
||||
|
||||
Увидим список всех дисков, в том числе и флешек. Что-то типа:
|
||||
```txt
|
||||
...
|
||||
...
|
||||
/dev/disk5 (external, physical):
|
||||
#: TYPE NAME SIZE IDENTIFIER
|
||||
0: GUID_partition_scheme *128.2 GB disk5
|
||||
1: BC13C2FF-59E6-4262-A352-B275FD6F7172 268.4 MB disk5s1
|
||||
2: Linux Filesystem 126.6 GB disk5s2
|
||||
(free space) 1.3 GB
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
Таким образом, флешка вставлена в `/dev/disk5`. Теперь можно записать образ на флешку:
|
||||
```shell
|
||||
sudo dd if=/path/to/flash-disk.img of=/dev/disk5 bs=1m
|
||||
```
|
||||
|
||||
### Восстановление домашней папки из SMB
|
||||
|
||||
Восстановление из резервной копии -- не регулярное действие, поэтому не нужно автоматизировать
|
||||
процесс. Для восстановления из резервной копии на Orange Pi, нужно смонтировать сетевую папку:\
|
||||
```shell
|
||||
mount -t cifs -o username=[samba-login],password=[smaba-pwd] //192.168.1.50/NetBackup /media/backup/
|
||||
```
|
||||
|
||||
И восстановить файлы из архива:
|
||||
```shell
|
||||
sudo unzip -o /media/backup/orange-pi-backup/home-volum--YYYY-MM-DD.zip -d /
|
||||
```
|
||||
|
||||
Проверим права доступа после восстановления и убедимся, что права доступа к файлам восстановлены корректно:
|
||||
```shell
|
||||
ls -l /home/имя_пользователя
|
||||
```
|
||||
|
||||
Если права сбились, установим правильного владельца и группу для вашего пользователя:
|
||||
```shell
|
||||
sudo chown -R orangepi:orangepi /home/orangepi
|
||||
```
|
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 из указанных стран при появлении их в логах.
|
||||
И если честно, можно вообще все страны забанить, кроме тех, где бываю в отпуске. Нечего им делать на моем сервере. :)
|
||||
|
840
raspberry-and-orange-pi/k3s.md
Normal file
@@ -0,0 +1,840 @@
|
||||
# Развертывание k3s на Orange Pi
|
||||
|
||||
K3s — это облегчённая версия Kubernetes, созданная для слабых или малых серверов (Raspberry Pi, Orange Pi,
|
||||
IoT-устройства, edge-серверы и т.п.). Для кластера из нескольких Orange Pi он предпочтительнее, так как:
|
||||
|
||||
* K3S менее требователен к ресурсам (Полный k8s на ARM может сожрать 1-2 ГБ только на управление кластером,
|
||||
а k3s занимает ~500 МБ.
|
||||
* K3s проще устанавливать и обновлять. Shell-скрипт с [https://get.k3s.io](get.k3s.io) все сделает сам, и не нужно
|
||||
погружаться сложные настройки kubeadm. Обычный Kubernetes состоит из множества компонентов: kube-apiserver,
|
||||
kube-controller-manager, kube-scheduler, kubelet на каждой ноде, kube-proxy, etcd и т.д. В K3s всё это
|
||||
упаковано в один бинарник.
|
||||
* Всё работает "из коробки" благодаря встроенному Flannel (CNI) и не надо вручную настраивать Calico, Weave, Cilium.
|
||||
* В отличие от "классического" Kubernetes (например, kubeadm), где мастер-узлы по умолчанию изолированы от рабочих нагрузок с помощью taint'ов (например, NoSchedule), k3s не добавляет такие ограничения автоматически. Это значит:
|
||||
* Для моего проекта особо важно, что из коробки мастер-узел(ы)) в k3s является "гибридным" и выполняет одновременно
|
||||
функции управления (control-plane) и может запускать обычные поды, как воркер. Компоненты управления (API-сервер,
|
||||
контроллеры, etcd) работают как системные сервисы, а для пользовательских подов используется тот же kubelet,
|
||||
что и на воркерах. _**Кстати, что такое "поды".** Контейнеры в Kubernates называют "поды", чтобы отличать их от
|
||||
Docker-контейнеров и подчёркивать, что это абстракция уровня оркестрации. Но под — это не просто контейнер, это
|
||||
сущность Kubernetes, которая может включать несколько контейнеров, сетевые настройки и тома. Но под капотом
|
||||
контейнеры всё равно запускаются runtime’ом (это containerd в k3s). И Docker все равно еще нужен для создания
|
||||
образов, и если при установке k3s не указать `--docker` то k3s будет использовать его как runtime._
|
||||
|
||||
Но, есть у k3s и минус для конкретно моего случая — распределенная база **etcd**, в которой хранится состояния
|
||||
кластера, нод и подов, в нем заменена SQLite. Это круто для маленьких компьютеров: экономно по памяти и другим ресурсам,
|
||||
и, что главное, никак не сказывается на производительности (пока узлов меньше 50-80), но означает, что в кластере k3s
|
||||
может быть только одна мастер-нода. Если мастер-нода упадет, её некому будет заменить и весь кластер умрет.
|
||||
Мне же надо, чтобы как миниум две (а лучше все) ноды могли быть мастерами, так что я буду делать k3s-кластер
|
||||
с использованием *etcd*.
|
||||
|
||||
### Важное предупреждение
|
||||
|
||||
k3s — это не упрощенная мини-версия Kubernetes, здесь все компоненты упакованы в один бинарник, а значит намного
|
||||
проще не только добавлять узлы, но и удалять их. Так что если что-то пойдет не так с настройкой узла, просто удалите
|
||||
и начните заново. Удаление k3s с узла:
|
||||
```bash
|
||||
sudo /usr/local/bin/k3s-uninstall.sh # На мастерах
|
||||
sudo /usr/local/bin/k3s-agent-uninstall.sh # На воркере
|
||||
```
|
||||
|
||||
## Установка k3s на первом узле (мастер)
|
||||
|
||||
Некоторые требования к узлам:
|
||||
* На всех Orange Pi установлена одинаковая версия Ubuntu (например, 22.04 или 24.04).
|
||||
* Статические IP-адреса узлов (или зарезервированные под MAC-адреса IP в DHCP).
|
||||
* На уздах открыты порты 6443 (для API), 2379-2380 (для etcd) и 10250 (для kubelet).
|
||||
|
||||
|
||||
Установливаем первый мастер:
|
||||
```bash
|
||||
curl -sfL https://get.k3s.io | sh -s - server --cluster-init --tls-san=192.168.1.27
|
||||
```
|
||||
|
||||
Здесь:
|
||||
* `server` — значение по умолчанию, устанавливает узел k3s в режиме *мастер* (control-plane). В этом режиме узел
|
||||
будет запускать все компоненты управления Kubernetes: API-сервер, контроллер-менеджер, планировщик (scheduler).
|
||||
Такой узел отвечает за управление кластером и может также выполнять рабочие нагрузки (workloads), если
|
||||
не настроены ограничения (taints). Если бы мы указали `agent` — был бы установлен узел k3s в режиме *воркер*-узла.
|
||||
* `--cluster-init` — добавляет поддержку высокой доступности (HA — High Availability) через встроенный `etcd`. Это
|
||||
значит, что узел инициализирует новый кластер и готов к тому, чтобы другие мастер-узлы могли к нему подключиться
|
||||
(для создания HA-конфигурации).
|
||||
* `--tls-san=192.168.1.27` — добавляет IP 192.168.1.27 в сертификаты API-сервера, чтобы другие узлы и клиенты
|
||||
могли обращаться к нему по этому адресу.
|
||||
|
||||
Проверим, что все k3s запущен:
|
||||
```bash
|
||||
sudo service k3s status
|
||||
```
|
||||
|
||||
Увидим что-то типа:
|
||||
```text
|
||||
● k3s.service - Lightweight Kubernetes
|
||||
Loaded: loaded (/etc/systemd/system/k3s.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since …
|
||||
…
|
||||
…
|
||||
```
|
||||
|
||||
## Перемещение конфигурации kubectl
|
||||
|
||||
При выполнении команд `kubectl` используется файл конфигурации из переменной `KUBECONFIG`. При установке k3s создает
|
||||
его в `/etc/rancher/k3s/k3s.yaml` и у него права `600` (только для root). Чтобы использовать `kubectl` от имени
|
||||
пользователя хоста, нужно скопировать файл конфигурации в домашнюю директорию и изменить права доступа.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.kube/config
|
||||
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
|
||||
```
|
||||
|
||||
Дадим права:
|
||||
```bash
|
||||
sudo chown opi:opi ~/.kube
|
||||
chmod 755 ~/.kube
|
||||
sudo chown opi:opi ~/.kube/config
|
||||
chmod 755 ~/.kube/config
|
||||
sudo chown opi:opi ~/.kube/config/k3s.yaml
|
||||
chmod 755 ~/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
Проверим:
|
||||
```bash
|
||||
ls -ld ~/.kube ~/.kube/config ~/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
Должны увидеть что-то вроде:
|
||||
```text
|
||||
drwxr-xr-x 4 opi opi 4096 Apr 26 11:32 /home/opi/.kube
|
||||
drwxr-xr-x 2 opi opi 4096 Apr 26 11:09 /home/opi/.kube/config
|
||||
-rwxr-xr-x 1 opi opi 2961 Apr 27 16:31 /home/opi/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
Установить переменную KUBECONFIG:
|
||||
```bash
|
||||
export KUBECONFIG=~/.kube/config/k3s.yaml
|
||||
```
|
||||
|
||||
И добавь её в `~/.bashrc` чтобы не вводить каждый раз после входа в систему или перезагрузки:
|
||||
```bash
|
||||
echo 'export KUBECONFIG=~/.kube/config/k3s.yaml' >> ~/.bashrc
|
||||
```
|
||||
|
||||
Проверим, что переменная установлена:
|
||||
```bash
|
||||
echo $KUBECONFIG
|
||||
```
|
||||
|
||||
## Проверим установку k3s
|
||||
|
||||
Посмотрим сколько нод в кластере (теперь можно не использовать `sudo`):
|
||||
```bash
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
И, та-да! Увидим одну ноду:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-2 Ready control-plane,etcd,master 31m v1.31.5+k3s1
|
||||
```
|
||||
|
||||
Как видим, узел `opi5plus-2` готов к работе и выполняет роли *control-plane*, *etcd* и *master*.
|
||||
|
||||
|
||||
А что там внутри? Посмотрим на поды:
|
||||
```bash
|
||||
kubectl get pod -A
|
||||
```
|
||||
|
||||
Целых семь подов (минималистичная установка k3s):
|
||||
```text
|
||||
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||
kube-system coredns-ccb96694c-tfjwj 1/1 Running 0 13m
|
||||
kube-system helm-install-traefik-crd-bdbgd 0/1 Completed 0 13m
|
||||
kube-system helm-install-traefik-mlztm 0/1 Completed 1 13m
|
||||
kube-system local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 13m
|
||||
kube-system metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 13m
|
||||
kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0 12m
|
||||
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 12m
|
||||
```
|
||||
|
||||
* READY `X/Y` в выводе `kubectl get pod` показывает:
|
||||
* Y — сколько контейнеров должно быть в поде (по спецификации).
|
||||
* X — сколько из них сейчас работает (running).
|
||||
* STATUS показывает состояние пода. Основные статусы которые могут быть:
|
||||
* `Running` — под работает и все контейнеры в нем запущены.
|
||||
* `Completed` — под завершил свою работу (например, Job или CronJob).
|
||||
* `Error` — под завершился с ошибкой.
|
||||
* `ContainerCreating` — под в процессе создания, контейнеры в нем еще не запущены.
|
||||
* `CrashLoopBackOff` — под перезапускается из-за ошибки.
|
||||
* `ImagePullBackOff` — не удалось загрузить образ контейнера.
|
||||
* `Pending` — под ожидает ресурсов (например, памяти или CPU).
|
||||
|
||||
Представлены следующие поды:
|
||||
1. `coredns` — это DNS-сервер для кластера. Он отвечает за разрешение имен внутри Kubernetes (например, чтобы поды
|
||||
могли обращаться друг к другу по именам сервисов вроде my-service.default.svc.cluster.local).
|
||||
2. `helm-install-traefik-crd` — это временный под (Job), который устанавливает Custom Resource Definitions (CRD)
|
||||
для *Traefik* — ingress-контроллера, встроенного в k3s. CRD нужны для управления ingress-ресурсами
|
||||
(маршрутизацией HTTP/HTTPS). Этот под — одноразовая задача (Job), а не постоянный сервис. Он запустился, выполнил
|
||||
работу (установил CRD) и завершился. Статус "*Completed*" значит, что он больше не работает.
|
||||
3. `helm-install-traefik` — ещё один Job, который устанавливает сам Traefik через Helm-чарт. Этот под развернул
|
||||
основной Traefik-под и завершился.
|
||||
4. `local-path-provisioner` — компонент для автоматического создания локальных Persistent Volumes (PV) на узлах. Он
|
||||
позволяет подам запрашивать хранилище (например, через PersistentVolumeClaim) без сложной настройки NFS или внешних
|
||||
хранилищ. В k3s это встроено для простоты.
|
||||
5. `metrics-server` — собирает данные об использовании ресурсов (CPU, память) подов и узлов. Это нужно для команд
|
||||
вроде `kubectl top` или для Horizontal Pod Autoscaler (HPA). Установку метрик можно отключить при запуске k3s
|
||||
флагом `--disable=metrics-server`.
|
||||
6. `svclb-traefik` - это под для балансировки нагрузки (Service Load Balancer) для Traefik. В k3s нет встроенного
|
||||
облачного балансировщика (как в AWS/GCP), поэтому *svclb* эмулирует его на уровне узла, перенаправляя трафик
|
||||
к сервисам типа LoadBalancer. У нас два таких контейнера:
|
||||
* один для самой логики балансировки;
|
||||
* другой для мониторинга или дополнительной функциональности (например, *keepalived* или аналога) и это зависит
|
||||
от реализации в k3s.
|
||||
7. `traefik` — сам Traefik, ingress-контроллер, который обрабатывает HTTP/HTTPS трафик кластера и маршрутизирует
|
||||
его к соответствующим подам (с динамической конфигурацией нашим) и сервисам по правилам Ingress. Traefik в k3s
|
||||
установлен по умолчанию, но его можно отключить при запуске k3s флагом `--disable=traefik` (не будет ни *traefik*,
|
||||
ни *svclb*, ни связанных *Helm Jobs*).
|
||||
|
||||
Обратите внимание, что, например, под `coredns` получил имя `coredns-ccb96694c-tfjwj`. Имена подов (Pod Names)
|
||||
в Kubernetes генерируются автоматически на основе правил, чтобы каждый под в кластере имел уникальное имя.
|
||||
Структура имени — `<имя-приложения>-<хеш-ревизии>-<случайный-суффикс>`. Впрочем, `<хеш-ревизии>` может отсутствовать,
|
||||
если под не имеет контроллера репликации (например, Job или CronJob).
|
||||
|
||||
Можно проверить, что API нашего узла (кластера) отвечает на порту `6443` (порт по умолчанию):
|
||||
```bash
|
||||
curl -k https://192.168.1.27:6443
|
||||
```
|
||||
|
||||
Здесь ключ `-k` означает, что мы не проверяем сертификаты (нам важно только, что сервер отвечает). Должны получить
|
||||
Unauthorized JSON-ответ от API. Что-то вроде:
|
||||
```json
|
||||
{
|
||||
"kind": "Status",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {},
|
||||
"status": "Failure",
|
||||
"message": "Unauthorized",
|
||||
"reason": "Unauthorized",
|
||||
"code": 401
|
||||
}
|
||||
```
|
||||
|
||||
ВАЖНО: Надо проверить версию Traefik, которая установилась. Например версия 3.3.2 отличается от 3.3.6 значением
|
||||
`apiVersion` в которые надо указывать в манифесте:
|
||||
* `traefik.io/v1alpha1` -- для Traefik v3.3.6
|
||||
* `traefik.containo.us/v1alpha1` -- для Traefik v3.3.2
|
||||
|
||||
Проверить версию можно так (подставьте вместо `<хеш-ревизии>-<случайный-суффикс>` свой, из вывода `kubectl get pod`):
|
||||
```bash
|
||||
kubectl get pod -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -o jsonpath='{.spec.containers[0].image}'
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
rancher/mirrkubectl exec -n kube-system traefik-<хеш-ревизии>-<случайный-суффикс> -- traefik versionik-67bfb46dcb-prvjd -- traefik version
|
||||
Version: 3.3.2
|
||||
Codename: saintnectaire
|
||||
Go version: go1.23.4
|
||||
Built: 2025-01-14T15:52:27Z
|
||||
OS/Arch: linux/arm64
|
||||
```
|
||||
|
||||
## Установка блочного хранилища (PVC -- Persistent Volume Claim) Longhorn
|
||||
|
||||
Longhorn -- это блочное хранилище k3s, которое позволяет создавать и управлять блочными томами в кластере
|
||||
для обеспечения высокой доступности и отказоустойчивости. Если узел, на котором находится том, выходит из строя,
|
||||
Longhorn автоматически перемещает том на другой узел и контейнер продолжает работу с томом, как будто ничего
|
||||
не произошло (с некоторой задержкой, конечно).
|
||||
|
||||
Если установить Longhorn сразу, то при добавлении новых узлов в кластер Longhorn автоматически будет устанавливаться
|
||||
на них. Но если вы хотите установить Longhorn позже, то нужно будет вручную установить его на новых узлах.
|
||||
|
||||
| **ВАЖНО** |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Longhorn требует наличия iSCSI на узлах. Убедитесь, что молуль iSCSI включен или собран в ядро Linux на вашем хосте. Для Orange Pi 5 Plus (Ubuntu на ядре 6.1.43-rockchip-rk3588) [потребует перекомпиляцию ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md), на Orange Pi 5 (Ubuntu ядро 5.10.160-rockchip-rk3588) iSCSI разрешен к подключению как модуль. |
|
||||
|
||||
Установим Longhorn на первый узел (мастер):
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml
|
||||
```
|
||||
|
||||
Проверим, что все поды Longhorn запустились:
|
||||
```bash
|
||||
kubectl get pod -n longhorn-system
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
csi-attacher-5d68b48d9-6wsff 1/1 Running 0 9m18s
|
||||
csi-attacher-5d68b48d9-t9wht 1/1 Running 0 9m19s
|
||||
csi-attacher-5d68b48d9-w772m 1/1 Running 0 9m18s
|
||||
csi-provisioner-6fcc6478db-bd26m 1/1 Running 0 9m18s
|
||||
csi-provisioner-6fcc6478db-vg9g9 1/1 Running 0 9m18s
|
||||
csi-provisioner-6fcc6478db-xc8ks 1/1 Running 0 9m18s
|
||||
csi-resizer-6c558c9fbc-47pw9 1/1 Running 0 9m18s
|
||||
csi-resizer-6c558c9fbc-n5ndj 1/1 Running 0 9m18s
|
||||
csi-resizer-6c558c9fbc-xjflz 1/1 Running 0 9m18s
|
||||
csi-snapshotter-874b9f887-2cbn8 1/1 Running 0 9m18s
|
||||
csi-snapshotter-874b9f887-5x9fd 1/1 Running 0 9m18s
|
||||
csi-snapshotter-874b9f887-79dgn 1/1 Running 0 9m18s
|
||||
engine-image-ei-b907910b-2jgjr 1/1 Running 0 10m
|
||||
instance-manager-fba73d00a7ff718b4ddabef450cfe759 1/1 Running 0 9m34s
|
||||
longhorn-csi-plugin-h92s8 3/3 Running 0 9m18s
|
||||
longhorn-driver-deployer-5f44b4dc59-z7tlc 1/1 Running 0 9m34s
|
||||
longhorn-manager-k2gtm 2/2 Running 0 10m
|
||||
longhorn-ui-f7ff9c74-7bbsw 1/1 Running 0 10m
|
||||
longhorn-ui-f7ff9c74-b4svq 1/1 Running 0 10m
|
||||
```
|
||||
|
||||
Что это за поды? Longhorn состоит из нескольких компонентов, каждый из которых отвечает за свою задачу:
|
||||
* CSI-компоненты (attacher, provisioner, resizer, snapshotter) интегрируют Longhorn с Kubernetes для работы
|
||||
с Persistent Volumes (PV) и Persistent Volume Claims (PVC).
|
||||
* `csi-attacher` -- (3 пода) для присоединения (attaches) тома Longhorn к подам Kubernetes, когда PVC монтируется.
|
||||
Три реплики CSI -- для отказоустойчивости. На одном узле достаточно одной, но Longhorn по умолчанию разворачивает 3.
|
||||
* `csi-provisioner` -- (3 пода) для сздания новыех PV (томов) при запросе PVC. Отвечает за динамическое
|
||||
выделение хранилища.
|
||||
* `csi-resizer` -- (3 пода) позволяет изменять размер томов (можно только увеличивать PVC) без остановки приложений.
|
||||
* `csi-snapshotter` -- (3 пода): управля.т созданием и восстановлением снапшотов томов.
|
||||
* `engine-image` -- хранит бинарники движка Longhorn, используемые для работы томов (чтение/запись данных).
|
||||
Один под на узел, содержит образ движка для всех томов на этом узле и запускается на каждом узле.
|
||||
* `instance-manager` -- (1 под) Управляет движками и репликами томов на узле (например, запускает процессы для
|
||||
чтения/записи данных). Один под на узел для локального управления томами.
|
||||
* `longhorn-csi-plugin` -- (1 под, 3 контейнера) интерфейс между Kubernetes CSI и Longhorn. Обрабатывает
|
||||
монтирование/управление томами на узле.
|
||||
* `longhorn-driver-deployer` -- (1 под) устанавливает CSI-драйвер и регистрирует его в Kubernetes.
|
||||
Нужен для инициализации CSI-интеграции и он обычно один в кластере.
|
||||
* `longhorn-manager` -- (1 под, 2 контейнера) Основной компонент Longhorn. Управляет узлами, томами, репликами,
|
||||
снапшотами и синхронизацией данных. Один под на узел, 2 контейнера (основной + kube-rbac-proxy для авторизации).
|
||||
* `longhorn-ui` -- (2 реплики) Предоставляют веб-интерфейс для управления Longhorn (графики, настройка томов,
|
||||
* мониторинг). Две реплики для отказоустойчивости, хотя на одном узле это избыточно.
|
||||
|
||||
|
||||
## Подключение второго узла (мастер)
|
||||
|
||||
Для начала, на первой ноде получим токен для подключения нового узла к кластеру:
|
||||
```bash
|
||||
sudo cat /var/lib/rancher/k3s/server/node-token
|
||||
```
|
||||
|
||||
Вывод будет что-то вроде `K10…::server:longrandomstring`. Это и есть токен, который нужно будет использовать.
|
||||
|
||||
Теперь на втором Orange Pi (например, с IP 192.168.1.28) можно запустить второй мастер-узел (вставим токен
|
||||
из предыдущего шага):
|
||||
```bash
|
||||
curl -sfL https://get.k3s.io | sh -s - server --server https://192.168.1.27:6443 --token <ТОКЕН> --tls-san=192.168.1.28
|
||||
```
|
||||
Здесь ключи:
|
||||
* `--server https://192.168.1.27:6443` — указывает на API мастер-узла, чтобы наш новый узел мог подключиться к кластеру.
|
||||
* `--token` — токен аутентификации из предыдущего шага.
|
||||
* `--tls-san=192.168.1.28` — добавляет IP нашего второго мастера в сертификаты (для будущих подключений).
|
||||
|
||||
Проверим какие теперь ноды в кластере:
|
||||
```bash
|
||||
sudo k3s kubectl get nodes
|
||||
```
|
||||
|
||||
Теперь увидим две ноды:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-2 Ready control-plane,etcd,master 2h v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 110s v1.31.5+k3s1
|
||||
```
|
||||
|
||||
Проверим поды кластера и посмотрим на каких нодах они запущены:
|
||||
```bash
|
||||
sudo k3s kubectl get pods -A -o wide
|
||||
```
|
||||
|
||||
И увидим, что на второй ноде запустились те же поды, что и на первой:
|
||||
```text
|
||||
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
kube-system coredns-ccb96694c-tfjwj 1/1 Running 0 2h 10.42.0.4 opi5plus-2 <none> <none>
|
||||
kube-system helm-install-traefik-crd-bdbgd 0/1 Completed 0 2h <none> opi5plus-2 <none> <none>
|
||||
kube-system helm-install-traefik-mlztm 0/1 Completed 1 2h <none> opi5plus-2 <none> <none>
|
||||
kube-system local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 2h 10.42.0.3 opi5plus-2 <none> <none>
|
||||
kube-system metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 2h 10.42.0.2 opi5plus-2 <none> <none>
|
||||
kube-system svclb-traefik-4f8c2580-jddgz 2/2 Running 0 2h 10.42.0.7 opi5plus-2 <none> <none>
|
||||
kube-system svclb-traefik-4f8c2580-xzt5d 2/2 Running 0 2m35s 10.42.1.2 opi5plus-3 <none> <none>
|
||||
kube-system traefik-5d45fc8cc9-t5d58 1/1 Running 0 2h 10.42.0.8 opi5plus-2 <none> <none>
|
||||
```
|
||||
|
||||
Как видим, у нас появился еще один `svclb-traefik` на второй ноде. Это под — Service Load Balancer (SLB) для Traefik.
|
||||
Он эмулирует облачный балансировщик нагрузки (типа AWS ELB), которого нет в локальном окружении вроде Orange Pi.
|
||||
SLB перенаправляет внешний трафик (например, на порты 80/443) к сервисам типа LoadBalancer внутри кластера.
|
||||
|
||||
## Подключение третьего узла (воркера)
|
||||
|
||||
Добавление третьего узда в качестве воркера (рабочего узла) мы сделаем временно. Во-первых, чтобы показать как это
|
||||
делается, а во-вторых, чтобы показать как удалять узел и с какими особенностями это связано. И наконец, в-третьих,
|
||||
объяснить что такое кворум и почему важно, чтобы в кластере было нечетное количество мастер-узлов.
|
||||
|
||||
И так, подключение рабочего узла даже проще, чем мастера. Выполним на нашем новом узле:
|
||||
```bash
|
||||
curl -sfL https://get.k3s.io | sh -s - agent --server https://192.168.1.10:6443 --token <ТОКЕН>
|
||||
```
|
||||
|
||||
Здесь ключ:
|
||||
* `agent` — устанавливает узел в режиме воркера (worker). Это значит, что узел будет выполнять рабочие нагрузки
|
||||
(поды), но не будет управлять кластером (без *control-plane*, *master* и на нем нет реплики *etcd*).
|
||||
|
||||
Посмотрим на ноды (команда выполняется на одном из мастер-узлов):
|
||||
```bash
|
||||
sudo k3s kubectl get nodes
|
||||
```
|
||||
|
||||
Теперь у нас три ноды, и все они имеют статус *Ready*:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-1 Ready <none> 96s v1.31.5+k3s1
|
||||
opi5plus-2 Ready control-plane,etcd,master 3h v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 2h v1.31.5+k3s1
|
||||
```
|
||||
|
||||
Новая нода `opi5plus-1` готова к работе и не имеет ролей, а только выполняет рабочие нагрузки (поды).
|
||||
|
||||
Посмотрим на поды:
|
||||
```bash
|
||||
sudo k3s kubectl get pods -n kube-system -o wide
|
||||
```
|
||||
|
||||
И увидим, что на новом воркере (opi5plus-1) запустился под балансировщика `svclb-traefik`:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-ccb96694c-tfjwj 1/1 Running 0 3h 10.42.0.4 opi5plus-2 <none> <none>
|
||||
helm-install-traefik-crd-bdbgd 0/1 Completed 0 3h <none> opi5plus-2 <none> <none>
|
||||
helm-install-traefik-mlztm 0/1 Completed 1 3h <none> opi5plus-2 <none> <none>
|
||||
local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 3h 10.42.0.3 opi5plus-2 <none> <none>
|
||||
metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 3h 10.42.0.2 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-4q7dj 3/3 Running 0 92s 10.42.2.2 opi5plus-1 <none> <none>
|
||||
svclb-traefik-4f8c2580-h7b9c 3/3 Running 0 2h 10.42.0.9 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-qmzf6 3/3 Running 0 2h 10.42.1.5 opi5plus-3 <none> <none>
|
||||
traefik-6c979cd89d-98fk8 1/1 Running 0 1h 10.42.1.6 opi5plus-3 <none> <none>
|
||||
```
|
||||
|
||||
Посмотрим состояние сервисов в кластере:
|
||||
```bash
|
||||
sudo k3s kubectl get service -n kube-system
|
||||
```
|
||||
|
||||
Увидим, что сервис *traefik* доступен на всех нодах:
|
||||
```text
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
kube-dns ClusterIP 10.43.0.10 <none> 53/UDP,53/TCP,9153/TCP 3d
|
||||
metrics-server ClusterIP 10.43.248.208 <none> 443/TCP 3d
|
||||
traefik LoadBalancer 10.43.164.48 192.168.1.26,192.168.1.27,192.168.1.28 80:31941/TCP,443:30329/TCP,9000:32185/TCP 3d
|
||||
```
|
||||
|
||||
Можем так же настроить [web-панель Traefik](../kubernetes/k3s-setting-up-web-access-to-dashboard.md).
|
||||
|
||||

|
||||
|
||||
Что ж, теперь у нас есть кластер k3s с тремя нодами: двумя мастерами и одним воркером. Но, как я уже говорил, это не
|
||||
идеальная конфигурация, так как у нас четное количество мастер-узлов.
|
||||
|
||||
Попробует отключить один из мастеров (не обязательно выключать питание, достаточно отсоединить сетевой кабель ethernet)
|
||||
и посмотрим что произойдет.
|
||||
|
||||
Само-собой доступ к панели Traefik на "погашенном узле" пропадет, но с обоих работающих узлов (живого мастера
|
||||
и воркера) сохранится. И еще будет потеряна возможность работать с кластером через `kubectl`. Почему kubectl
|
||||
не работает на втором мастере? Ошибка на втором мастере после отключения первого говорит о том, что кластер потерял
|
||||
полную функциональность API-сервера. Как говорилось ранее, k3s с настройкой HA (высокая доступность) используется
|
||||
встроенный etcd для хранения состояния. Для работы etcd в HA-режиме требуется кворум.
|
||||
|
||||
Кворум в etcd — это минимальное количество узлов, которые должны быть доступны для согласования данных и принятия
|
||||
решений в кластере. Это основа отказоустойчивости распределённой системы. При двух мастерах: **Кворум = N/2 + 1**,
|
||||
где N — количество мастер-узлов. Для 2 узлов: *кворум = 2/2 + 1 = 2*. Это значит, что оба мастера должны быть живы,
|
||||
чтобы etcd работал. Если один мастер падает, второй не может достичь кворума (1 < 2) и останавливает работу etcd.
|
||||
Без etcd API-сервер на втором мастере не может отвечать на запросы kubectl, хотя поды продолжают работать, так как
|
||||
им не нужен доступ к etcd в реальном времени.
|
||||
|
||||
В чем может быть смысл иметь два мастера? Это обеспечивает репликацию данных (второй хранит копию etcd), но не
|
||||
даёт отказоустойчивости — когда один мастер упал, кластер становится неуправляемым (нет управления через kubectl),
|
||||
рабочие нагрузки (поды) могут продолжать работать, пока жив хотя бы один узел, но новые изменения (развертывание
|
||||
подов и обновления) невозможны.
|
||||
|
||||
Таким образом, два мастера это не идеальная HA (High Availability), а скорее "полу-HA". Полная HA начинается
|
||||
с трёх узлов! Три мастера — это стандарт для настоящей отказоустойчивости в Kubernetes (и k3s). При трёх мастерах:
|
||||
**Кворум = 3/2 + 1 = 2**. Это значит, что кластер остаётся рабочим, если один мастер уме, но живы минимум 2 из 3.
|
||||
Два оставшихся поддерживают кворум (2 >= 2), и кластер полностью управляем (kubectl работает и можно деплоить поды).
|
||||
|
||||
### Удаление узла из кластера
|
||||
|
||||
Чтобы снова получить возможность управлять кластером включим погашенный мастер-узел, подождем пока кворум восстановится
|
||||
и удалим с k3s воркер-узел (opi5plus-1):
|
||||
```bash
|
||||
sudo /usr/local/bin/k3s-agent-uninstall.sh
|
||||
```
|
||||
|
||||
Теперь состояние узлов в кластере:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-1 NotReady <none> 147m v1.31.5+k3s1
|
||||
opi5plus-2 Ready control-plane,etcd,master 3d2h v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 2d v1.31.5+k3s1
|
||||
```
|
||||
|
||||
Нода со статусом `NotReady` с ролью `<none>` — это остатки бывшего воркера. Если запустить на том же хосте масте, узел
|
||||
может "ожить" и перерегистрироваться с новыми ролями. Но это не обязательно удалит старый объект Node — он может
|
||||
либо обновиться (если имя совпадает), либо создать дубликат, что приведёт к путанице. Надежнее удалить старый узел из
|
||||
кластера:
|
||||
```bash
|
||||
sudo k3s kubectl delete node opi5plus-1
|
||||
```
|
||||
|
||||
Теперь состояние узлов:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-2 Ready control-plane,etcd,master 3d2h v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 2d v1.31.5+k3s1
|
||||
```
|
||||
|
||||
После удаления узла, проверим состояние подов кластера (правильнее, конечно, было бы проверить поды до удаления узла,
|
||||
но, допустим, мы имитировали ситуацию "смерти" узла):
|
||||
```bash
|
||||
sudo k3s kubectl get pods -n kube-system -o wide
|
||||
```
|
||||
|
||||
Увидим:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-ccb96694c-tfjwj 1/1 Running 0 4d19h 10.42.0.4 opi5plus-2 <none> <none>
|
||||
helm-install-traefik-crd-bdbgd 0/1 Completed 0 4d19h <none> opi5plus-2 <none> <none>
|
||||
helm-install-traefik-mlztm 0/1 Completed 1 4d19h <none> opi5plus-2 <none> <none>
|
||||
local-path-provisioner-5cf85fd84d-jwz5n 1/1 Running 0 4d19h 10.42.0.3 opi5plus-2 <none> <none>
|
||||
metrics-server-5985cbc9d7-n9dwz 1/1 Running 0 4d19h 10.42.0.2 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-h7b9c 3/3 Running 0 2d18h 10.42.0.9 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-nhz65 3/3 Running 0 38h 10.42.2.2 opi5plus-1 <none> <none>
|
||||
svclb-traefik-4f8c2580-qmzf6 3/3 Running 0 2d18h 10.42.1.5 opi5plus-3 <none> <none>
|
||||
traefik-6c979cd89d-98fk8 1/1 Terminating 0 2d15h 10.42.1.6 opi5plus-3 <none> <none>
|
||||
traefik-6c979cd89d-t4rhw 1/1 Running 0 38h 10.42.2.3 opi5plus-1 <none> <none>
|
||||
```
|
||||
|
||||
Если бы у нас были рабочие поды на удаленном узле, то они бы перезапустились на других нодах. Но, у нас там был только
|
||||
`svclb-traefik`, который теперь стал в статусе `Terminating`. Это процесс удаления пода. Kubernetes не сразу удаляет
|
||||
поды, особенно если они находятся в состоянии "зависания" (например, `Terminating` или `Running`, но стали недоступны).
|
||||
Так как агент удалён вместе с узлом, то некому сообщить кластеру, что под завершил работу, и он остается "призраком"
|
||||
в списке. Удалим под `svclb-traefik` вручную (не забудьте заменить `xxxxxxxxx-xxxxx` на реальные значения
|
||||
`<хеш-ревизии>`и `<суффикс>`):
|
||||
```bash
|
||||
sudo k3s kubectl delete pod svclb-traefik-xxxxxxxxx-xxxxx -n kube-system --force --grace-period=0
|
||||
```
|
||||
|
||||
Здесь `--force` и `--grace-period=0` говорят Kubernetes удалить под "форсированно" и "немедленно". Даже если узел
|
||||
недоступен. Так как это DaemonSet, он не перезапустится на opi5plus-1, потому что узел уже NotReady.
|
||||
|
||||
## Добавление третьего мастера
|
||||
|
||||
Теперь у нас осталось две мастер-ноды и можно добавить третий мастер. Как это сделать, см выше. Но теперь
|
||||
при добавлении можно в флаге `--server` указать IP как первого, так и второго мастера. И не забудьте в `--tls-san`
|
||||
указать IP хоста нового (третьего) мастера.
|
||||
|
||||
### Тюнинг kube-dns
|
||||
|
||||
После установки можно попробовать отключить один из мастеров и убедиться, что кластер остаётся работоспособным,
|
||||
а спустя некоторое время (иногда 10-15 минут) поды с погашенного мастера перезапустятся на других нодах. Например:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-ccb96694c-wzh96 1/1 Running 0 101m 10.42.1.8 opi5plus-3 <none> <none>
|
||||
local-path-provisioner-5cf85fd84d-s9frj 1/1 Running 0 101m 10.42.1.9 opi5plus-3 <none> <none>
|
||||
metrics-server-5985cbc9d7-q525g 1/1 Terminating 0 101m 10.42.2.4 opi5plus-1 <none> <none>
|
||||
metrics-server-5985cbc9d7-v8vlt 1/1 Running 0 29m 10.42.0.12 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-h7b9c 3/3 Running 3 (35m ago) 3d2h 10.42.0.10 opi5plus-2 <none> <none>
|
||||
svclb-traefik-4f8c2580-nhz65 3/3 Running 0 47h 10.42.2.2 opi5plus-1 <none> <none>
|
||||
svclb-traefik-4f8c2580-qmzf6 3/3 Running 3 (133m ago) 3d2h 10.42.1.7 opi5plus-3 <none> <none>
|
||||
traefik-6c979cd89d-t4rhw 1/1 Terminating 0 46h 10.42.2.3 opi5plus-1 <none> <none>
|
||||
traefik-6c979cd89d-z6wwm 1/1 Running 0 29m 10.42.0.11 opi5plus-2 <none> <none>
|
||||
```
|
||||
|
||||
Хотя, в целом, кластер остается рабочим, и сам чинится при отключении и восстановлении узлов, но если отключается нода
|
||||
на которой исполняется под с `coredns` — то временно будет затруднен перезапуска и создание новых подов, а значит
|
||||
и "переезд" подов с погашенного узла, до восстановления `coredns` тоже будет замедлен. Кроме того, если сценарий
|
||||
приложения(ий) развернутых внутри k3s предполагает переподключение с использованием имен подов или обнаружение подов,
|
||||
то это тоже перестанет работать.
|
||||
|
||||
Решением может быть использование двух реплик `coredns` (вместо одной). Откроем файл конфигурации k3s на редактирование:
|
||||
```bash
|
||||
sudo k3s kubectl edit deployment coredns -n kube-system
|
||||
```
|
||||
|
||||
Здесь:
|
||||
* `kubectl edit` — Открывает редактор (по умолчанию *vim*) для изменения ресурса Kubernetes напрямую в кластере.
|
||||
Вместо создания локального YAML-файла и применения его через `kubectl apply`, мы сразу редактируем "живой" конфиг.
|
||||
* `deployment coredns` — Указывает, что редактируем объект типа *deployment* с именем `coredns`. Deployment — это
|
||||
контроллер, который управляет набором подов (в данном случае coredns), обеспечивая их количество (реплики),
|
||||
перезапуск и обновления.
|
||||
* `-n kube-system` — Указывает пространство имён (namespace), где находится *coredns8. В k3s системные компоненты,
|
||||
к которым относится *coredns(, обычно живут в kube-system.
|
||||
|
||||
В открывшемся окне найдем строку `replicas: 1` и заменим её на `replicas: 2`.
|
||||
```yaml
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 2
|
||||
revisionHistoryLimit: 0
|
||||
```
|
||||
|
||||
Сохраним изменения и выйдем из редактора. Изменения сразу применятся, и k3s создаст вторую реплику `coredns`:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-ccb96694c-n4qsp 0/1 ContainerCreating 0 5s <none> opi5plus-1 <none> <none>
|
||||
coredns-ccb96694c-wzh96 1/1 Running 0 3h10m 10.42.1.8 opi5plus-3 <none> <none>
|
||||
…
|
||||
```
|
||||
|
||||
А затем:
|
||||
```text
|
||||
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
|
||||
coredns-ccb96694c-n4qsp 1/1 Running 0 15s 10.42.2.6 opi5plus-1 <none> <none>
|
||||
coredns-ccb96694c-wzh96 1/1 Running 0 3h10m 10.42.1.8 opi5plus-3 <none> <none>
|
||||
…
|
||||
```
|
||||
|
||||
**Как это будет работать?** Обе реплики `coredns` привязаны к сервису `kube-dns` в пространстве имён `kube-system`.
|
||||
Он имеет фиксированный *Cluster IP* (внутренний IP-адрес кластера) и балансирует запросы между всеми зарегистрированными
|
||||
подами `coredns` (у нас теперь две реплики). Каждый под `coredns` регистрируется как endpoint в `kube-dns` при старте.
|
||||
|
||||
Посмотеть endpoint'ы сервиса `kube-dns` можно командой:
|
||||
```bash
|
||||
sudo k3s kubectl get endpoints kube-dns -n kube-system
|
||||
```
|
||||
|
||||
И увидим, что у `kube-dns` несколько endpoint'ов (IP-адресов подов `coredns`) включая оба новых и старые, которые
|
||||
гасили при экспериментах с устойчивостью кластера:
|
||||
```text
|
||||
NAME ENDPOINTS AGE
|
||||
kube-dns 10.42.1.8:53,10.42.2.6:53,10.42.1.8:53 + 3 more… 5d23h
|
||||
```
|
||||
|
||||
Каждый под `coredns` — самостоятельный DNS-сервер. Они не взаимодействуют друг с другом и не обмениваются данными. Это
|
||||
просто экземпляры одного и того же сервиса, работающие параллельно. Они независимы, получают данные из API Kubernetes
|
||||
и отвечают на запросы параллельно. В каждом поде кластера в качестве DNS настроен `kube-dns` (задаётся в файле
|
||||
`/etc/resolv.conf` внутри пода). Когда под отправляет DNS-запрос, его получит `kube-dns` и перенаправит запрос
|
||||
к одному из доступных `coredns`. Балансировка происходит по случайного выбора (Round-Robin). Если один из `coredns`
|
||||
недоступен (например, узел выключен), `kube-dns` не получит ответа, и направит запросы к живому `coredns`.
|
||||
|
||||
### Разные архитектуры на узлах кластера (гетерогенность)
|
||||
|
||||
Когда мы подключили узлы (мастеры и воркеры) к кластеру, мы использовали одинаковые Orange Pi 5 Plus. Но, в реальности,
|
||||
кластеры Kubernetes часто состоят из узлов с разными архитектурами и характеристиками. Например, если подключить к
|
||||
к кластеру Raspberry Pi 3B увидим примерно такую картину:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-1 Ready control-plane,etcd,master 3d3h v1.31.5+k3s1
|
||||
opi5plus-2 Ready control-plane,etcd,master 6d3h v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 5d1h v1.31.5+k3s1
|
||||
rpi3b Ready <none> 27s v1.31.6+k3s1
|
||||
```
|
||||
|
||||
Но надо помнить, что разные архитектуры могут быть оказаться несовместимы с некоторыми приложениями и образами.
|
||||
Например, Raspberry Pi 3B — это 32-битный ARMv7 (armv7l), а Orange Pi 5 Plus — 64-битный ARMv8 (aarch64). Если в
|
||||
подах используются бинарные файлы, скомпилированные под определённую архитектуру, то они могут не работать на узлах
|
||||
с другой архитектурой. Также, некоторые образы Docker могут быть доступны только для определённых архитектур.
|
||||
|
||||
В ограниченном объеме можно подключать узлы на других платформах. Например, Windows, может иметь только воркер-узлы на k8s
|
||||
(начиная с версии 1.14), а в k3s экспериментальная поддержка Windows-воркеров (начиная с с версии 1.24). На macOS нет
|
||||
официальной поддержки Kubernetes/k3s для узлов на macOS (можно использовать обходные пути с использованием виртуальныех
|
||||
машин).
|
||||
|
||||
> **На всякий случай:**
|
||||
>
|
||||
> Если для вашего Kubernetes-кластера требуется блочное хранилище `longhorn` (для обеспечения репликации файлов между узлами кластера и высокой доступности данных), то понадобится модуль `iSCSI` (_Internet Small Computer System Interface_) на уровне системы. В составе Ubuntu 22.04 для Orange Pi 5 этого модуля нет. Потребуется [компиляция ядра](opi5plus-rebuilding-linux-kernel-for-iscsi.md).
|
||||
|
||||
### Добавление узлов во "внешнем" интернете
|
||||
|
||||
В моем проекте (специализированном поисковике) будет нужно парсить и интернет сайты, включая заблокированные сайты.
|
||||
К сожалению современный интернет имеет взаимные региональные блокировки и просто использовать VPN интернет-соединения
|
||||
не сработает. Выходом может стать использование воркер-узлов во внешнем интернете. Идея в том, что если какой-нибудь
|
||||
URL не получится обработать на поде одного узла, то можно попробовать обработать его на другом узле, в другой локации.
|
||||
|
||||
#### Настройка Keepalived
|
||||
|
||||
Так как узлы k3s взаимодействуют через API на 6443-порте, то для доступа к кластеру из внешнего интернета нужно будет
|
||||
обеспечить проброс трафика через роутер сети на один из мастер-узлов. НО у нас три мастер-узла, а значит если упадет
|
||||
узел на который происходит проброс, то удаленный воркер-узел "отвелится" и потеряет доступ к кластеру. Объединить
|
||||
IP всеx мастер-узлов в один можно с помощью балансировщика нагрузки **Keepalived**. Он создает виртуальный IP-адрес
|
||||
(VIP), c которого перенапрвляет трафик на один из мастер-узлов, и если этот узел упадет, то трафик перенаправится
|
||||
на другой и так далее.
|
||||
|
||||
Установи `Keepalived` на все мастер-ноды:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install keepalived
|
||||
```
|
||||
|
||||
Настроим `Keepalived` последовательно на каждом мастере. Для этого отредактируем (создадим) файл конфигурации
|
||||
`/etc/keepalived/keepalived.conf`:
|
||||
```bash
|
||||
sudo nano /etc/keepalived/keepalived.conf
|
||||
```
|
||||
|
||||
На первом мастер-узле (хост — `opi5plus-1`, IP — `192.168.1.26`):
|
||||
```pycon
|
||||
vrrp_instance VI_1 {
|
||||
state MASTER # ЭТО ГЛАВНЫЙ ХОСТ. ПО УМОЛЧАНИЮ ТРАФИК С VIP БУДЕТ ПЕРЕНАПРАВЛЯТЬСЯ НА ЭТОТ ХОСТ
|
||||
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
|
||||
virtual_router_id 51
|
||||
priority 100 # Самый высокий приоритет
|
||||
advert_int 1
|
||||
unicast_src_ip 192.168.1.26 # IP текущего хоста (opi5plus-1)
|
||||
unicast_peer {
|
||||
192.168.1.27 # IP второго хоста (opi5plus-2)
|
||||
192.168.1.28 # IP третьего хоста (opi5plus-3)
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.1.200 # Виртуальный IP (VIP), он должен быть исключен из диапазона DHCP
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
На втором мастер-узле (хост — `opi5plus-2`, IP — `192.168.1.27`):
|
||||
```pycon
|
||||
vrrp_instance VI_1 {
|
||||
state BACKUP # ЭТО ВТОРОЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ ХОСТ УПАДЕТ
|
||||
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и хост подключен по интерфейсу enP4p65s0
|
||||
virtual_router_id 51
|
||||
priority 90 # Меньший приоритет
|
||||
advert_int 1
|
||||
unicast_src_ip 192.168.1.27 # IP текущего хоста (opi5plus-2)
|
||||
unicast_peer {
|
||||
192.168.1.26 # IP первого хоста (opi5plus-1)
|
||||
192.168.1.28 # IP третьего хоста (opi5plus-3)
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.1.200 # Виртуальный IP
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
И, наконец, на третьем мастер-узле (хост — `opi5plus-3`, IP — `192.168.1.28`):
|
||||
```pycon
|
||||
vrrp_instance VI_1 {
|
||||
state BACKUP # ЭТО ТРЕТИЙ ХОСТ. ОН БУДЕТ ПОЛУЧАТЬ ТРАФИК С VIP, ЕСЛИ ГЛАВНЫЙ- И БЭКАП-ХОСТ УПАДЕТ
|
||||
interface enP4p65s0 # У Orange Pi 5 plus два интерфейса, и этот узел подключен по enP4p65s0
|
||||
virtual_router_id 51
|
||||
priority 80 # Еще меньший приоритет
|
||||
advert_int 1
|
||||
unicast_src_ip 192.168.1.28 # IP текущего хоста (opi5plus-3)
|
||||
unicast_peer {
|
||||
192.168.1.27 # IP первого хоста (opi5plus-1)
|
||||
192.168.1.28 # IP второго хоста (opi5plus-2)
|
||||
}
|
||||
virtual_ipaddress {
|
||||
192.168.1.200 # Виртуальный IP
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Добавим `Keepalived` в автозагрузку на всех мастер-узлах и запустим его:
|
||||
```bash
|
||||
sudo systemctl enable keepalived
|
||||
sudo systemctl start keepalived
|
||||
```
|
||||
|
||||
Теперь, если вы на первом мастер-узле (opi5plus-1) проверить доступные IP-адреса:
|
||||
```bash
|
||||
ip addr show
|
||||
```
|
||||
|
||||
то увидим:
|
||||
```text
|
||||
…
|
||||
…
|
||||
2: enP4p65s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
|
||||
link/ether c0:74:2b:fd:42:3c brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.1.26/24 brd 192.168.1.255 scope global dynamic noprefixroute enP4p65s0
|
||||
valid_lft 68779sec preferred_lft 68779sec
|
||||
inet 192.168.1.200/32 scope global enP4p65s0
|
||||
valid_lft forever preferred_lft forever
|
||||
…
|
||||
```
|
||||
Обратите внимание на виртуальный IP-адрес `192.168.1.200` находится в другой подсети (CIDR) и имеет маску `/32` (то
|
||||
есть с маской подсети `255.255.255.255`). Это "точечная" подсеть, содержащая только один адрес, не привязан к основной
|
||||
подсети интерфейса (/24) и это позволяет VIP "плавать" между узлами, не вызывая конфликтов с основными IP-адресами
|
||||
и не требуя изменения подсети на каждом узле. VIP рассматривается как уникальный адрес, не требующий маршрутизации,
|
||||
он просто "привязан" к интерфейсу.
|
||||
|
||||
Теперь панель `Traefik` доступна по VIP-адресу `http://192.168.1.200:9000/dashboard/#`, т.к. трафик с этого адреса
|
||||
будет перенаправлен на один из мастер-узлов.
|
||||
|
||||
API Kubernetes тоже теперь доступен по VIP-адресу. Все воркер-узлы, подключенные к кластеру, лучше подключать к
|
||||
кластеру через VIP-адрес. Сами мастер узлы знают свои IP и взаимодействую через `etcd`, но воркеры подключаясь
|
||||
через VIP будут более устойчивы к сбоям мастер-узлов. Подсоединить удаленный воркер-узел к кластеру лучше через VIP.
|
||||
Для этого нужно на роутере сети настроить проброс порта `6443` с внешнего IP роутера, на виртуальный IP-адрес внутри
|
||||
сети (тоже на `6443` порт). После проверить, что с внешнего хоста API Kubernetes доступно:
|
||||
```bash
|
||||
curl -k https://<PUBLIC_IP_ROUTER>:6443
|
||||
```
|
||||
|
||||
Если отклик есть (например, `Unauthorized`), то можно подключить удаленый воркер-узел к кластеру:
|
||||
```bash
|
||||
curl -sfL https://get.k3s.io | sh -s - agent --server https://<PUBLIC_IP_ROUTER>:6443 --token <TOKEN>
|
||||
```
|
||||
|
||||
Когда процесс завершится, на любом мастер-узле можно проверить, что воркер-узел подключился:
|
||||
```bash
|
||||
sudo k3s kubectl get nodes
|
||||
```
|
||||
|
||||
Получим, например:
|
||||
```text
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
opi5plus-1 Ready control-plane,etcd,master 1d4h v1.31.5+k3s1
|
||||
opi5plus-2 Ready control-plane,etcd,master 1d v1.31.5+k3s1
|
||||
opi5plus-3 Ready control-plane,etcd,master 1d2h v1.31.5+k3s1
|
||||
rpi3b Ready <none> 25h v1.31.6+k3s1
|
||||
vps-sw-eye Ready <none> 35m v1.31.6+k3s1
|
||||
```
|
||||
|
||||
#### Проблема Flannel для внешних узлов
|
||||
|
||||
Узлы во внешнем интернете создаются и управляются через Kubernetes API, используя порт `6443/TCP`. Но, для того чтобы
|
||||
передавать трафик и данные между узлами k3s использует сетевой плагин **Flannel**. Он встроен в бинарник k3s (в отличие
|
||||
от k8s, где Flannel работают как поды) и использует overlay-сеть (`10.42.x.x`). Это внутренняя сеть k3s, VXLAN-туннель
|
||||
между всеми узлами кластера (мастерами и воркерами). Flannel использует порт `8472/UDP`.
|
||||
|
||||
К сожалению проброс порта `8472` с внешнего хоста в домашнюю сеть через роутер не поможет, так обмен идёт не через TCP-,
|
||||
а UDP-порт. Внешний узел отправит пакеты через overlay-сеть Flannel (10.42.x.x) через 8472/UDP к мастеру, но Мастер
|
||||
отвечает через свой реальный IP (192.168.1.x), который недоступен напрямую из интернета без обратного проброса или VPN.
|
||||
Проброс <PUBLIC_IP>:8472 → <VIP>>:8472 позволяет трафику от внешнего хоста доходить до домашней сети, но ответный
|
||||
трафик от мастеров к VPS (например, от <NODE-1>:8472 к <VPS_IP>:8472) не будет проходить, потому что NAT в роутере
|
||||
"односторонний" — он не знает, как маршрутизировать UDP-ответы от мастеров обратно к VPS через интернет.
|
||||
|
||||
Таким образом, для управления удаленным узлом нужно чтобы он имел локальный IP-адрес в домашней сети, а не внешний.
|
||||
SSH-тоннель с помощью `autossh` и упаковкой UDP-трафика в TCP через `socat` не сработает (а я надеялся). Таким образом
|
||||
"пробросить" Flannel для полноценного подключения удаленного k3s-узла — VPN-туннель между каждой мастер-нодой на
|
||||
удаленный узел. Это вполне рабочия вариант, если удаленные узлы — полноценные и произвольные хосты. Но в моём
|
||||
случае удаленный узел — хост на 1 ядро и 1 ГБ ОЗУ. К тому же он на платформе x86_64, а не ARM, а значит ради одного
|
||||
узла не стоит заморачиваться с VPN.
|
||||
|
||||
Другим вариантом является подключение внутри самих подов на удаленном узле к необходимым сервисам напрямую. Но таким
|
||||
образом порты базы данных, менеджеров очередей и т.д. будут доступны из интернета, что небезопасно и в целом плохая идея.
|
||||
|
||||
|
||||
|
||||
-----
|
||||
Cертификаты
|
||||
sudo kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml
|
||||
|
||||
PVC Longhorn
|
||||
sudo kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.5.3/deploy/longhorn.yaml
|
||||
|
||||
Установить Helm (менеджер пакетов для Kubernetes):
|
||||
|
||||
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
Добавить репозиторий Traefik в Helm:
|
||||
helm repo add traefik https://helm.traefik.io/traefik
|
||||
|
||||
Обновить репозитории:
|
||||
helm repo update
|
||||
|
||||
|
||||
|
||||
Установить Traefik:
|
||||
```bash
|
||||
helm install traefik traefik/traefik -n kube-system --set service.type=LoadBalancer
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
1305
raspberry-and-orange-pi/k8s.md
Normal file
448
raspberry-and-orange-pi/measuring-performance-storage-devices.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# Измерение скорости накопителей (HDD, SSD, USB Flash, NAS и т.д.) под Linux
|
||||
|
||||
Способов измерения скорости накопителей под Linux много, и можно подобрать такие, что более точно покажут
|
||||
производительность именно на ваших задачах.
|
||||
|
||||
|
||||
## Самые простые измерения с помощью `dd`
|
||||
|
||||
Официально в Unix/Linux не существует строгого определения, что такое `dd`. Утилита берёт свое название
|
||||
из команды "Data Definition" в языке описания данных IBM JCL (Job Control Language) для мейнфреймов.
|
||||
Название намекает на ее основное предназначение — копирование и преобразование данных.
|
||||
|
||||
Использование `dd` для измерения скорости чтения и записи диска — не очень точные, т.к. это, по сути,
|
||||
обычное копирование данных. Не учитываются такие факторы, такие как кэширование операционной системы,
|
||||
фрагментация диска и т.д.
|
||||
|
||||
Тем не менее, `dd` подходит для быстрого теста. Вот, например, как это сделать:
|
||||
|
||||
#### Запись (Write)
|
||||
```shell
|
||||
dd if=/dev/zero of=/path/to/testfile bs=1G count=1 oflag=direct
|
||||
```
|
||||
|
||||
Где:
|
||||
* `if=/dev/zero` — указывает, что источник данных — это непрерывный поток нулей;
|
||||
* `of=/path/to/testfile` — задает файл, в который записываются данные. Убедитесь, что путь указывает на тестируемый диск;
|
||||
* `bs=1G` — размер блока (здесь 1 гигабайт за раз);
|
||||
* `count=1` — количество блоков для записи;
|
||||
* `oflag=direct` — обходит кэш операционной системы, что обеспечивает более точное измерение.
|
||||
|
||||
|
||||
Результат для моего SSD NVMe Kingston SNV2S/2000G в Orange Pi 5 Plus:
|
||||
```text
|
||||
1+0 records in
|
||||
1+0 records out
|
||||
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.01244 s, 1.1 GB/s
|
||||
```
|
||||
|
||||
Здесь 1.1 GB/s — это скорость записи. Результат для eMCC памяти Orange Pi 5 Plus:
|
||||
```text
|
||||
1+0 records in
|
||||
1+0 records out
|
||||
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 4.76196 s, 225 MB/s
|
||||
```
|
||||
|
||||
Как видим, скорость записи NVMe SSD более чем в 4 раза быстрее, чем у eMMC.
|
||||
|
||||
### Чтение (Read)
|
||||
|
||||
Теперь у нас есть тестовый файл, который мы можем использовать для измерения скорости чтения:
|
||||
|
||||
```shell
|
||||
dd if=/path/to/testfile of=/dev/null bs=1G iflag=direct
|
||||
```
|
||||
|
||||
Где:
|
||||
* `if=/path/to/testfile` — файл, созданный в предыдущем тесте.
|
||||
* `of=/dev/null` — данные не сохраняются, что позволяет измерять только скорость чтения.
|
||||
* `bs=1G` — размер блока.
|
||||
* `iflag=direct` — обходит кэш операционной системы.
|
||||
|
||||
Результат для SSD NVMe Kingston SNV2S/2000G в Orange Pi 5 Plus:
|
||||
```text
|
||||
1+0 records in
|
||||
1+0 records out
|
||||
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.74686 s, 1.4 GB/s
|
||||
```
|
||||
|
||||
Для eMMC Orange Pi 5 Plus:
|
||||
```text
|
||||
1+0 records in
|
||||
1+0 records out
|
||||
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 3.8901 s, 276 MB/s
|
||||
```
|
||||
|
||||
Как видим, скорость чтения NVMe SSD более чем в 5 раз выше, чем у eMMC.
|
||||
|
||||
### Советы по тестированию с помощью `dd`
|
||||
|
||||
* Используйте достаточно большой файл чтобы результаты были более репрезентативными. В идеале размер файла должен превышать объем оперативной памяти (правда это не всегда возможно).
|
||||
* Очистите кэш перед тестом чтения: `sync; echo 3 > /proc/sys/vm/drop_caches`. Это предотвратит влияние системного кэша на тест.
|
||||
* Помните: `dd` не моделирует реальное рабочее окружение. Результаты могут отличаться от повседневной нагрузки на диск.
|
||||
* Не забудьте удалить тестовый файл после теста: `rm /path/to/testfile`
|
||||
|
||||
-----
|
||||
|
||||
## Измерение скорости операций ввода/вывода c помощью `ioping`
|
||||
|
||||
Исходя тз названия, казалось бы, утилита `ioping` проверяет задержки накопителей данных. И это справедливо для NAS. Она похож на ping, но для дисков. Но в реальности `ioping` — это более универсальный инструмент, который может использоваться для измерения скорости операций ввода/вывода (IOPS) и пропускной способности дисков.
|
||||
|
||||
Установим ioping:
|
||||
```shell
|
||||
sudo apt install ioping
|
||||
```
|
||||
|
||||
Замер:
|
||||
```shell
|
||||
ioping -c 10 -s 1m -W .
|
||||
```
|
||||
|
||||
Где:
|
||||
* `-c 10` — количество замеров;
|
||||
* `-s 10m` — размер блока данных для тестирования. `10m` — 10 мегабайт. В целом `ioping` — это тест производительности дисковой подсистемы, через измерения задержек при доступе к небольшим блокам данных (обычно 4 KiB). Но отлично работает и на больших блоках, оказывая производительность диска в целом.
|
||||
* `-W` — С записью вместо чтения. Это более безопасно для временного файла в целевом каталоге, и, кроме того, запись ввода-вывода даёт более надежные результаты для систем, где некэшированные чтения не поддерживаются
|
||||
или происходит искажающее замеры кэширование на каком-то уровне.
|
||||
* `.` — каталог, в котором будут производиться замеры. `.` — текущий каталог.
|
||||
|
||||
Другие ключи и опции можно посмотреть в справке `man ioping` или `ioping --help`.
|
||||
|
||||
Результаты теста будут примерно такими:
|
||||
```text
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=1 time=21.1 ms (warmup)
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=2 time=29.7 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=3 time=23.5 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=4 time=22.3 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=5 time=21.7 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=6 time=23.1 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=7 time=21.7 ms (fast)
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=8 time=31.1 ms (slow)
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=9 time=23.0 ms
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=10 time=22.7 ms
|
||||
|
||||
--- . (ext4 /dev/nvme0n1p2 1.79 TiB) ioping statistics ---
|
||||
9 requests completed in 218.8 ms, 90 MiB written, 41 iops, 411.3 MiB/s
|
||||
generated 10 requests in 9.04 s, 100 MiB, 1 iops, 11.1 MiB/s
|
||||
min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms
|
||||
```
|
||||
|
||||
Сверху выводятся данные по индивидуальным запросам:
|
||||
```text
|
||||
10 MiB >>> . (ext4 /dev/nvme0n1p2 1.79 TiB): request=1 time=21.1 ms (warmup)
|
||||
```
|
||||
|
||||
Где:
|
||||
* `10 MiB` — размер блока данных, к которому идет доступ;
|
||||
* `<<< .` — текущая директория, на которой проводится тест;
|
||||
* `(ext4 /dev/nvme0n1p2 1.79 TiB)` — файловая система (ext4) и устройство (/dev/nvme0n1p2) с емкостью 1.79 ТБ, где выполняется тест;
|
||||
* `request=1` — номер запроса;
|
||||
* `time=21.1 ms` — время выполнения запроса (в миллисекундах — `ms`, в микросекунды — `us`);
|
||||
* `(warmup)` — первый запрос может использоваться для "прогрева", его результаты могут быть исключены из финальной статистики, `(fast)` — запрос выполнен быстрее, чем среднее время выполнения, `(slow)` — запрос выполнен медленнее, чем среднее время выполнения.
|
||||
|
||||
Снизу выводятся статистические данные по всем запросам:
|
||||
```text
|
||||
9 requests completed in 218.8 ms, 90 MiB written, 41 iops, 411.3 MiB/s
|
||||
generated 10 requests in 9.04 s, 100 MiB, 1 iops, 11.1 MiB/s
|
||||
min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms
|
||||
```
|
||||
|
||||
Где:
|
||||
* `9 requests completed in 218.8 ms` — количество запросов, завершенных за время теста и общее время выполнения;
|
||||
* `90 MiB written` — общий объем данных, записанных на диск;
|
||||
* `41 iops` — количество операций ввода/вывода в секунду (IOPS);
|
||||
* `411.3 MiB/s` — скорость записи данных на диск;
|
||||
* `generated 10 requests in 9.04 s` — количество запросов, сгенерированных за время теста и общее время выполнения;
|
||||
* `100 MiB` — общий объем данных, сгенерированных тестом;
|
||||
* `1 iops` — количество операций ввода/вывода в секунду (IOPS) при генерации данных;
|
||||
* `11.1 MiB/s` — скорость генерации данных;
|
||||
* `min/avg/max/mdev = 21.7 ms / 24.3 ms / 31.1 ms / 3.32 ms` — **минимальное**, **среднее**, **максимальное** задержка выполнения запросов и **стандартное отклонение**, показатель того, насколько задержки разнятся между запросами.
|
||||
|
||||
-----
|
||||
|
||||
## Тесты накопителей с помощью `fio`
|
||||
|
||||
`fio` (Flexible I/O Tester) — мощный инструмент для тестирования производительности систем ввода-вывода (I/O) на
|
||||
дисках и файловых системах. Он гибко настраивается, поддерживает разные режимы ввода-вывода, размеры блоков,
|
||||
параллелизм и типы нагрузок. Используется для оценки скорости чтения/записи, задержек и IOPS (число операций
|
||||
ввода/вывода в секунду).
|
||||
|
||||
Установка fio:
|
||||
```shell
|
||||
sudo apt install fio
|
||||
```
|
||||
|
||||
Запуск теста (например тест с высокой нагрузкой, эмулирующий многопоточный доступ к диску с небольшими блоками данных):
|
||||
```shell
|
||||
fio --name=seq_nvme --size=10G --bs=1M --bs=4k --direct=1 --rw=randrw --rwmixread=70 --numjobs=4 --iodepth=32 --runtime=60
|
||||
```
|
||||
|
||||
Где:
|
||||
* `--name=seq_nvme` — имя теста.
|
||||
* `--size=10G` — размер файла для теста (10 ГБ).
|
||||
* `--bs=1M` — размер блока данных (1 МБ).
|
||||
* `--bs=4k` — размер блока данных (4 КБ).
|
||||
* `--direct=1` — обход кэша операционной системы.
|
||||
* `--rw=randrw` — тип операции (случайное чтение и запись). Также можно установить и другие параметры: `write` — тип операции (последовательная запись). `read` — последовательное чтение, `randwrite` — случайная запись, `randread` — случайное чтение, `rw` — смешанный режим (чтение и запись).
|
||||
* `--rwmixread=70` — процент операций чтения/запись (70% чтения, 30% записи).
|
||||
* `--numjobs=4` — количество потоков (заданий), которые одновременно выполняются.
|
||||
* `--iodepth=32` — количество операций ввода-вывода, которые отправляются одновременно.
|
||||
* `--runtime=60` — продолжительность теста (60 секунд), для длинных тестов используйте `--time_based` чтобы тест завершался по времени.
|
||||
|
||||
И так далее. Еще есть ключ `--output=test.log` для сохранения результатов в log-файл. Подробнее о параметрах можно посмотреть в справке `man fio` или `fio --help`.
|
||||
|
||||
Результаты выполнения вышеуказанной команды для моего NVMe SSD (Kingston SNV2S/2000G) были следующие:
|
||||
```text
|
||||
seq_nvme: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=32
|
||||
...
|
||||
fio-3.28
|
||||
Starting 4 processes
|
||||
Jobs: 4 (f=4): [m(4)][100.0%][r=33.4MiB/s,w=15.1MiB/s][r=8550,w=3861 IOPS][eta 00m:00s]
|
||||
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44678: Fri Nov 29 23:41:45 2024
|
||||
read: IOPS=2415, BW=9662KiB/s (9894kB/s)(566MiB/60001msec)
|
||||
clat (usec): min=60, max=41549, avg=298.88, stdev=321.06
|
||||
lat (usec): min=61, max=41550, avg=300.04, stdev=321.13
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 76], 5.00th=[ 99], 10.00th=[ 135], 20.00th=[ 165],
|
||||
| 30.00th=[ 190], 40.00th=[ 225], 50.00th=[ 281], 60.00th=[ 306],
|
||||
| 70.00th=[ 334], 80.00th=[ 371], 90.00th=[ 420], 95.00th=[ 486],
|
||||
| 99.00th=[ 1663], 99.50th=[ 1991], 99.90th=[ 2573], 99.95th=[ 3228],
|
||||
| 99.99th=[ 4490]
|
||||
bw ( KiB/s): min= 6888, max=12838, per=24.33%, avg=9668.65, stdev=1269.47, samples=119
|
||||
iops : min= 1722, max= 3209, avg=2417.03, stdev=317.35, samples=119
|
||||
write: IOPS=1033, BW=4133KiB/s (4232kB/s)(242MiB/60001msec); 0 zone resets
|
||||
clat (usec): min=19, max=21700, avg=224.76, stdev=261.15
|
||||
lat (usec): min=19, max=21702, avg=226.79, stdev=261.28
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 32], 5.00th=[ 33], 10.00th=[ 46], 20.00th=[ 76],
|
||||
| 30.00th=[ 106], 40.00th=[ 141], 50.00th=[ 174], 60.00th=[ 217],
|
||||
| 70.00th=[ 293], 80.00th=[ 334], 90.00th=[ 396], 95.00th=[ 453],
|
||||
| 99.00th=[ 1483], 99.50th=[ 1893], 99.90th=[ 2442], 99.95th=[ 2966],
|
||||
| 99.99th=[ 4047]
|
||||
bw ( KiB/s): min= 2784, max= 5516, per=24.23%, avg=4138.13, stdev=561.74, samples=119
|
||||
iops : min= 696, max= 1379, avg=1034.42, stdev=140.40, samples=119
|
||||
lat (usec) : 20=0.01%, 50=3.27%, 100=9.03%, 250=38.81%, 500=44.77%
|
||||
lat (usec) : 750=2.25%, 1000=0.21%
|
||||
lat (msec) : 2=1.21%, 4=0.44%, 10=0.01%, 20=0.01%, 50=0.01%
|
||||
cpu : usr=7.75%, sys=19.95%, ctx=206950, majf=0, minf=29
|
||||
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
issued rwts: total=144938,61997,0,0 short=0,0,0,0 dropped=0,0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=32
|
||||
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44679: Fri Nov 29 23:41:45 2024
|
||||
read: IOPS=2390, BW=9562KiB/s (9791kB/s)(560MiB/60001msec)
|
||||
clat (usec): min=63, max=22486, avg=293.54, stdev=282.59
|
||||
lat (usec): min=63, max=22487, avg=294.85, stdev=282.65
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 94], 5.00th=[ 129], 10.00th=[ 147], 20.00th=[ 165],
|
||||
| 30.00th=[ 188], 40.00th=[ 217], 50.00th=[ 247], 60.00th=[ 297],
|
||||
| 70.00th=[ 322], 80.00th=[ 363], 90.00th=[ 416], 95.00th=[ 482],
|
||||
| 99.00th=[ 1663], 99.50th=[ 2008], 99.90th=[ 2606], 99.95th=[ 3195],
|
||||
| 99.99th=[ 4359]
|
||||
bw ( KiB/s): min= 6944, max=13312, per=24.09%, avg=9573.84, stdev=1150.52, samples=119
|
||||
iops : min= 1736, max= 3328, avg=2393.38, stdev=287.64, samples=119
|
||||
write: IOPS=1027, BW=4110KiB/s (4209kB/s)(241MiB/60001msec); 0 zone resets
|
||||
clat (usec): min=20, max=41544, avg=236.88, stdev=347.28
|
||||
lat (usec): min=21, max=41545, avg=239.32, stdev=347.36
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 32], 5.00th=[ 41], 10.00th=[ 90], 20.00th=[ 115],
|
||||
| 30.00th=[ 137], 40.00th=[ 159], 50.00th=[ 186], 60.00th=[ 215],
|
||||
| 70.00th=[ 277], 80.00th=[ 334], 90.00th=[ 392], 95.00th=[ 453],
|
||||
| 99.00th=[ 1516], 99.50th=[ 1876], 99.90th=[ 2409], 99.95th=[ 3064],
|
||||
| 99.99th=[ 4113]
|
||||
bw ( KiB/s): min= 2904, max= 5680, per=24.10%, avg=4116.52, stdev=533.76, samples=119
|
||||
iops : min= 726, max= 1420, avg=1029.00, stdev=133.43, samples=119
|
||||
lat (usec) : 50=1.74%, 100=5.26%, 250=48.61%, 500=40.37%, 750=2.13%
|
||||
lat (usec) : 1000=0.21%
|
||||
lat (msec) : 2=1.22%, 4=0.45%, 10=0.01%, 20=0.01%, 50=0.01%
|
||||
cpu : usr=9.60%, sys=22.32%, ctx=205089, majf=0, minf=31
|
||||
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
issued rwts: total=143428,61649,0,0 short=0,0,0,0 dropped=0,0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=32
|
||||
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44680: Fri Nov 29 23:41:45 2024
|
||||
read: IOPS=2596, BW=10.1MiB/s (10.6MB/s)(609MiB/60001msec)
|
||||
clat (usec): min=65, max=40409, avg=284.56, stdev=285.63
|
||||
lat (usec): min=65, max=40410, avg=285.50, stdev=285.70
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 75], 5.00th=[ 96], 10.00th=[ 113], 20.00th=[ 159],
|
||||
| 30.00th=[ 186], 40.00th=[ 206], 50.00th=[ 262], 60.00th=[ 293],
|
||||
| 70.00th=[ 322], 80.00th=[ 359], 90.00th=[ 404], 95.00th=[ 474],
|
||||
| 99.00th=[ 1631], 99.50th=[ 1975], 99.90th=[ 2540], 99.95th=[ 3064],
|
||||
| 99.99th=[ 4146]
|
||||
bw ( KiB/s): min= 8015, max=14080, per=26.15%, avg=10392.20, stdev=1341.91, samples=119
|
||||
iops : min= 2003, max= 3520, avg=2597.87, stdev=335.53, samples=119
|
||||
write: IOPS=1116, BW=4465KiB/s (4572kB/s)(262MiB/60001msec); 0 zone resets
|
||||
clat (usec): min=24, max=41585, avg=198.90, stdev=319.52
|
||||
lat (usec): min=25, max=41587, avg=200.53, stdev=319.59
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 32], 5.00th=[ 32], 10.00th=[ 34], 20.00th=[ 59],
|
||||
| 30.00th=[ 75], 40.00th=[ 96], 50.00th=[ 145], 60.00th=[ 190],
|
||||
| 70.00th=[ 269], 80.00th=[ 314], 90.00th=[ 375], 95.00th=[ 429],
|
||||
| 99.00th=[ 1369], 99.50th=[ 1795], 99.90th=[ 2376], 99.95th=[ 2999],
|
||||
| 99.99th=[ 4490]
|
||||
bw ( KiB/s): min= 3401, max= 5984, per=26.16%, avg=4468.75, stdev=561.75, samples=119
|
||||
iops : min= 850, max= 1496, avg=1117.05, stdev=140.43, samples=119
|
||||
lat (usec) : 50=5.17%, 100=12.96%, 250=36.62%, 500=41.68%, 750=1.84%
|
||||
lat (usec) : 1000=0.19%
|
||||
lat (msec) : 2=1.12%, 4=0.41%, 10=0.01%, 20=0.01%, 50=0.01%
|
||||
cpu : usr=6.73%, sys=17.26%, ctx=222857, majf=0, minf=29
|
||||
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
issued rwts: total=155819,66981,0,0 short=0,0,0,0 dropped=0,0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=32
|
||||
seq_nvme: (groupid=0, jobs=1): err= 0: pid=44681: Fri Nov 29 23:41:45 2024
|
||||
read: IOPS=2531, BW=9.89MiB/s (10.4MB/s)(593MiB/60000msec)
|
||||
clat (usec): min=60, max=41793, avg=282.72, stdev=305.59
|
||||
lat (usec): min=60, max=41794, avg=283.89, stdev=305.67
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 76], 5.00th=[ 98], 10.00th=[ 131], 20.00th=[ 159],
|
||||
| 30.00th=[ 184], 40.00th=[ 200], 50.00th=[ 237], 60.00th=[ 285],
|
||||
| 70.00th=[ 318], 80.00th=[ 355], 90.00th=[ 408], 95.00th=[ 474],
|
||||
| 99.00th=[ 1631], 99.50th=[ 1975], 99.90th=[ 2540], 99.95th=[ 3064],
|
||||
| 99.99th=[ 4178]
|
||||
bw ( KiB/s): min= 7624, max=14408, per=25.52%, avg=10140.96, stdev=1641.40, samples=119
|
||||
iops : min= 1906, max= 3602, avg=2535.13, stdev=410.35, samples=119
|
||||
write: IOPS=1092, BW=4369KiB/s (4474kB/s)(256MiB/60000msec); 0 zone resets
|
||||
clat (usec): min=19, max=22481, avg=214.85, stdev=280.81
|
||||
lat (usec): min=19, max=22483, avg=216.97, stdev=280.96
|
||||
clat percentiles (usec):
|
||||
| 1.00th=[ 31], 5.00th=[ 32], 10.00th=[ 37], 20.00th=[ 77],
|
||||
| 30.00th=[ 113], 40.00th=[ 139], 50.00th=[ 165], 60.00th=[ 196],
|
||||
| 70.00th=[ 251], 80.00th=[ 322], 90.00th=[ 383], 95.00th=[ 445],
|
||||
| 99.00th=[ 1467], 99.50th=[ 1844], 99.90th=[ 2442], 99.95th=[ 3032],
|
||||
| 99.99th=[ 4490]
|
||||
bw ( KiB/s): min= 3192, max= 6072, per=25.61%, avg=4373.76, stdev=743.54, samples=119
|
||||
iops : min= 798, max= 1518, avg=1093.28, stdev=185.89, samples=119
|
||||
lat (usec) : 20=0.01%, 50=4.00%, 100=8.72%, 250=45.79%, 500=37.74%
|
||||
lat (usec) : 750=1.97%, 1000=0.19%
|
||||
lat (msec) : 2=1.16%, 4=0.41%, 10=0.01%, 20=0.01%, 50=0.01%
|
||||
cpu : usr=8.77%, sys=20.44%, ctx=217410, majf=0, minf=32
|
||||
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
|
||||
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
|
||||
issued rwts: total=151861,65532,0,0 short=0,0,0,0 dropped=0,0,0,0
|
||||
latency : target=0, window=0, percentile=100.00%, depth=32
|
||||
|
||||
Run status group 0 (all jobs):
|
||||
READ: bw=38.8MiB/s (40.7MB/s), 9562KiB/s-10.1MiB/s (9791kB/s-10.6MB/s), io=2328MiB (2441MB), run=60000-60001msec
|
||||
WRITE: bw=16.7MiB/s (17.5MB/s), 4110KiB/s-4465KiB/s (4209kB/s-4572kB/s), io=1001MiB (1049MB), run=60000-60001msec
|
||||
|
||||
Disk stats (read/write):
|
||||
nvme0n1: ios=594831/255707, merge=0/0, ticks=134992/39009, in_queue=174001, util=100.00%
|
||||
```
|
||||
|
||||
Разберем результаты выполнения вашего fio-теста о основным метрикам и объясним их значение.
|
||||
|
||||
#### Общий результат (Run status group 0):
|
||||
* Чтение (READ):
|
||||
* Скорость: 38.8 MiB/s (40.7 MB/s) — средняя скорость чтения всех заданий.
|
||||
* Диапазон: 9562 KiB/s - 10.1 MiB/s — скорости отдельных потоков.
|
||||
* Объем данных: 2328 MiB (около 2.44 GB).
|
||||
* Запись (WRITE):
|
||||
* Скорость: 16.7 MiB/s (17.5 MB/s).
|
||||
* Диапазон: 4110 KiB/s - 4465 KiB/s.
|
||||
* Объем данных: 1001 MiB (около 1.049 GB).
|
||||
|
||||
#### Метрики на потоки
|
||||
|
||||
Для каждого потока представлены следующие данные:
|
||||
|
||||
* Чтение (READ)
|
||||
* IOPS — Средние значения IOPS (операции в секунду) для потоков составляют, например, 2415 IOPS для первого задания. Это число показывает, сколько операций чтения в секунду выполнялось.
|
||||
* Latency (задержка) — Время отклика операций ввода-вывода (чтение). Например, среднее время отклика (clat) составляет 298.88 микросекунд, а 99-й процентиль (99.00th) — 1663 микросекунды. Это означает, что 99% операций чтения завершаются за это время.
|
||||
* Bandwidth (пропускная способность) — Средние скорости чтения: 9.66 MiB/s, 9.57 MiB/s и т.д.
|
||||
* Запись (WRITE)
|
||||
* IOPS — Средние значения: около 1033-1117 IOPS. Меньше, чем для чтения из-за меньшего процента операций записи.
|
||||
* Latency (задержка) — Средняя задержка: около 200-230 микросекунд. Процентиль 99%: до 1483-4490 микросекунд.
|
||||
* Bandwidth (пропускная способность) — Средняя скорость записи: 4.1-4.4 MiB/s.
|
||||
|
||||
#### Сводка задержек (Latency Summary)
|
||||
|
||||
* Чтение: 50% операций завершались за ~281 микросекунду (медиана). 99.5% операций — за менее чем 1991 микросекунду.
|
||||
* Запись:- 50% операций завершались за ~174-186 микросекунд. 99% — менее чем за 1467 микросекунд.
|
||||
|
||||
#### Нагрузка на диск (Disk Utilization)
|
||||
|
||||
* Число операций: 594831 операций чтения и 255707 операций записи. Диск был загружен на 100% времени, что ожидаемо для высокой глубины очереди (iodepth=32).
|
||||
|
||||
#### Общие выводы по M.2 SSD Kingston SNV2S/2000G установленном в мой Orange Pi 5 Plus
|
||||
|
||||
Для случайных операций на маленьких блоках (4 КБ) Kingston SNV2S/2000G демонстрирует хорошую производительность (средние 40.7 MB/s чтения и 17.5 MB/s записи). IOPS соответствуют значениям для высокопроизводительных NVMe SSD. Средние задержки в районе 200-300 микросекунд — это нормально для NVMe. Процентиль 99% показывает стабильность: отклонения есть, но они незначительны. Полная загрузка диска (util=100%) свидетельствует о том, что диск эффективно обрабатывает запросы в рамках установленной глубины очереди.
|
||||
|
||||
### Чистка
|
||||
|
||||
Не забудьте удалить файлы `seq_nvme.*` после завершения теста, чтобы освободить место на диске.
|
||||
```shell
|
||||
rm seq_nvme.*
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
## Измерение производительности дисков, RAID-массивов и сравнение скорости работы разных файловых систем (ext4, XFS, btrfs и др.) с помощью `bonnie++`
|
||||
|
||||
`bonnie++` — это утилита для тестирования производительности файловой системы, которая позволяет оценить скорость чтения/записи, IOPS (число операций ввода/вывода в секунду), задержки и другие параметры. Она позволяет провести тестирование на различных уровнях нагрузки и с разными параметрами.
|
||||
|
||||
Установка bonnie++:
|
||||
```shell
|
||||
sudo apt install bonnie++
|
||||
```
|
||||
|
||||
Запуск теста (например):
|
||||
```shell
|
||||
bonnie++ -s 8192 -r 4096 -u $(whoami)
|
||||
```
|
||||
|
||||
Где:
|
||||
* `-s 8192` — размер файла для теста (8 ГБ).
|
||||
* `-r 4096` — размер блока данных (4 КБ).
|
||||
* `-u $(whoami)` — имя пользователя, от имени которого запускается тест.
|
||||
|
||||
Результаты выполнения вышеуказанной команды для моего NVMe SSD (Kingston SNV2S/2000G) в Orange Pi 5 Plus были следующие:
|
||||
```text
|
||||
Version 2.00 ------Sequential Output------ --Sequential Input- --Random-
|
||||
-Per Chr- --Block-- -Rewrite- -Per Chr- --Block-- --Seeks--
|
||||
Name:Size etc /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP
|
||||
opi5plus-1 8G 958k 99 1.1g 98 1.8g 93 2919k 99 4.4g 99 +++++ +++
|
||||
Latency 39397us 155us 92us 4420us 87us 641us
|
||||
Version 2.00 ------Sequential Create------ --------Random Create--------
|
||||
opi5plus-1 -Create-- --Read--- -Delete-- -Create-- --Read--- -Delete--
|
||||
files /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP /sec %CP
|
||||
16 +++++ +++ +++++ +++ +++++ +++ +++++ +++ +++++ +++ +++++ +++
|
||||
Latency 2136us 600us 449us 1023us 241us 65us
|
||||
```
|
||||
|
||||
Разберем результаты выполнения вашего bonnie++-теста и объясним основные метрики:
|
||||
|
||||
* `------Sequential Output------` — последовательный вывод (запись)
|
||||
* `-Per Chr-` — посимвольная запись
|
||||
* `/sec` — скорость записи в байтах в секунду. Например, 958k = 958 КБ/с.
|
||||
* `%CP` — процент использования процессора 99%.
|
||||
* `Latency` — задержка записи в микросекундах. Например, 39397 микросекунд.
|
||||
* `-Block-` — блочная запись
|
||||
* `/sec` — скорость записи в байтах в секунду. Например, 1.1g = 1.1 ГБ/с.
|
||||
* `%CP` — процент использования процессора 98%.
|
||||
* `Latency` — задержка записи в микросекундах 155 микросекунд.
|
||||
* `-Rewrite-` — перезапись
|
||||
* `/sec` — скорость записи в байтах в секунду. Например, 1.8g = 1.8 ГБ/с.
|
||||
* `%CP` — процент использования процессора 93%.
|
||||
* `Latency` — задержка записи в микросекундах 92 микросекунд.
|
||||
* `--Sequential Input-` — последовательный ввод (чтение)
|
||||
* `-Per Chr-` — посимвольное чтение
|
||||
* `/sec` — скорость чтения в байтах в секунду. Например, 2919k = 2919 КБ/с.
|
||||
* `%CP` — процент использования процессора 99%.
|
||||
* `Latency` — задержка чтения в микросекундах 4420 микросекунд.
|
||||
* `-Block-` — блочное чтение
|
||||
* `/sec` — скорость чтения в байтах в секунду. Например, 4.4g = 4.4 ГБ/с.
|
||||
* `%CP` — процент использования процессора 99%.
|
||||
* `Latency` — задержка чтения в микросекундах 87 микросекунд.
|
||||
* `--Random-` — случайный доступ.
|
||||
* 'Latency' — задержка в микросекундах. Например, 641 микросекунда.
|
||||
* ...
|
||||
* ...
|
||||
* И так далее.
|
||||
|
||||
Более подробно о параметрах и метриках bonnie++ можно прочитать в справке `man bonnie++`.
|
||||
|
395
raspberry-and-orange-pi/opi5plus-move-system-to-nvme-or-emmc.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# Orange Pi 5 Plus: установка (перенос) системы на eMMC или SSD NVMe (M.2)
|
||||
|
||||
По правде сказать, приведенный ниже рецепт не только для переноса системы с MicroSD (она же MicroSDHC или MicroSDXC) на
|
||||
eMMC-носитель или SSD-накопитель NVMe. Он отлично сработает и для установки чистой системы на eMMC или SSD. Причем,
|
||||
установка чистой системы будет намного проще и быстрее.
|
||||
|
||||
Процедура немного напоминает магию, но это результат глубокого заныривания в интернет, и проверено срабатывает и для
|
||||
SSD NVMe, и для eMMC. Но, как и всякая магия, она может не сработать, и тогда придется начинать все сначала и повторить.
|
||||
|
||||
## Установка накопителей
|
||||
|
||||
Выключим Orange Pi 5 Plus и установим в него eMMC-носитель…
|
||||
|
||||
| Фото до и после установки eMMC. Внимание, устанавливайте до щелчка с обоих сторон! |
|
||||
|:-------------------------------------------------------------------------------------|
|
||||
|  |
|
||||
|  |
|
||||
|
||||
…и/или SSD-накопитель NVMe
|
||||
|
||||
| Фото до и после установки NVMe. Более просто в установке. Для надёжности закрепить винтиком. |
|
||||
|:---------------------------------------------------------------------------------------------------|
|
||||
|  |
|
||||
|  |
|
||||
|
||||
Кстати, производительность (скорость чтения и записи) eMMC-носителей и SSD-накопителей NVMe может сильно отличаться.
|
||||
Лично мой выбор — SSD-накопитель NVMe. По опыту он в пять-шесть раз быстрее eMMC.
|
||||
Вот специальная заметка [как замерить производительность накопителя](measuring-performance-storage-devices.md).
|
||||
|
||||
После этого включим Orange Pi 5 Plus. И после того как он загрузится, посмотрим какие устройства и тома есть в системе:
|
||||
```shell
|
||||
sudo lsblk
|
||||
```
|
||||
|
||||
Увидим что-то подобное:
|
||||
```text
|
||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
||||
mtdblock0 31:0 0 16M 0 disk
|
||||
mmcblk1 179:0 0 59.7G 0 disk
|
||||
├─mmcblk1p1 179:1 0 1G 0 part /boot
|
||||
└─mmcblk1p2 179:2 0 58G 0 part /var/log.hdd
|
||||
/
|
||||
mmcblk0 179:32 0 233G 0 disk
|
||||
└─mmcblk0p1 179:33 0 230.6G 0 part
|
||||
mmcblk0boot0 179:64 0 4M 1 disk
|
||||
mmcblk0boot1 179:96 0 4M 1 disk
|
||||
zram0 254:0 0 7.7G 0 disk [SWAP]
|
||||
zram1 254:1 0 200M 0 disk /var/log
|
||||
```
|
||||
|
||||
Важно обратить внимание на объемы накопителей. В данном случае, у нас есть MicroSD-носитель `mmcblk1` (59.7G — это
|
||||
64Gb флешка) и eMMC-носитель `mmcblk0` (233G — это 256Gb eMMC). У вас могут быть другие объемы и другие имена
|
||||
устройств. Так же обратите внимание, ещё у нас есть `mtdblock0` — это внутренняя SPI-флеш, которая
|
||||
используется для загрузки системы. Если у вас есть SSD-накопитель NVMe, он будет иметь имя вроде `nvme0n1`.
|
||||
|
||||
На этом этапе, если мы хотим именно перенести систему, самое время сделать образ нашей MicroSD на внешний
|
||||
носитель (например, на USB-диск или сетевое хранилище). Смонтируем внешний накопитель, например для сетевого
|
||||
хранилища c Samba:
|
||||
```shell
|
||||
mount -t cifs -o username=NAS_USERNAME,password=SECRET //xxx.xxx.xxx.xxx/путь-к-месту-для-сохранения-образа /media/backup/
|
||||
```
|
||||
|
||||
Где:
|
||||
- `NAS_USERNAME` — имя пользователя для доступа к сетевому хранилищу;
|
||||
- `SECRET` — пароль для доступа к сетевому хранилищу;
|
||||
- `xxx.xxx.xxx.xxx` — IP-адрес сетевого хранилища;
|
||||
- `путь-к-каталогу-для-сохраненияь-обраа` — путь к каталогу на сетевом хранилище, куда будет сохранен образ;
|
||||
- `/media/backup/` — точка монтирования сетевого хранилища.
|
||||
|
||||
Сделаем образ MicroSD в файл `flash-disk.img` на этом внешнем накопителе:
|
||||
```shell
|
||||
sudo dd if=/dev/mmcblk1 of=/media/backup/flash-disk.img status=progress
|
||||
```
|
||||
|
||||
Это займет некоторое время (и иногда, в зависимости от скорости внешнего накопителя и размера MicroSD, довольно
|
||||
продолжительное). После того как образ будет готов, установим в систему `gdisk` — утилиту для работы
|
||||
с таблицами разделов:
|
||||
```shell
|
||||
sudo apt install gdisk
|
||||
```
|
||||
## Очистим разделы на SPI-флеш (внутренней флеш-памяти с загрузчиками)
|
||||
|
||||
Запустим `gdisk` для работы с заделами на SPI `mtdblock0` (загрузчиками):
|
||||
|
||||
```shell
|
||||
sudo gdisk /dev/mtdblock0
|
||||
```
|
||||
|
||||
Увидим что-то подобное:
|
||||
```text
|
||||
GPT fdisk (gdisk) version 1.0.8
|
||||
|
||||
Partition table scan:
|
||||
MBR: protective
|
||||
BSD: not present
|
||||
APM: not present
|
||||
GPT: present
|
||||
|
||||
Found valid GPT with protective MBR; using GPT.
|
||||
|
||||
Command (? for help):
|
||||
```
|
||||
|
||||
Если введем `?` и нажмем Enter, увидим список команд:
|
||||
```text
|
||||
b back up GPT data to a file
|
||||
c change a partition's name
|
||||
d delete a partition
|
||||
i show detailed information on a partition
|
||||
l list known partition types
|
||||
n add a new partition
|
||||
o create a new empty GUID partition table (GPT)
|
||||
p print the partition table
|
||||
q quit without saving changes
|
||||
r recovery and transformation options (experts only)
|
||||
s sort partitions
|
||||
t change a partition's type code
|
||||
v verify disk
|
||||
w write table to disk and exit
|
||||
x extra functionality (experts only)
|
||||
? print this menu
|
||||
```
|
||||
|
||||
Выполним команду `p` и Enter, чтобы увидеть список разделов:
|
||||
```text
|
||||
Disk /dev/mtdblock0: 32768 sectors, 16.0 MiB
|
||||
Sector size (logical/physical): 512/512 bytes
|
||||
Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
||||
Partition table holds up to 128 entries
|
||||
Main partition table begins at sector 2 and ends at sector 33
|
||||
First usable sector is 34, last usable sector is 8158
|
||||
Partitions will be aligned on 64-sector boundaries
|
||||
Total free space is 1021 sectors (510.5 KiB)
|
||||
|
||||
Number Start (sector) End (sector) Size Code Name
|
||||
1 64 1023 480.0 KiB 8300 idbloader
|
||||
2 1024 7167 3.0 MiB 8300 uboot
|
||||
|
||||
Command (? for help):
|
||||
```
|
||||
|
||||
Как видим, у нас есть два раздела: `idbloader` и `uboot`. Нам нужно удалить их. Для этого выполним команду `d` и Enter.
|
||||
Увидим:
|
||||
```text
|
||||
Partition number (1-2):
|
||||
```
|
||||
|
||||
Введем номер раздела `1` и Enter. Раздел будет удален. Повторим для раздела `2`. Снова выполним команду `d` и Enter.
|
||||
Теперь нас не спросят номер раздела, оставшийся раздел будет удален без лишних вопросов. Если у вас, вдруг, было
|
||||
больше двух разделов, надо последовательно удалить их все.
|
||||
|
||||
Теперь нам нужно сохранить изменения. Для этого выполним команду `w` и Enter. Увидим:
|
||||
```text
|
||||
Warning! Secondary header is placed too early on the disk! Do you want to
|
||||
correct this problem? (Y/N):
|
||||
```
|
||||
|
||||
Подтверждаем наше намерения перезаписать таблицу разделов. Вводим введя `y` и Enter. Увидим:
|
||||
```text
|
||||
Have moved second header and partition table to correct location.
|
||||
|
||||
Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
|
||||
PARTITIONS!!
|
||||
|
||||
Do you want to proceed? (Y/N):
|
||||
```
|
||||
|
||||
Еще раз подтверждаем наше намерение перезаписать таблицу разделов. Вводим `y` и Enter. Увидим:
|
||||
```text
|
||||
OK; writing new GUID partition table (GPT) to /dev/mtdblock0.
|
||||
Warning: The kernel is still using the old partition table.
|
||||
The new table will be used at the next reboot or after you
|
||||
run partprobe(8) or kpartx(8)
|
||||
The operation has completed successfully.
|
||||
```
|
||||
|
||||
## Очистим разделы на целевом eMMC (или SSD NVMe)
|
||||
|
||||
Теперь нам нужно очистить разделы на целевом накопителе. Для этого запустим `gdisk` для работы с разделами на eMMC
|
||||
(в нашем случае это `mmcblk0`):
|
||||
```shell
|
||||
sudo gdisk /dev/mmcblk0
|
||||
```
|
||||
|
||||
Проделаем те же операции, что и с SPI-флешем. Не буду повторяться, так как процедура аналогична. Важно помнить, что
|
||||
нам нужно удалить все(!) разделы.
|
||||
|
||||
## Выравняем разделы на eMMC (или SSD NVMe)
|
||||
|
||||
Выравнивание секторов eMMC гарантирует правильное распознавание загрузочного диска. Снова запустим `gdisk` для нашего
|
||||
eMMC:
|
||||
```shell
|
||||
sudo gdisk /dev/mmcblk0
|
||||
```
|
||||
|
||||
Дадим команду `p` и Enter, чтобы, чтобы увидеть список разделов, и обратим внимание, на текст над таблицей разделов:
|
||||
```text
|
||||
Disk /dev/mmcblk0: 488570880 sectors, 233.0 GiB
|
||||
Sector size (logical/physical): 512/512 bytes
|
||||
Disk identifier (GUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
||||
Partition table holds up to 128 entries
|
||||
Main partition table begins at sector 2 and ends at sector 33
|
||||
First usable sector is 2048, last usable sector is 488570846
|
||||
Partitions will be aligned on 2048-sector boundaries
|
||||
Total free space is 4974559 sectors (2.4 GiB)
|
||||
```
|
||||
|
||||
В данном случае все нормально: как видим выше, основная таблицы разделов начинается с сектора 2 и заканчивается на 33,
|
||||
а первый используемый сектор — это любое число, кроме 34 (в нашем случае 2048). Можно пропустить следующие шаги. Но
|
||||
если у вас нет так, то необходимо переформатировать сектора перед записью новой таблицы разделов на диск.
|
||||
|
||||
Для этого выполним следующие шаги:
|
||||
|
||||
Вводим команду `x` и Enter, чтобы перейти в экспертный режими. В режиме доступны следующие команды:
|
||||
```text
|
||||
a set attributes
|
||||
b byte-swap a partition's name
|
||||
c change partition GUID
|
||||
d display the sector alignment value
|
||||
e relocate backup data structures to the end of the disk
|
||||
f randomize disk and partition unique GUIDs
|
||||
g change disk GUID
|
||||
h recompute CHS values in protective/hybrid MBR
|
||||
i show detailed information on a partition
|
||||
j move the main partition table
|
||||
l set the sector alignment value
|
||||
m return to main menu
|
||||
n create a new protective MBR
|
||||
o print protective MBR data
|
||||
p print the partition table
|
||||
q quit without saving changes
|
||||
r recovery and transformation options (experts only)
|
||||
s resize partition table
|
||||
t transpose two partition table entries
|
||||
u replicate partition table on new device
|
||||
v verify disk
|
||||
w write table to disk and exit
|
||||
z zap (destroy) GPT data structures and exit
|
||||
? print this menu
|
||||
```
|
||||
|
||||
Переместите основную таблицу разделов. Для этого введите `j` и Enter. Будет предложено задать сектор для расположения
|
||||
начала основной таблицы разделов:
|
||||
```text
|
||||
Currently, main partition table begins at sector 2 and ends at sector 33
|
||||
Enter new starting location (2 to 61408; default is 2; 1 to abort):
|
||||
```
|
||||
|
||||
Вводим `2` и Enter. Затем сохраняем изменения, выполнив команду `w` и Enter. И пройдя два подтверждения (`y` и Enter)
|
||||
выходим из `gdisk`.
|
||||
|
||||
## Перезаписываем загрузчик
|
||||
|
||||
Несколькими попытками проверено, что только такой порядок действий (обновление загрузчика) гарантирует, что
|
||||
Orange Pi 5 будет загружаться с eMMC. Надо запустить встроенное приложение конфигурации Orange Pi 5:
|
||||
```shell
|
||||
sudo orangepi-config
|
||||
```
|
||||
|
||||
Панель orangepi-config на Orange Pi 5 выглядит так:
|
||||
|
||||

|
||||
|
||||
Выбираем пункт '**System: System and security settings**' и заходим в панель '**System Settings**'. Выбираем в ней
|
||||
пункт '**Install: Install to/update boot loader**':
|
||||
|
||||

|
||||
|
||||
Выбираем последний пункт '**Install/Update the bootloader on SPI Flash**':
|
||||
|
||||

|
||||
|
||||
Подтверждаем наше намерение обновить загрузчик:
|
||||
|
||||

|
||||
|
||||
Спустя несколько минут мы снова увидим панель '**System Settings**' приложения `orangepi-config`. На этом этапе
|
||||
можно обновить пакеты системы, выбрав пункт '**Firmware: Run apt update & apt upgrade**'. Но это не обязательно,
|
||||
можно просто выйти из `orangepi-config`.
|
||||
|
||||
Перезагружаем наш Orange Pi 5. Все еще не извлекая MicroSD:
|
||||
```shell
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
| Важно! |
|
||||
|:-------|
|
||||
| Возможно Orange Pi не загрузится. Просто извлеките MicroSD, перезапишите не неё образ системы (лучше чистой, [с официального сайта производителя](http://www.orangepi.org/html/serviceAndSupport/index.html)), загрузитесь снова и проделайте все вышеперечисленное ещё раз. |
|
||||
|
||||
## Записываем образ чистой системы на eMMC
|
||||
|
||||
Самый простой, быстрый и проверенный способ — записать на eMMC образ чистой системы, скаченный с официального сайта.
|
||||
К слову сказать, образы официальных сборок для Orannge Pi лежать на Goolge Drive, так что самое оптимальное скачать
|
||||
образ на каком-нибудь другом компьютере и перенести его на Orange Pi с помощью USB-накопителя или NAS.
|
||||
|
||||
Записываем файл с образом на eMMC:
|
||||
```shell
|
||||
sudo dd bs=1M if=Orangepi5plus_1.0.8_ubuntu_jammy_server_linux6.1.43.img of=/dev/mmcblk0 status=progress
|
||||
```
|
||||
|
||||
Все. Можно выключить Orange Pi 5 Plus:
|
||||
```shell
|
||||
sudo shutdown 0
|
||||
```
|
||||
|
||||
Извлекаем MicroSD и включаем Orange Pi 5. Он должен загрузиться с eMMC.
|
||||
|
||||
## Перенос системы с MicroSD (или с резервной копии) на eMMC
|
||||
|
||||
Если установка чистой системы на eMMC не подходит (наприер, если на MicroSD уже настроена и отлажена система), то
|
||||
можно перенести систему с MicroSD на eMMC. Правда это не сработает, если размер eMMC меньше размера MicroSD (1), а
|
||||
если сработает (размер eMMC больше размера MicroSD), то на eMMC, после копирования, будут созданы тома и разделы
|
||||
ровно такого же размера, как на MicroSD (2).
|
||||
|
||||
Для копирования раздела с MicroSD на eMMC:
|
||||
```shell
|
||||
sudo dd bs=1M if=/dev/mmcblk1 of=/dev/mmcblk0 status=progress
|
||||
```
|
||||
|
||||
а для восстановления из резервной копии (не забываем смонтировать внешний накопитель с резервными копиями) что-то типа:
|
||||
```shell
|
||||
sudo dd if=/dev/mmcblk1 of=/media/backup/flash-disk.img status=progress
|
||||
```
|
||||
|
||||
Это займет продолжительное время. Если мы делали восстановление из резервной копии, то можно будет сразу извлечь
|
||||
MicroSD и перезагрузиться. Но если мы копировали с MicroSD на eMMC, то нам надо будет еще расширить раздел на eMMC
|
||||
на весь объем накопителя, и расширить файловую систему. Так как она еще не смонтирована, то сделать это не сложно.
|
||||
|
||||
Установим в систему пакет утилит `cloud-guest-utils`. Из ее состава нас интересует утилита `growpart`
|
||||
для автоматического увеличения размеров разделов на диске.
|
||||
```shell
|
||||
sudo apt install cloud-guest-utils
|
||||
```
|
||||
|
||||
Увеличиваем второй раздел на eMMC (на первом у нас загрузчик):
|
||||
```shell
|
||||
sudo growpart /dev/mmcblk0 2
|
||||
```
|
||||
|
||||
Увидим что-то подобное:
|
||||
```text
|
||||
CHANGED: partition=2 start=2158592 old: size=121737216 end=123895808 new: size=486412255 end=488570847
|
||||
```
|
||||
|
||||
Теперь нужно "растянуть" файловую систему на этом разделе. Но для начала проведем проверки файловой системы раздела:
|
||||
```shell
|
||||
sudo e2fsck -f /dev/mmcblk0p2
|
||||
```
|
||||
|
||||
Увидим что-то типа:
|
||||
```text
|
||||
e2fsck 1.46.5 (30-Dec-2021)
|
||||
opi_root: recovering journal
|
||||
Pass 1: Checking inodes, blocks, and sizes
|
||||
Pass 2: Checking directory structure
|
||||
Pass 3: Checking directory connectivity
|
||||
Pass 4: Checking reference counts
|
||||
Pass 5: Checking group summary information
|
||||
Free blocks count wrong (13505398, counted=13505373).
|
||||
Fix<y>? yes
|
||||
Free inodes count wrong (3730719, counted=3730708).
|
||||
Fix<y>? yes
|
||||
|
||||
opi_root: ***** FILE SYSTEM WAS MODIFIED *****
|
||||
opi_root: 48812/3779520 files (0.2% non-contiguous), 1711779/15217152 blocks
|
||||
```
|
||||
|
||||
Как видим, нам показали две ошибки и предложили их исправить, и мы согласились (`y`). Теперь, после проверок
|
||||
и исправлений, "растянем" файловую систему:
|
||||
```shell
|
||||
sudo resize2fs /dev/mmcblk0p2
|
||||
```
|
||||
|
||||
Увидим примерно следующее:
|
||||
```text
|
||||
resize2fs 1.46.5 (30-Dec-2021)
|
||||
Resizing the filesystem on /dev/mmcblk0p2 to 60801531 (4k) blocks.
|
||||
The filesystem on /dev/mmcblk0p2 is now 60801531 (4k) blocks long.
|
||||
```
|
||||
|
||||
Все. Раздел на eMMC увеличен на весь объем накопителя, и файловая система на нем тоже "растянута". Теперь можно
|
||||
выключить Orange Pi:
|
||||
```shell
|
||||
sudo shutdown 0
|
||||
```
|
||||
|
||||
Извлекаем MicroSD и включаем Orange Pi снова. Он должен загрузиться уже с eMMC.
|
||||
|
||||
|
||||
|
||||
## PS
|
||||
|
||||
В составлении этой заметки большую помощь оказала инструкция [Kaveh Kaviani](
|
||||
https://github.com/kaveh-kaviani/Tutorials/blob/main/content/sbc/orange-pi/orange-pi-5/boot-linux-from-emmc/README.md).
|
||||
Большое спасибо ему.
|
@@ -0,0 +1,399 @@
|
||||
# Сборка ядра Linux для Orange Pi 5 Plus (и других устройств на Rockchip RK3588) с поддержкой iSCSI
|
||||
|
||||
Для моего проекта (k3s на Orange Pi 5 Plus) потребовалась поддержка система управления блочным хранилищем 'longhorn'
|
||||
(longhorn.io). _Longhorn_ позволяет создавать надёжные тома для контейнеров, автоматически реплицируя их между узлами
|
||||
и обеспечения высокую доступности приложений (если один узел выходит из строя, данные остаются доступны на репликах
|
||||
на других узлах).
|
||||
|
||||
Для _longhorn_ нужен `iSCSI` (**Internet Small Computer System Interface**) — сетевой протокол, который по TCP/IP
|
||||
подсоединяет удалённые диски так, будто они подключены физически. Longhorn через iSCSI обеспечивает передачу
|
||||
данных между узлами по сети.
|
||||
|
||||
В ядре Linux поддержка _iSCSI_ реализована через модули, такие как `iscsi_tcp` и `scsi_transport_iscsi`. Однако,
|
||||
в большинстве дистрибутивов для одноплатных компьютеров, включая и Orange Pi 5 Plus, эти модули отключены. Это делают
|
||||
для экономии ресурсов, да и в целом, это нетипичный сценарий использования Orange Pi. В версии Ubuntu 22.04
|
||||
для Orange Pi 5 Plus (в ядре от Xunlong), поддержка iSCSI тоже отключена, и это делает невозможным запуск Longhorn
|
||||
из коробки.
|
||||
|
||||
**А значит нужно компиляция ядра (сбока)**
|
||||
|
||||
Мы вручную включим поддержку _iSCSI_ (модули `iscsi_tcp` и `scsi_transport_iscsi`), и cможем настроить
|
||||
ядро под свои задачи. Однако компиляция и его замена требует осторожности и внимательности, ошибка в процессе может
|
||||
привести к тому, что система не загрузится (но мы сделаем резервную копию текущего ядра, загрузчика, чтобы можно
|
||||
было восстановить систему).
|
||||
|
||||
Обновления системы (например, через `apt`) с новым ядром будет проходить, как обычно: пакеты, утилиты и приложения
|
||||
обновятся без проблем. Однако пересобранное ядро больше не будет автоматически обновляться через стандартные средства
|
||||
дистрибутива. Если выйдет новая версия ядра, то придётся вручную повторить процесс компиляции. К счастью,
|
||||
поставщик (это Xunlong) обновляет ядро не часто.
|
||||
|
||||
| ✎ Заметка |
|
||||
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Orange Pi 5 Plus использует процессор **Rockchip RK3588** (как и многие другие устройства под брендом Orange). Процесс компиляции ядра для всех RK35xx почти идентичен, и базируется на одном и том же дистрибутиве. Однако, разные периферийные устройства: например, GPIO, USB или Ethernet-контроллеры могут отличаться. Мы будем использовать `.config` от текущего, рабочего ядра, так что настройки будут актуальны. Но следует иметь в виду, что возможно, в будущем могут появиться изменения в конфигурации, дистрибутива и т.п. Следует учитывать, это и **никто ничего не гарантирует**. |
|
||||
|
||||
## План сборки ядра с iSCSI
|
||||
|
||||
* Резервное копирование текущего ядра, загрузчика, дерева устройств, /boot и т.п.
|
||||
* Установить инструментарий для настройки и компиляции ядра
|
||||
* Скачать исходники.
|
||||
* Настроить конфиг для включения модулей iSCSI (и мониторинга NVMe).
|
||||
* Скомпилировать ядро и модули.
|
||||
* Установить новое ядро.
|
||||
* Перезагрузиться в новое ядро.
|
||||
* Проверка.
|
||||
|
||||
## Резервное копирование текущего ядра и /boot
|
||||
|
||||
Архивируем том `/boot` на нашем Orange Pi 5 Plus во временный каталог `/tmp`:
|
||||
```bash
|
||||
sudo tar -czf /tmp/boot-backup.tar.gz /boot
|
||||
```
|
||||
|
||||
Проверим, что архив создался:
|
||||
```bash
|
||||
ls -al /tmp/boot-backup.tar.gz
|
||||
```
|
||||
|
||||
Увидим что архив создался:
|
||||
```text
|
||||
-rw-r--r-- 1 root root 62116591 мар 23 20:33 /tmp/boot-backup.tar.gz
|
||||
```
|
||||
|
||||
Теперь нам нужно скопировать его на какой-нибудь внешний хост (или носитель, но я буду копировать на хост другого
|
||||
компьютера). Если что-то пойдёт не так (например, после обновления ядра система не загрузится) можно будет восстановить
|
||||
файлы из этого архива. Мой Orange Pi 5 Plus, на котором я буду компилировать ядро имеет хост — `opi5plus-3` (замени
|
||||
на свой хост), а имя пользователя от которого я работаю — `opi` (замени на свой). На рабочем компьютере, с которого
|
||||
я захожу по SSH на Orange, у меня есть папка `~/backup/` (`mkdir -p ~/backup`). Скачиваю в неё архив:
|
||||
```bash
|
||||
scp opi@opi5plus-3.local:/tmp/boot-backup.tar.gz ~/backup/opi5plus-3-boot-backup.tar.gz
|
||||
```
|
||||
|
||||
### Что в архиве?
|
||||
|
||||
Если что-то пойдет не так, нам надо знать какие файлы в `/boot` важны и для чего предназначены. Вот список файлов
|
||||
('ls -al /boot'):
|
||||
```text
|
||||
drwxr-xr-x 4 root root 4096 янв 1 1970 ./
|
||||
drwxr-xr-x 19 root root 4096 фев 16 19:59 ../
|
||||
-rwxr-xr-x 1 root root 230456 ноя 21 09:56 boot.bmp*
|
||||
-rwxr-xr-x 1 root root 3341 ноя 21 09:55 boot.cmd*
|
||||
-rwxr-xr-x 1 root root 3413 ноя 21 10:04 boot.scr*
|
||||
-rwxr-xr-x 1 root root 230393 ноя 21 07:08 config-6.1.43-rockchip-rk3588*
|
||||
drwxr-xr-x 3 root root 4096 ноя 21 09:55 dtb/
|
||||
drwxr-xr-x 3 root root 4096 ноя 21 09:55 dtb-6.1.43-rockchip-rk3588/
|
||||
-rwxr-xr-x 1 root root 40579584 ноя 21 07:08 Image*
|
||||
-rwxr-xr-x 1 root root 15462140 мар 21 16:36 initrd.img-6.1.43-rockchip-rk3588*
|
||||
-rwxr-xr-x 1 root root 1152056 ноя 21 09:56 logo.bmp*
|
||||
-rwxr-xr-x 1 root root 0 ноя 21 09:55 .next*
|
||||
-rwxr-xr-x 1 root root 178 фев 16 19:59 orangepiEnv.txt*
|
||||
-rwxr-xr-x 1 root root 1542 ноя 21 09:56 orangepi_first_run.txt.template*
|
||||
-rwxr-xr-x 1 root root 6209932 ноя 21 07:08 System.map-6.1.43-rockchip-rk3588*
|
||||
-rwxr-xr-x 1 root root 15462204 мар 21 16:36 uInitrd*
|
||||
-rwxr-xr-x 1 root root 40579584 ноя 21 07:08 vmlinuz-6.1.43-rockchip-rk3588*
|
||||
```
|
||||
|
||||
Ключевые файлы:
|
||||
* `Image` — собственно ядро Linux (в данном случае версия 6.1.43 для Rockchip RK3588). При старте компьютера загрузчик
|
||||
U-Boot загрузит его в память и передаст ему управление. Без этого файла система не запустится.
|
||||
* `vmlinuz-6.1.43-rockchip-rk3588` — копия ядра (в системе уже есть резервная копия).
|
||||
* `dtb/` — Каталог файлами **Device Tree Blob** (`DTB`). Это бинарные файлы, описывающие аппаратное обеспечение устройства.
|
||||
Для Orange Pi 5 Plus используется файл вроде `rk3588-orangepi-5-plus.dtb` (находится в подкаталоге `dtb/rockchip/`).
|
||||
DTB передаётся ядру, чтобы оно знало, как работать с процессором (количество ядер, частоты), памятью (RAM, её объём
|
||||
и адреса), периферией (UART, I2C, SPI, Ethernet, USB, GPIO, прерывания и тому подобное). На ARM-устройствах
|
||||
(в отличие от x86, где есть ACPI) нет стандартного способа обнаружения оборудования. DTB решает эту проблему,
|
||||
предоставляя ядру "карту" железа. U-Boot загружает DTB из `/boot/dtb/ `и передаёт его ядру при старте. Кстати,
|
||||
в подкаталоге dtb/rockchip/ есть `overlay/` — это дополнительные файлы, которые могут использоваться для добавления
|
||||
и/или изменения функциональности устройства. Например, можно добавить поддержку новых периферийных устройств (камеру,
|
||||
дисплей и т.п.).
|
||||
* `uInitrd` — Начальный RAM-диск (initrd), адаптированный для U-Boot. Содержит модули и скрипты, необходимые для
|
||||
пред-загрузки (выбор накопителя, монтирование корневой файловой системы и т.п.). Если он повреждён или несовместим
|
||||
с ядром, загрузка может упасть на этапе инициализации.
|
||||
* `orangepiEnv.txt` — Конфигурационный файл для U-Boot. Здесь задаются параметры загрузки, такие как путь расположение
|
||||
дерева DTB, UUID корневой файловой системы, тип файловой системы… Без правильных настроек в этом файле U-Boot не
|
||||
найдёт нужные для загрузки файлы.
|
||||
* `boot.scr` — Скрипт загрузки для U-Boot. Содержит команды для загрузки ядра, initrd и DTB. U-Boot выполняет его
|
||||
при старте системы. Этот файл создаётся из `boot.cmd` с помощью утилиты `mkimage`. Если он повреждён или отсутствует,
|
||||
U-Boot не сможет загрузить систему.
|
||||
* `dtb-6.1.43-rockchip-rk3588/` — Копия каталога `dtb/`, обычно появляется, когда ядро устанавливается или обновляется
|
||||
через пакетный менеджер (например, `apt`). Она привязана к конкретной версии ядра — в данном случае
|
||||
`6.1.43-rockchip-rk3588`, для того, чтобы: Хранить DTB-файлы, соответствующие этой версии ядра и избегать конфликты
|
||||
между DTB от разных версий ядра (если используется несколько ядер на одной системе).
|
||||
|
||||
Менее критичные, но полезные файлы:
|
||||
* `config-6.1.43-rockchip-rk3588` — Конфигурация ядра, использованная при его сборке (он нам понадобится, чтобы
|
||||
пересобрать ядро с iSCSI).
|
||||
* `System.map-6.1.43-rockchip-rk3588` — Карта меток (символов) ядра, полезна для отладки.
|
||||
* `initrd.img-6.1.43-rockchip-rk3588` — Обычный initrd, из которого генерируется uInitrd.
|
||||
* `boot.bmp` и `logo.bmp` — Изображения для экрана загрузки (не влияют на работу системы).
|
||||
|
||||
## Устанавливать инструменты для сборки ядра
|
||||
|
||||
Обновим репозитории и установим минимальный инструментарий для сборки ядра:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential bc flex bison libssl-dev libncurses-dev git
|
||||
```
|
||||
|
||||
Нам понадобятся следующие пакеты:
|
||||
|
||||
* `build-essential` — Включает `gcc` (для ARM64, make и другие базовые инструменты компиляции.
|
||||
* `bc` — Утилита для точных математических вычислений в командной строке. Используется для вычислений в скриптах сборки ядра.
|
||||
* `flex` и `bison` — генератор лексических анализаторов и парсер. Нужны для обработки конфигурационных файлов ядра.
|
||||
* `libssl-dev` — Для поддержки криптографии в ядре и `OpenSSL`.
|
||||
* `libncurses-dev` — Библиотека для создания текстовых интерфейсов в терминале. Необходимо для работы интерфейса
|
||||
`menuconfig` при настройке параметров ядра.
|
||||
* `git` — Для клонирования исходного кода ядра из репозитория Xunlong.
|
||||
|
||||
> **Примечание:** Если вы хотите собрать ядро на x86-системе, установите кросс-компилятор `gcc-aarch64-linux-gnu` (`sudo apt install gcc-aarch64-linux-gnu`) и используйте его вместо обычного gcc в командах сборки. Он позволит собрать ядро для ARM64-архитектуры на x86-системе.
|
||||
|
||||
|
||||
## Скачивание и подготавливаем исходники ядра
|
||||
|
||||
Клонируем репозиторий Orange Pi для ядра 6.1 из репозитория Xunlong. Мы будем брать ветку (branch)
|
||||
`orange-pi-6.1-rk35xx`. Это ветка с исходниками для всех устройств на базе Rockchip RK35xx (включая Orange Pi 5 Plus):
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/orangepi-xunlong/linux-orangepi -b orange-pi-6.1-rk35xx ~/linux-orangepi
|
||||
```
|
||||
|
||||
После клонирования репозитория, переходим в каталог с ним:
|
||||
```bash
|
||||
cd ~/linux-orangepi
|
||||
```
|
||||
|
||||
И первым делом в `Makefile` установим `EXTRAVERSION` (модификатор версии ядра). Узнаем текущую версию ядра:
|
||||
```bash
|
||||
uname -r
|
||||
```
|
||||
|
||||
Я на своем Orange Pi 5 Plus вижу (у вас может быть другая версия):
|
||||
```text
|
||||
6.1.43-rockchip-rk3588
|
||||
```
|
||||
|
||||
Открываем `Makefile` в текстовом редакторе:
|
||||
```bash
|
||||
nano Makefile
|
||||
```
|
||||
|
||||
И устанавливаем модификатор версии ядра, строку `XTRAVERSION =` (у меня это пятая строчка):
|
||||
```text
|
||||
EXTRAVERSION = -rockchip-rk3588
|
||||
```
|
||||
|
||||
|
||||
Затем копируем конфиг текущего ядра в конфиг для сборки.
|
||||
```bash
|
||||
zcat /proc/config.gz > .config
|
||||
```
|
||||
|
||||
`.config` содержит все текущие настройки ядра. Это очень удобно, так как нужно добавить только поддержку нужных модулей
|
||||
(в нашем случае `iSCSI`), а не настраивать всё с нуля.
|
||||
|
||||
## Настройка конфига с iSCSI
|
||||
|
||||
Теперь нужно отредактировать, `.config` чтобы включить `iSCSI`. Заходим в интерфейс конфигуратора ядра `menuconfig`:
|
||||
```bash
|
||||
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
|
||||
```
|
||||
|
||||
Используя стрелки на клавиатуре, чтобы двигаться по пунктам меню.
|
||||
1. Перейди в `Device Drivers` → `SCSI device support` → `SCSI Transports`.
|
||||
2. Найти внутри `iSCSI Transport Attributes`, с помощью стрелок выбрать его.
|
||||
3. Нажимает `M` (для модуля) — появится `<M>` рядом с пунктом. Это означает, что модуль будет собираться как модуль,
|
||||
а не встраиваться в ядро. Модуль (`iscsi_tcp.ko`) проще протестировать без перезагрузки.
|
||||
4. В нижнем меню выбрать `<Save>` и подтвердим сохранение в `.config`.
|
||||
6. Вернуться в меню выше `SCSI device support` (через нижнее меню `<Exit>`).
|
||||
7. Зайти в раздел `SCSI low-level drivers`.
|
||||
8. Выбрать `iSCSI Initiator over TCP/IP` и нажать `M` (для модуля).
|
||||
9. Снова сохраняем конфиг в `.config` (через нижнее меню `<Save>`).
|
||||
10. Выйти из `menuconfig` (через нижнее меню `<Exit>`, три раза).
|
||||
|
||||
| ✎ Может быть полезно: |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| На моём Orange Pi 5 Plus загрузка идет с NVMe диска ([см. как настроить](opi5plus-move-system-to-nvme-or-emmc.md)), и мне было полезно так же включить ещё и систему мониторинга NVMe: `Device Drivers` → `NVME Support`, найти пункт `SNVMe hardware monitoring` и включить его. В нижнем меню выбрать `<Save>` и подтвердим сохранение в `.config`. Выйти из `menuconfig` (через нижнее меню `<Exit>`, три раза). Теперь у меня будет возможность отслеживать состояние NVMe-диска (температуру, износ и т.д.) через инструменты вроде `nvme-cli`. |
|
||||
|
||||
Проверим, что iSCSI включён:
|
||||
```bash
|
||||
grep -E "ISCSI_TCP|SCSI_ISCSI_ATTRS|ISCSI_BOOT" .config
|
||||
```
|
||||
|
||||
Должны увидеть что-то типа:
|
||||
```text
|
||||
CONFIG_SCSI_ISCSI_ATTRS=m
|
||||
CONFIG_ISCSI_TCP=m
|
||||
# CONFIG_ISCSI_BOOT_SYSFS is not set
|
||||
```
|
||||
|
||||
| ✎ Проверка, что включен мониторинг NVMe (если включали): |
|
||||
|---------------------------------------------------------------|
|
||||
| `grep -E "NVME_HWMON" .config` и увидим `CONFIG_NVME_HWMON=y` |
|
||||
|
||||
## Компиляция ядра и модулей
|
||||
|
||||
Запускаем компиляцию ядра и модулей:
|
||||
```bash
|
||||
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8
|
||||
```
|
||||
|
||||
`-j8` — это количество потоков, используем столько, сколько у нас ядер. На Orange Pi 5 Plus восемь ядер. Но, например,
|
||||
если у вас четыре ядра, то `-j4`.
|
||||
|
||||
Компиляция займёт 20-40 минут (Orange Pi 5 Plus).
|
||||
|
||||
Установим модули ядра:
|
||||
```bash
|
||||
sudo make modules_install
|
||||
```
|
||||
|
||||
После завершения ядро будет находиться в каталоге `~/linux-orangepi/arch/arm64/boot/` (файл `Image`), а дерево описания
|
||||
устройства в `~/linux-orangepi/arch/arm64/boot/dts/rockchip/` (наш файл для Orange Pi 5 Plus --
|
||||
`rk3588-orangepi-5-plus.dtb`, дле версий 5, 5 Max, AI, Ultra соответствующие файлы там же).
|
||||
|
||||
Проверим, что ядро скомпилировалось:
|
||||
```bash
|
||||
ls -l arch/arm64/boot/Image
|
||||
```
|
||||
|
||||
Ожидаемый размер: около 40 МБ:
|
||||
```text
|
||||
-rw-rw-r-- 1 opi opi 40579584 Mar 22 19:09 arch/arm64/boot/Image
|
||||
```
|
||||
|
||||
Размер должен почти совпасть (или полностью совпадать) с размером текущего ядра:
|
||||
```bash
|
||||
ls -l /boot/Image
|
||||
```
|
||||
```text
|
||||
-rwxr-xr-x 1 root root 40579584 Nov 21 07:08 /boot/Image
|
||||
```
|
||||
|
||||
И это не ошибка. Мы добавили только `CONFIG_ISCSI_TCP=m` и `CONFIG_SCSI_ISCSI_ATTRS=m`. Это подключит модули (`iscsi_tcp.ko`),
|
||||
но само ядро (`Image`) почти не увеличивается, ведь `.ko`-модуль iSCSI не встраивается в ядро (мы выбрали `<M>`,
|
||||
а не `<*>`). Кроме того, Linux выравнивает размеры файлов по блокам (например, 4 Кб), и добавление
|
||||
пары небольших модулей могут не повлиять на итоговый размер.
|
||||
|
||||
## Устанавливаем новое ядро рядом со старым
|
||||
|
||||
Мы должны скопировать два файла в `/boot`. Первый — это `Image`. Это само ядро, и оно скомпилировалось в каталог
|
||||
`~/linux-orangepi/arch/arm64/boot/`. Второй файл — это `rk3588-orangepi-5-plus.dtb`. Он находится
|
||||
в `~/linux-orangepi/arch/arm64/boot/dts/rockchip/`. Как было сказано выше, это дерево описания оборудования
|
||||
(Device Tree Blob), и нужно при загрузке ядра. Кстати, при компиляции мы получили `.dtb` для всех моделей Orange Pi 5
|
||||
(5, 5 Plus, 5 Max, AI, Ultra). Для моей модели (Orange Pi 5 Plus) нужен `rk3588-orangepi-5-plus.dtb`, а вы можете
|
||||
взять другой, для своей модели.
|
||||
|
||||
Копируем новые файлы в `/boot` с уникальными именами (с суффиксом `-iscsi`):
|
||||
```bash
|
||||
sudo cp ~/linux-orangepi/arch/arm64/boot/Image /boot/Image-iscsi
|
||||
sudo cp ~/linux-orangepi/arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dtb /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-iscsi
|
||||
```
|
||||
|
||||
Старое ядро и дерево описания переименуем (это будет резервная копия, с суффиксом `-original`):
|
||||
```bash
|
||||
sudo mv /boot/Image /boot/Image-original
|
||||
sudo mv /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-original
|
||||
```
|
||||
|
||||
Устанавливаем новые ядро и дерево:
|
||||
```bash
|
||||
sudo cp /boot/Image-iscsi /boot/Image
|
||||
sudo cp /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb-iscsi /boot/dtb/rockchip/rk3588-orangepi-5-plus.dtb
|
||||
```
|
||||
|
||||
> **Важно**: Не обновляйте `uInitrd`, если текущий работает, — его замена может сломать загрузку системы.
|
||||
|
||||
Все готово.
|
||||
|
||||
## Перезагрузка в новое ядро
|
||||
|
||||
Тут все просто:
|
||||
```bash
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
#### Если что-то пошло не так и система не загрузилась
|
||||
|
||||
Если вы все делали правильно, то такого быть не должно. Но, тем не менее, если загрузка не произошла, то это может
|
||||
выглядеть двумя способами:
|
||||
* Система зависает на этапе загрузки: синенький огонек на Orange Pi не загорается и Ethernet тоже не мигает —
|
||||
_вы неправильно собрали ядро или испортили загрузчик.
|
||||
* Система, вроде как, грузится, но все никак… огоньки весело мигают, но не получается подключиться ни по SSH, ни
|
||||
другим способом, пинги на IP-адрес не проходят — вы забыли подключить модули, накосячили с конфигом или с `.dtb`_.
|
||||
|
||||
Чтобы починить, загружайтесь с MicroSD-карты (не забудьте отключить питание перед тем как вставить MicroSD-карту).
|
||||
Затем смонтируйте, в зависимости где у вас система, eMMC:
|
||||
```bash
|
||||
mkdir -p /mnt/emmc
|
||||
mount /dev/mmcblk2p1 /mnt/emmc
|
||||
cd /mnt/emmc
|
||||
```
|
||||
|
||||
…или NVMe:
|
||||
```bash
|
||||
mkdir -p /mnt/nvme
|
||||
mount /dev/nvme0n1p1 /mnt/nvme
|
||||
cd /mnt/nvme
|
||||
```
|
||||
|
||||
И восстанавливаем старое ядро и дерево описания:
|
||||
```bash
|
||||
sudo cp Image-original Image
|
||||
sudo cp dtb/rockchip/rk3588-orangepi-5-plus.dtb-original dtb/rockchip/rk3588-orangepi-5-plus.dtb
|
||||
```
|
||||
|
||||
Выключаем систему:
|
||||
```bash
|
||||
sudo poweroff
|
||||
```
|
||||
|
||||
Вынимаем MicroSD-карту, включаем, и загрузиться старая система.
|
||||
|
||||
## Проверка нового ядра
|
||||
|
||||
Если же иситма поле обновления ядра загрузилась, проверим текущую версию ядра:
|
||||
```bash
|
||||
uname -r
|
||||
```
|
||||
|
||||
Она должна остаться той же:
|
||||
```text
|
||||
6.1.43-rockchip-rk3588
|
||||
```
|
||||
|
||||
Проверим, работает ли iSCSI:
|
||||
```bash
|
||||
sudo systemctl status iscsid
|
||||
```
|
||||
|
||||
Увидим что-то вроде:
|
||||
```text
|
||||
● iscsid.service - iSCSI initiator daemon (iscsid)
|
||||
Loaded: loaded (/lib/systemd/system/iscsid.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2025-03-24 17:51:59 MSK; 28min ago
|
||||
TriggeredBy: ● iscsid.socket
|
||||
Docs: man:iscsid(8)
|
||||
Main PID: 1070 (iscsid)
|
||||
Tasks: 2 (limit: 18977)
|
||||
Memory: 4.6M
|
||||
CPU: 94ms
|
||||
CGroup: /system.slice/iscsid.service
|
||||
├─1069 /sbin/iscsid
|
||||
└─1070 /sbin/iscsid
|
||||
|
||||
мар 24 17:51:58 opi5plus-3 systemd[1]: Starting iSCSI initiator daemon (iscsid)...
|
||||
мар 24 17:51:59 opi5plus-3 iscsid[1061]: iSCSI logger with pid=1069 started!
|
||||
мар 24 17:51:59 opi5plus-3 iscsid[1069]: iSCSI daemon with pid=1070 started!
|
||||
мар 24 17:51:59 opi5plus-3 systemd[1]: Started iSCSI initiator daemon (iscsid).
|
||||
```
|
||||
|
||||
Как видим, `active (running)`, значит iSCSI работает.
|
||||
|
||||
| ✎ Заметка |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Если вы включили мониторинг NVMe, то проверим и его. Установим nvme-cli: `sudo apt install nvme-cli` и проверим состояние нашего накопителя `sudo nvme smart-log /dev/nvme0n1`. |
|
||||
|
||||
**ВСË!**
|
||||
|
||||
А, да, не забудьте удалить репозиторий с исходниками ядра, если он вам больше не нужен:
|
||||
```bash
|
||||
rm -rf ~/linux-orangepi
|
||||
```
|