Образи зібрались із першого разу, але далі знайшлось три речі. memlock без обмеження для Dragonfly дозволений не всюди: у контейнерній віртуалізації ядро відмовляє, і контейнер не стартує взагалі. Швидкість того не варта — обмеження знято. Caddy нескінченно просив Let's Encrypt видати сертифікат на IP, чого той не робить, і сайт лишався без TLS. Тепер порожня ACME_EMAIL означає самопідписаний сертифікат. Плюс default_sni: SNI не містить IP-адрес, тож без нього рукостискання обривалось «internal error». У README команда заведення власника не працювала: у образу ENTRYPOINT, і без --entrypoint ім'я команди йшло аргументом до netpulse-api. Заразом netpulse-user навчився створювати перший кабінет — свіжа база не має жодного, а користувач без кабінету нікуди не належить. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
230 lines
11 KiB
Markdown
230 lines
11 KiB
Markdown
# Розгортання NetPulse
|
||
|
||
Один хост, `docker compose`, автоматичний TLS. Такого розгортання
|
||
вистачає до кількох тисяч хостів на моніторингу; розносити служби по
|
||
машинах має сенс тоді, коли впирається БД, а не застосунок.
|
||
|
||
## Що з чого складається
|
||
|
||
| Служба | Роль | Порт |
|
||
| ----------- | ------------------------------------------------ | ----- |
|
||
| `db` | PostgreSQL 16 + TimescaleDB — усі дані | — |
|
||
| `cache` | DragonflyDB — черги й тимчасові стани | — |
|
||
| `migrate` | накочування схеми; відпрацьовує і зупиняється | — |
|
||
| `api` | HTTP API + вшитий інтерфейс | 8080 |
|
||
| `collector` | gRPC-колектор, до якого підключаються зонди | 9443 |
|
||
| `proxy` | Caddy: сертифікати, HTTPS, проксі до двох служб | 80/443/9443 |
|
||
|
||
Історія конфігів живе в Git на томі `git-data`, спільному для `api` й
|
||
`collector`: перший читає її для порівняння версій, другий пише під час
|
||
бекапу.
|
||
|
||
Назовні дивиться лише `proxy`. `api` і `collector` портів не публікують:
|
||
до них ходять через нього.
|
||
|
||
## Перший запуск
|
||
|
||
```sh
|
||
git clone <репозиторій> netpulse && cd netpulse
|
||
cp deploy/.env.example .env
|
||
```
|
||
|
||
Заповнити `.env`. Три значення обовʼязкові й генеруються так:
|
||
|
||
```sh
|
||
openssl rand -base64 24 # POSTGRES_PASSWORD
|
||
echo "np1=$(openssl rand -hex 32)" # NETPULSE_DEK
|
||
openssl rand -base64 48 # NETPULSE_JWT_SECRET
|
||
```
|
||
|
||
Далі:
|
||
|
||
```sh
|
||
docker compose up -d --build
|
||
docker compose run --rm api netpulse-user \
|
||
-tenant default -login admin -role owner -name "Адміністратор"
|
||
```
|
||
|
||
Пароль команда спитає інтерактивно — щоб він не осів в історії оболонки
|
||
й у списку процесів.
|
||
|
||
Інтерфейс — на `https://<NETPULSE_DOMAIN>`.
|
||
|
||
## Підключення зонда
|
||
|
||
В інтерфейсі: **Зонди → Додати зонд**. Видане запрошення (`np_enr_…`)
|
||
одноразове — після реєстрації воно згоряє.
|
||
|
||
На машині, де стоятиме зонд:
|
||
|
||
```sh
|
||
docker run -d --name netpulse-agent --restart unless-stopped \
|
||
--cap-add NET_RAW \
|
||
-v netpulse-agent:/var/lib/netpulse \
|
||
netpulse/agent:dev \
|
||
-server netpulse.example.com:9443 \
|
||
-enroll np_enr_… \
|
||
-name "Зонд у Львові" \
|
||
-modules icmp,snmp,topology,ncm
|
||
```
|
||
|
||
Зонд обміняє запрошення на постійний токен і збереже його в
|
||
`/var/lib/netpulse/agent.json` (права 0600). Наступні запуски токена вже
|
||
не потребують — том із посвідченням має пережити перестворення
|
||
контейнера, інакше кожен старт вимагатиме нового запрошення.
|
||
|
||
Усі зʼєднання зонда вихідні: у мережі клієнта не треба відкривати
|
||
жодного порту.
|
||
|
||
## Бекап
|
||
|
||
Три речі, і всі три обовʼязкові:
|
||
|
||
1. **База** — усе, крім секретів у відкритому вигляді.
|
||
2. **`NETPULSE_DEK`** — без нього паролі SSH і SNMP-community з бекапу не
|
||
розшифрувати. У БД лежить лише шифротекст.
|
||
3. **`NETPULSE_JWT_SECRET`** — без нього після відновлення всі активні
|
||
сесії відваляться. Не смертельно, але користувачі помітять.
|
||
|
||
```sh
|
||
docker compose exec -T db \
|
||
pg_dump -U netpulse -d netpulse -Fc --no-owner \
|
||
> netpulse-$(date +%F).dump
|
||
```
|
||
|
||
Формат `-Fc` (custom), а не простий SQL: він стискається і дозволяє
|
||
відновлювати вибірково.
|
||
|
||
Ключі зберігати **окремо від дампа** — інакше сенс шифрування секретів
|
||
зникає: той, хто дістав бекап, дістав і ключ до нього.
|
||
|
||
Том `git-data` бекапити не обовʼязково: тіла конфігів лежать
|
||
зашифрованими в базі, і репозиторій повністю відтворюється з неї —
|
||
|
||
```sh
|
||
docker compose run --rm --entrypoint netpulse-gitsync api
|
||
```
|
||
|
||
Зворотне невірно: з репозиторію базу не відновити. Тому джерелом істини
|
||
лишається дамп, а Git — похідне сховище, яке коштує один запуск команди.
|
||
|
||
### Автоматично, щодня
|
||
|
||
```cron
|
||
15 3 * * * cd /opt/netpulse && docker compose exec -T db pg_dump -U netpulse -d netpulse -Fc --no-owner > /var/backups/netpulse-$(date +\%F).dump && find /var/backups -name 'netpulse-*.dump' -mtime +30 -delete
|
||
```
|
||
|
||
## Відновлення
|
||
|
||
TimescaleDB вимагає рамки навколо відновлення: без неї фонові процеси
|
||
агрегації втручаються в наливання даних і дамп лягає пошкодженим.
|
||
|
||
```sh
|
||
docker compose stop api collector
|
||
|
||
docker compose exec -T db psql -U netpulse -d postgres -c \
|
||
'DROP DATABASE IF EXISTS netpulse; CREATE DATABASE netpulse;'
|
||
|
||
docker compose exec -T db psql -U netpulse -d netpulse -c \
|
||
'CREATE EXTENSION IF NOT EXISTS timescaledb;'
|
||
|
||
docker compose exec -T db psql -U netpulse -d netpulse -c \
|
||
'SELECT timescaledb_pre_restore();'
|
||
|
||
docker compose exec -T db pg_restore -U netpulse -d netpulse --no-owner \
|
||
< netpulse-2026-08-25.dump
|
||
|
||
docker compose exec -T db psql -U netpulse -d netpulse -c \
|
||
'SELECT timescaledb_post_restore();'
|
||
|
||
docker compose up -d api collector
|
||
```
|
||
|
||
У `.env` має лежати **той самий** `NETPULSE_DEK`, що й на момент дампа.
|
||
Інакше застосунок підніметься, але кожна спроба скористатись збереженим
|
||
паролем поверне помилку розшифрування — і виглядатиме це як зламані
|
||
креденшели, а не як втрачений ключ.
|
||
|
||
Перевірка після відновлення:
|
||
|
||
```sh
|
||
docker compose exec -T db psql -U netpulse -d netpulse -c \
|
||
'SELECT count(*) FROM core.devices;'
|
||
curl -sf https://<NETPULSE_DOMAIN>/healthz && echo OK
|
||
```
|
||
|
||
## Оновлення
|
||
|
||
```sh
|
||
git pull
|
||
docker compose up -d --build
|
||
```
|
||
|
||
`migrate` відпрацює першим і не дасть піднятись API, якщо схема не
|
||
накотилась. Міграції йдуть по одній у транзакції; уже застосований файл
|
||
зі зміненою контрольною сумою зупиняє весь запуск — це захист від
|
||
мовчазного розходження схеми з кодом.
|
||
|
||
Відкат схеми не передбачений: зворотні міграції на даних телеметрії
|
||
коштують дорожче, ніж відновлення з бекапу.
|
||
|
||
## Зміна ключа шифрування
|
||
|
||
Ключі перелічуються через кому, новий — першим:
|
||
|
||
```
|
||
NETPULSE_DEK=np2=<новий hex>,np1=<старий hex>
|
||
```
|
||
|
||
Нові секрети шифруються першим ключем, старі читаються своїм. Прибирати
|
||
старий ключ можна лише після того, як усі секрети перезаписані.
|
||
|
||
## Чому саме так
|
||
|
||
**Два образи, а не пʼять.** `api`, `collector`, `migrate`, `netpulse-user`
|
||
і `netpulse-secret` — з одного модуля, з половиною спільного коду. Один
|
||
образ гарантує, що API і колектор ходять у схему БД однією версією; окремі
|
||
образи дають їм можливість розʼїхатись саме там, де це найдорожче.
|
||
|
||
Зонд — окремо: він їде в чужу мережу, і DSN, ключі шифрування та команди
|
||
заведення користувачів не повинні бути в тому образі навіть як
|
||
невикористані файли.
|
||
|
||
**Міграції окремою службою.** API піднімається в кількох примірниках;
|
||
накочування схеми зі старту означало б гонку між ними.
|
||
|
||
**TLS на проксі, а не в застосунку.** Прострочений сертифікат на системі,
|
||
яка сама має повідомляти про проблеми, — найгірший спосіб дізнатись про
|
||
проблему. Caddy оновлює його сам.
|
||
|
||
## Без домену
|
||
|
||
Домен потрібен для справжнього сертифіката: Let's Encrypt не видає їх на
|
||
IP-адреси. Поки домену немає, лишіть `ACME_EMAIL` порожнім — Caddy
|
||
випише самопідписаний, і система працюватиме одразу, з попередженням у
|
||
браузері.
|
||
|
||
Коли домен зʼявиться: замінити `NETPULSE_DOMAIN`, вписати `ACME_EMAIL` і
|
||
`docker compose restart proxy`. Caddyfile змонтований, тож перезбирати
|
||
образи не треба — але й `up -d` без `restart` його не перечитає.
|
||
|
||
## Дашборд на телевізор
|
||
|
||
В інтерфейсі: **Дашборд → На телевізор → Видати посилання**. Отриману
|
||
адресу відкривають на екрані в диспетчерській — вона не потребує входу.
|
||
|
||
Телевізор нікуди не залогиниш: сесія протермінується, браузер оновиться,
|
||
і зранку на стіні висітиме форма входу замість карти мережі — рівно
|
||
тоді, коли на неї дивляться.
|
||
|
||
Що дає посилання і чого не дає:
|
||
|
||
- **тільки читання** цього дашборда: його плитки, активні алерти й
|
||
метрики тих хостів, які на ньому показані;
|
||
- метрики чужого хоста за ним не дістати навіть підбором ідентифікатора;
|
||
- решта кабінету — інвентар, конфіги, налаштування, секрети —
|
||
недоступна;
|
||
- відкликається одним рухом, старе посилання одразу мертве.
|
||
|
||
Токен показується один раз. Видати нове можна будь-коли — попереднє при
|
||
цьому перестає працювати.
|