Netpulse_SasS/deploy/README.md
byrsapty 32442f8177
All checks were successful
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 1m4s
CI / server (push) Successful in 1m8s
CI / dbtest (push) Successful in 1m0s
CI / agent (push) Successful in 1m29s
Рішення власника: дзеркало конфігів лишається дослівним
Обрано побайтову точність архіву замість маскування — без неї
відновлення з архіву не працює. Наслідок прийнято свідомо: доступ до
дзеркала рівносильний доступу до всіх паролів у мережі.

Маскування лишається там, де конфіг читає людина: план відкату й
стенограми завдань.

Записано, щоб не переобговорювати.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:52:30 +03:00

34 KiB
Raw Blame History

Розгортання 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 портів не публікують: до них ходять через нього.

Перший запуск

git clone <репозиторій> netpulse && cd netpulse
cp deploy/.env.example .env

Заповнити .env. Три значення обовʼязкові й генеруються так:

openssl rand -base64 24                 # POSTGRES_PASSWORD
echo "np1=$(openssl rand -hex 32)"      # NETPULSE_DEK
openssl rand -base64 48                 # NETPULSE_JWT_SECRET

Далі:

docker compose up -d --build
docker compose run --rm --entrypoint netpulse-user cli \
  -tenant default -login admin -role owner -name "Адміністратор"

Пароль команда спитає інтерактивно — щоб він не осів в історії оболонки й у списку процесів.

Інтерфейс — на https://<NETPULSE_DOMAIN>.

Пісочниця: повна установка, яку можна перевірити

Твердження «нова інсталяція піднімається сама» довго було доведене міркуванням, а не запуском. Сухий прогін проходив, окремі кроки перевірялись на живій базі — а повної установки з нуля не робив ніхто, бо ніде: єдина доступна машина була бойовим стендом на 4 ГБ, і другий повний стек поклав би робочу систему.

./netpulse sandbox ставить NetPulse по-справжньому, з тим самим docker-compose.yml, тими самими міграціями й тією самою самоперевіркою, але в окремому проєкті compose з власними томами й портами на 127.0.0.1. Це не імітація: вона або справді піднімає стек і заходить у нього справжнім паролем, або зупиняється й каже, на чому.

./netpulse sandbox               # підняти й лишити, щоб подивитись
./netpulse sandbox once          # підняти, перевірити, знести все
./netpulse sandbox upgrade       # другий режим: оновлення з версії на версію
./netpulse sandbox check         # та сама перевірка ще раз
./netpulse sandbox status        # що вона зараз займає
./netpulse sandbox down          # знести все (обидва режими), включно з томами
./netpulse sandbox logs [служба] # журнали

Режимів два, і вони перевіряють різне: sandbox — установку з нуля, sandbox upgrade — шлях «стояла стара версія, стала нова». Другий описано нижче окремим розділом.

Розробникові перед випуском./netpulse sandbox once. Він робить повну установку з нуля, проганяє всі твердження самоперевірки (вхід справжнім паролем, кабінет назвався, сім переліків із міграцій непорожні, дванадцять ендпоїнтів відповідають, зонд зареєструвався в колекторі) і прибирає за собою до останнього тому. Ненульовий код виходу означає, що цю збірку клієнтові віддавати не можна.

Клієнтові — щоб подивитись до того, як ставити./netpulse sandbox без підкоманди. Після установки вона друкує адресу https://localhost:<порт>, логін і пароль; система жива, у ній є кабінет, локальний зонд і всі довідники. Подивились — ./netpulse sandbox down, і на машині не лишається нічого.

Скільки це коштує

Що Скільки
Пам'ять у роботі ~550900 МБ на всі шість служб
Стеля пам'яті ~2.05 ГБ — жорстке обмеження з накладки, вище не підніметься
Пік при збірці ще ~12 ГБ, поки збираються образи Go
Диск: томи ~250 МБ (порожня база зі схемою, внутрішній CA Caddy, посвідчення зонда)
Диск: образи ~2 ГБ, спільні з бойовою інсталяцією — якщо вони вже зібрані, пісочниця не додає нічого
Порти 18080, 18081, 18082 (або наступна вільна трійка), усі на 127.0.0.1

Точні цифри після запуску показує ./netpulse sandbox status — вони виміряні, а не оцінені.

Коли її запускати НЕ можна

Установник перевіряє це сам і відмовляється, а не пробує:

  • вільно менше 3 ГБ пам'яті (4 ГБ, якщо образи ще треба зібрати). Міряється MemAvailable, а не вся пам'ять: на машині, де вже працює бойовий стек, «4 ГБ встановлено» не має жодного стосунку до того, скільки з них можна взяти;
  • вільно менше 3 ГБ диска (8 ГБ без готових образів);
  • на машині працює бойова інсталяція NetPulse. Пісочниця не зіпсує їй ані даних, ані портів — вона в іншому проєкті compose. Але пам'ять і диск у них спільні, і два Postgres не вміщуються там, де ледве вміщується один. Обійти можна прапорцем --alongside, і це свідоме рішення, а не формальність;
  • docker compose старший за 2.24.4 — див. нижче;
  • пісочниця вже стоїть. Друга поверх першої поділила б із нею томи, і прогін «з нуля» перестав би бути прогоном з нуля.

Правильне місце для пісочниці — машина розробника або окрема віртуалка.

Що буде при обриві

Пісочниця, яка лишила по собі том на 250 МБ і контейнер, що тримає порт, — це та сама шкода, від якої вона мала захистити. Тому:

  • Ctrl-C, SIGTERM, SIGHUP перехоплюються і прибирають усе;
  • будь-яка зупинка установки (ЗУПИНКА на кроці …) теж прибирає все;
  • у накладці стоїть restart: "no" — забута пісочниця не воскресає після перезавантаження хоста;
  • прибирання йде трьома ешелонами: compose down -v, потім пряме docker rm/docker volume rm за міткою проєкту (ловить те, що лишилось від обірваного up), потім видалення .env.sandbox;
  • --keep лишає уламки для розбору журналів — але тільки при невдачі установки, не при сигналі: перерваний прогін лишає по собі не стенд, а половину стенду.

Чого перехопити неможливо: kill -9 і зникнення живлення. Саме тому джерелом правди про залишки є не файл-позначка, а сам docker — і ./netpulse install та ./netpulse check при кожному запуску кажуть, якщо на машині висить забута пісочниця.

Чого пісочниця НЕ доводить

Це найважливіший абзац розділу. Зелена перевірка доводить тільки те, що вона перевіряє — цей проєкт уже платив за протилежне припущення.

  • Let's Encrypt і DNS. Пісочниця стоїть на localhost із самопідписаним сертифікатом і навмисно не читає netpulse.conf: справжній DOMAIN звідти відправив би її по сертифікат для адреси, яка веде на бойовий стенд, і витрачені спроби списались би з тижневої квоти домену.
  • Прийом SNMP-трапів і правило DOCKER-USER. Порт 162/udp назовні не виставляється взагалі: він не має автентифікації, і відкривати його заради перевірки означало б купити перевірку ціною дірки.
  • Розрахунок shared_buffers із пам'яті хоста. У пісочниці він заданий числом — це умова того, щоб вона нічого не поклала.
  • Поведінка під навантаженням. База порожня, хостів на моніторингу немає, історії немає.

Чому потрібен compose 2.24.4

Списки ports при накладанні compose-файлів додаються, а не замінюються. Щоб зсунуті порти пісочниці не стали додатком до базових 80/443/9443, накладка перевизначає їх тегом !override, а він з'явився у docker compose 2.24.4. На старішій версії пісочниця відмовляється працювати — бо мовчки зайняти порти бойового проксі гірше, ніж не запуститись.

Вимога знімається одним рядком у docker-compose.yml: якщо зробити базові порти змінними —

    ports:
      - "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_HTTP:-80}:80"
      - "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_HTTPS:-443}:443"
      - "${NETPULSE_BIND:-0.0.0.0}:${NETPULSE_PORT_GRPC:-9443}:9443"

— то пісочниці вистачить власного .env.sandbox, накладка портів стане непотрібною, а мінімальна версія compose лишиться 2.0, як у решті установника.

Пісочниця, режим другий: оновлення з версії на версію

Установку з нуля клієнт робить один раз, а оновлення — щоразу. І ламається воно частіше: міграції котяться не на порожню базу, а на ту, де вже лежать чужі дані; конфігурація змінюється; зонд у мережі клієнта лишається старим, а колектор стає новим. Досі цього не перевіряло нічого, тобто перевіряв перший клієнт.

./netpulse sandbox upgrade              # попередня версія обереться сама
./netpulse sandbox upgrade --from REF   # попередня версія — цей коміт або тег
./netpulse sandbox upgrade --keep       # лишити стенд після прогону

Прогін іде п'ятьма етапами, і кожен друкує свою назву — падіння називає етап, а не лише крок:

Етап Що відбувається
0 вибір попередньої версії й розгортання її дерева
А попередня версія ставиться з нуля тим самим install і проходить ту саму самоперевірку
Б у базу наливаються локації, 24 хости, доба ICMP і метрик із кроком 5 хв, конфіги NCM, тригер і алерти; знімаються кількості «до»
В дамп, збірка нової версії, міграції поверх наявних даних, перезапуск служб, перевірка старого зонда проти нового колектора, оновлення зонда
Г та сама selfcheck, що й після чистої установки
Д кількості «після» проти «до», по рядках

За замовчуванням стенд зноситься разом із томами: це ворота випуску, а не стенд для розглядання. --keep лишає все на місці — і тоді прибрати обов'язково: ./netpulse sandbox down.

Що таке «попередня версія», якщо версіонування ще немає

Питання не риторичне: від відповіді залежить, чого вартий прогін. Тегів немає, реєстру немає, образ завжди netpulse/server:dev.

Обрано коміт git. Коміт — це повне дерево: Go-код, міграції, docker-compose.yml, Caddyfile, Dockerfile. З нього збираються справжні старі образи, тобто попередня версія тут не описана, а виконується. Це відтворювано: той самий ref дасть той самий стенд і за півроку.

І це не тимчасове рішення. Тег у git — теж ref, тому в день першого тегу тут не зміниться жодного рядка: --from v0.1.0 запрацює сам.

Чому не інакше:

  • Тег. Найправильніше — і неможливе сьогодні: перевірка оновлення є вхідним квитком до першого тегу (ROADMAP, Етап 13). Вимагати тег означало б вимагати те, заради чого вона й пишеться.
  • Збережений дамп бази. Найдешевше й доводить найменше. Дамп — це схема з даними, але не бінарники: він не запускає старий колектор і старий зонд, тобто не бачить двох названих класів поломки з трьох. Гірше інше: дамп старіє мовчки. Його зробила версія, яку вже ніхто не збере, і коли прогін почервоніє, розрізнити «зламався код» і «протух дамп» буде нічим.

Типовий ref обчислюється, а не вписаний числом: береться найновіший коміт, у якого міграцій менше, ніж у HEAD. Причина та сама, що й у решті установника: зелений прогін, який нічого не перевірив, гірший за відсутність прогону. Оновлення без жодної нової міграції доводить лише те, що служби перезапустились, — і мовчки видається за доказ, що міграції котяться поверх даних. Якщо в парі версій нових міграцій немає, прогін про це кричить — і в момент вибору, і в підсумку.

Перед випуском беріть --from явно — із тією версією, з якої клієнти оновлюватимуться насправді. Автоматичний вибір — це «найближча попередня», а не «та, що справді стоїть».

Ізоляція: чому це не може зачепити нічого чужого

Що Як розведено
Проєкт compose netpulse-sandbox-upgrade — окремі контейнери, мережа й томи
Порти від 18300 на 127.0.0.1; режим установки з нуля перебирає 18080…18272 і сюди не дістає
.env власний .env.sandbox-upgrade; бойовий .env не читається й не пишеться
netpulse.conf не читається взагалі (див. read_conf)
Образи власні теги netpulse/*:sandbox-prev і :sandbox-new; спільний тег :dev не чіпається жодного разу
Файрвол хоста правило DOCKER-USER не додається: TRAPS_FROM порожній
Репозиторій дерево розгортається через git archive — він лише читає; ані worktree, ані checkout

Окремі теги образів — не дрібниця. Без них обидві версії називались би netpulse/server:dev, друга перетерла б першу, а обірваний прогін лишив би цей тег указувати на стару збірку — на очах у бойової інсталяції, яка ділить із пісочницею реєстр образів.

Дерево попередньої версії лежить у .sandbox-prev/ поруч із проєктом (не в /tmp: там часто tmpfs, і другий примірник вихідних текстів ліг би в оперативну пам'ять). З контексту збірки його виключає .dockerignore.

./netpulse sandbox down прибирає обидва режими одразу — контейнери, томи, мережу, .env, дерево й власні теги образів. Пам'ятати, у якому режимі стенд піднімали, не треба.

Що саме звіряється в кінці

Дві різні вимоги, тому й два правила:

  • exact — рядки, які налила сама пісочниця (хости sb-sw-*, їхні проби, ряди метрик, конфіги, тригер, алерти). Їхня кількість не має права змінитись ані в який бік: поменшало — оновлення знищило, побільшало — роздвоїло. Обидва однаково погані й обидва беззвучні.
  • minусе інше (кабінети, користувачі, довідники, аудит). Тут дозволено лише рости: система під час оновлення жива.

Кожен exact-запит звужено до рядків пісочниці, бо локальний зонд весь цей час пише свою телеметрію. Алерти рахуються по alr.alerts і alr.alerts_history одразу: фоновий такт переносить погашені в історію, і рядок, що переїхав, — це не втрачений рядок.

Чого цей режим НЕ доводить

  • Гілку «наявна інсталяція» в міграторі. База пісочниці народжується чистою, тому public.netpulse_install.fresh назавжди true, і мігратор іде гілкою «видати паролі ролям». Перехід стенду, зробленого до 0063 (deploy/RLS-EXISTING-INSTALL.md), лишається неперевіреним.
  • Зміни в самому установнику між версіями. Обидва етапи веде один і той самий ./netpulse — вимірювальний прилад має бути тим самим на обох кінцях вимірювання, інакше різниця в приладі читається як різниця у виробі. Під перевіркою тут те, що установник ставить, а не він сам.
  • Оновлення через кілька версій підряд. Перевіряється рівно один стрибок: з обраного ref у HEAD.
  • Усе те, чого не доводить і перший режим: Let's Encrypt і DNS, трапи на 162/udp, поведінка під навантаженням.

Підключення зонда

В інтерфейсі: Зонди → Додати зонд. Видане запрошення (np_enr_…) одноразове — після реєстрації воно згоряє.

На машині, де стоятиме зонд:

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 — без нього після відновлення всі активні сесії відваляться. Не смертельно, але користувачі помітять.
docker compose exec -T db \
  pg_dump -U netpulse -d netpulse -Fc --no-owner \
  > netpulse-$(date +%F).dump

Формат -Fc (custom), а не простий SQL: він стискається і дозволяє відновлювати вибірково.

Ключі зберігати окремо від дампа — інакше сенс шифрування секретів зникає: той, хто дістав бекап, дістав і ключ до нього.

Том git-data бекапити не обовʼязково: тіла конфігів лежать зашифрованими в базі, і репозиторій повністю відтворюється з неї —

docker compose run --rm --entrypoint netpulse-gitsync api

Зворотне невірно: з репозиторію базу не відновити. Тому джерелом істини лишається дамп, а Git — похідне сховище, яке коштує один запуск команди.

Автоматично, щодня

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 вимагає рамки навколо відновлення: без неї фонові процеси агрегації втручаються в наливання даних і дамп лягає пошкодженим.

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, що й на момент дампа. Інакше застосунок підніметься, але кожна спроба скористатись збереженим паролем поверне помилку розшифрування — і виглядатиме це як зламані креденшели, а не як втрачений ключ.

Перевірка після відновлення:

docker compose exec -T db psql -U netpulse -d netpulse -c \
  'SELECT count(*) FROM core.devices;'
curl -sf https://<NETPULSE_DOMAIN>/healthz && echo OK

Оновлення

git pull
docker compose up -d --build

migrate відпрацює першим і не дасть піднятись API, якщо схема не накотилась. Міграції йдуть по одній у транзакції; уже застосований файл зі зміненою контрольною сумою зупиняє весь запуск — це захист від мовчазного розходження схеми з кодом.

Відкат схеми не передбачений: зворотні міграції на даних телеметрії коштують дорожче, ніж відновлення з бекапу.

Перед тим як віддавати оновлення клієнтові, цей самий шлях треба прогнати в ізоляції: ./netpulse sandbox upgrade --from <версія, що стоїть у клієнта>. Він ставить стару версію з нуля, наливає в неї дані, оновлює й окремо звіряє, що дані пережили. Розділ «Пісочниця, режим другий» вище.

Ізоляція кабінетів (RLS)

Нова інсталяція вже під політиками — робити нічого не треба. netpulse-migrate на чистій базі сам видає паролі ролям netpulse_app і netpulse_worker, і застосунок з першої секунди ходить роллю без BYPASSRLS.

Ізоляція тримається на двох незалежних рубежах: політика RLS у базі й предикат tenant_id у кожному запиті коду. Другий потрібен окремо, бо на гіпертаблицях RLS не працює взагалі — TimescaleDB не поєднує його зі стисненням, а туди йде вся телеметрія.

Інсталяціям, старшим за 0063, застосунок і далі ходить роллю netpulse — тобто суперкористувачем, який політики обходить, — і другий рубіж вмикається окремою оборотною процедурою: deploy/RLS-EXISTING-INSTALL.md. Вона не змінює даних. Робити її разом з оновленням версії не варто: ламатись у них різне, і розбирати доведеться одночасно.

Свій випадок видно одним запитом:

docker compose exec -T db psql -U netpulse -d netpulse   -c "SELECT fresh FROM public.netpulse_install"

Зміна ключа шифрування

Ключі перелічуються через кому, новий — першим:

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 його не перечитає.

Дашборд на телевізор

В інтерфейсі: Дашборд → На телевізор → Видати посилання. Отриману адресу відкривають на екрані в диспетчерській — вона не потребує входу.

Телевізор нікуди не залогиниш: сесія протермінується, браузер оновиться, і зранку на стіні висітиме форма входу замість карти мережі — рівно тоді, коли на неї дивляться.

Що дає посилання і чого не дає:

  • тільки читання цього дашборда: його плитки, активні алерти й метрики тих хостів, які на ньому показані;
  • метрики чужого хоста за ним не дістати навіть підбором ідентифікатора;
  • решта кабінету — інвентар, конфіги, налаштування, секрети — недоступна;
  • відкликається одним рухом, старе посилання одразу мертве.

Токен показується один раз. Видати нове можна будь-коли — попереднє при цьому перестає працювати.