Один NAT Gateway – це чверть витрат на AWS, і навіть маючи AWS Credits мене трошки душить жаба.
Є, звісно, варіант прибрати NAT Gateway зі схеми взагалі:
можна просто перевести EC2 в Public Subnet: насправді, описаний сетап з використанням AWS Load Balancer дійсно трохи overkill для проекту типу якогось невеликого блогу – але мені все ж хочеться мати “красиву” інфрастуктуру, побудовану “по канонам” та більш наближену до AWS Well-Architected Framework (свідомо без Reliability – тільки одна Availability Zone, та без infrastructure as code – див. AWS Well-Architected Framework Design Principles)
можна прибрати NAT Gateway взагалі – але залишити EC2 в Private Subnet:
по факту, в описаному сетапі NAT GW використовується тільки на самому початку, коли встановлюються пакети nginx, php та скачується архів з WordPress
але потім, по-перше – треба встановлювати апгрейди із зовнішніх репозиторіїв, по-друге – сам WordPress може ходити в інтернет при запуску своїх cron jobs або робити виклики з плагінів (цікавий приклад є в пості TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death”, де плагін Page View Count постійно робив запити з серверу RTFM до Cloudflare)
Але є і третій варіант – це власний підняти “poor man’s NAT gateway“: просто запустити окремий мінімальний t3.nano EC2, на ньому мати Linux чи FreeBSD, а там налаштувати звичайний NAT.
Бо по суті AWS VPC можна розглядати по аналогії з домашньою мережею, де в ролі NAT Gateway виступає роутер – TP-Link, MikroTik тощо, і цей роутер можна замінити на будь-який ноутбук або ПК з Linux/FreeBSD та налаштувати там NAT з блекджеком і моніторингом.
В коментах до цього поста підсказали про проект fck-nat – вже готовий до використання AWS AMI, до того ж для fck-nat є Terraform модуль terraform-aws-fck-nat.
UPD: Картина в Costs Explorer через кілька днів:
AWS Managed NAT Gateway прибрано взагалі
додано t2.nano в ролі self-managed EC2 NAT Gateway
EC2 блога замінено з t3.medium на t3.small
Результат: 8-9 березня загальна вартість була рівно $5.00, 13-го стало $3.26.
Плюси-мінуси
Звісно, для якогось production сетапу якогось реального проекту простіше і – головне – надійніше мати стандартний AWS Managed NAT Gateway: zero геморою з апгрейдами, zero проблем з availability – за все відповідає AWS.
Якщо ж мати власний NAT Gateway – то це додатковий час на встановлення апгрейдів, моніторинг, ну і якщо такий інстанс впаде – то інстанси в приватних сабнетах залишаться без доступу в Internet.
Ну і якщо робити автоматизацію – то описати з Terraform один чи кілька NAT Gateways набагато простіше, ніж описувати налаштування окремого EC2 – фактично, достатньо просто вказати один параметр у VPC module resource.
Але якщо подивитись на вартість…
А шо по грошам?
Давайте порівняємо вартість AWS Managed NAT Gateway та власного NAT на t3.nano.
Порівнювати будемо і вартість самого інстансу, і трафіку, бо все рахується окремо.
NAT Gateway vs t3.nano: per-hour pricing
Перше, і саме відчутне – це погодинна оплата:
AWS Managed NAT Gateway: $0.048/година – це ~$32 в місяць
AWS EC2 t3.nano: $0.0059/год – це ~$4.3 на місяць
t3.nano в ролі NAT Gateway для якогось невеликого проекту – з головою: всі операції виконуються в ядрі системи, навантаження на CPU/RAM мінімальне. Єдине. на що варто звертати увагу, це network bandwidth – але для t3.nano маємо до 5 Gbps burst – з запасом, див. Amazon EC2 instance network bandwidth.
NAT Gateway vs t3.nano: per-gigabyte pricing
Тут є нюанс з тим, за що саме ми платимо при використанні AWS Managed NAT Gateway, див. Amazon VPC pricing:
NAT Gateway Data Processing Charge: 1 GB data went through the NAT gateway. The Data Processing charge will result in a charge of $0.045.
тобто кожен переданий гігабайт, незалежно від напрямку, ми платимо $0.045
Data Transfer Charge: This is the standard EC2 Data Transfer charge for internet traffic […] With the Data Transfer Out to Internet rate set at $0.09 per GB […]
і на додачу до NAT Gateway Data Processing – ми ще сплачуємо $0.09 за кожен гігабайт, яки віддали в інтернет
Отже, кожен переданий з VPC в інтернет через AWS Managed NAT Gateway гігабайт нам коштує $0.135.
Тоді як при використанні AWS EC2 instance в ролі NAT Gateway ми сплачуємо тільки Data Transfer Charge $0.09 per GB.
План дій
Отже, що будемо робити:
запустимо EC2 в публічній мережі
Security Group:
дозволяємо тільки out трафік – на вхід з інтернету нічого не має приходити
дозволяємо весь ingress з Private Subnets – наш EC2 буде передавати дані назовні
дозволяємо SSH з VPC або користуємось EC2 Instance Connect
на EC2 маємо якийсь Linux, на якому з iptables налаштовуємо NAT
в Route Tables для VPC і Private Subnets налаштуємо default route – 0.0.0.0/0 через Private IP цього EC2
Вибір операційної системи – в принципі, будь-який Linux, тут будемо робити з Amazon Linux, бо дійсно менше геморою ніж з Debian.
Хоча спочатку взагалі думав про FreeBSD – бо FreeBSD традиційно сильна саме в networking, але як вже основний сервер для самого блогу на Amazon Linux, то не буду розводити зоопарк.
На Amazon Linux 2023 по-дефолту встановлений nftables – але iptables теж є як wrapper, встановлюється з репозиторію окремим пакетом.
Створення NAT Gateway Security Group
Переходимо в VPC, додаємо нову групу:
Створення EC2
Задаємо ім’я, вибираємо операційну систему, тут буде Amazon Linux:
Вибираємо тип інстансу, вибираємо або створюємо ключ для SSH:
В Network settings вибираємо VPC, в Subnet вказуємо той, в якому “main EC2” з блогом – аби уникнути cross Availability Zone traffic (див. Overview of Data Transfer Costs for Common Architectures), та включаємо Auto-assign public IP – бо NAT Gateway повинен мати Public IP, на який буде отримувати пакети від клієнтів в інтернеті:
Тут все – запускаємо створення інстансу.
Після старту робимо важливу зміну в його налаштуваннях – треба виключити перевірку Source/Destination check: вона вказує EC2 приймати і відправляти тільки той трафік, де source або destination співпадає з його власним IP.
Для NAT це треба вимкнути, бо NAT instance по визначенню пересилає чужий трафік, де ні source, ні destination не є його власним IP:
Перевіряємо параметр net.ipv4.ip_forward, який задає ядру системи дозвіл на пересилання пакетів між мережевими інтерфейсами – саме це і є основою будь-якого NAT і роутингу:
[ec2-user@ip-10-0-1-79 ~]$ ip a s
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
...
2: enX0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
...
-A POSTROUTING: додаємо правило в POSTROUTING chain – тобто правило спрацьовує після того, як ядро вже вирішило куди відправити пакет – але ще не відправило отримувачу
-o enX0: правило тільки для пакетів що виходять через інтерфейс enX0 (наш публічний інтерфейс)
-j MASQUERADE: підміняємо source IP пакету (Private IP інстансу EC2, на якому буде блог) на IP інтерфейсу enX0 – публічний IP нашого інстансу EC2 з NAT Gateway
В upgrade_type можна задати security, а не встановлювати всі – бо це все ж gateway:
...
# default = all available upgrades
# security = only the security upgrades
upgrade_type = security
...
apply_updates = yes
...
[email]
# TODO in the following rtfm.co.ua posts about setting up AWS
Про відправку email з AWS EC2 інстансів може буду писати окремо в частині по налаштуванню AWS Simple Email Service.
Прийшов час для мажорного апгрейду серверу RTFM, який зазвичай роблю переїздом на новий сервер, бо заодно роблю різні інші апгрейди, як-от версію PHP або навіть міграцію в інший клауд.
Цього разу планую переїжджати з DigitalOcean, де RTFM хоститься з 2020 року. До самого DigitalOcean претензій нуль – всі ці роки системи працювали бєз єдіного разриву (с) Антон Уральский, але, раптом, з’ясував, що в мене на AWS накопичилась купа AWS Credits, які мені як AWS Hero видають кожного року. При цьому за хостинг і бекапи в DigitalOcean я плачу живими гривнями і, в принципі, немало – близько 40 баксів в місяць.
Ну і RTFM в AWS вже колись хостився – з 2015, здається, до 2020 року.
Отже, що треба захостити – це невеликий блог WordPress, тому сетап буде без fault tolerance і high availability.
Робити будемо методом “clickops” – без Terraform, просто руками.
Чому без Terraform – бо, по-перше, мені цікаво глянути, що ж нового з’явилось в AWS Console, бо насправді не так часто туди заглядаю і тим більш щось роблю руками. По-друге – просто нема сенсу робити якусь автоматизацію, бо скоріш за все щось буду перероблювати-міняти і потім більше часу витрачу на зміни в коді, ніж на сам сетап. Ну і сама інфраструктура відносно маленька.
А оскільки це чисто особистий проект для хостінгу одного сайту і без всяких Dev/Staging/Prod оточень – то і сенсу тягнути сюди Terraform мало.
Та і насправді коли робив все описане нижче – ловив дуже приємні флешбеки в роки 2015-2016, коли тільки ще знайомився з AWS і мало користувався Terraform.
І навіть є якийсь особливий вайб в тому, щоб самому все створити ручками, а не просто описати в коді Terraforrm resources.
Втім, новий пост напишу, і буде він більшедля тих, хто тільки знайомиться з AWS і хоче побачити як можна побудувати базову інфрастуктуру для хостінг веб-сайта – або для тих, хто хочеться трохи ностальгії за тими часами, коли ми не все крутили в Кубернітісах 🙂
Намагався описати максимально стисло – але вийшло багато матеріалу.
Планування архітектури
Чисто базовий етап для майже будь-якого веб-сервісу – базова мережа, один EC2, один RDS, все в одній Availabilty Zone.
На EC2 буде Amazon Linux з NGINX та PHP-FPM, база даних блогу – AWS RDS MariaDB.
Спочатку планував Debian, бо система “поставив і забув”, але її використання в AWS потребує трохи геморою – а Amazon Linux працює просто з коробки.
EC2 буде в приватній мережі, без прямого доступу з інтернету, і спочатку для доступу до інстансу думав використати AWS SSM, який насправді ніколи толком не юзав, бо по роботі все в єтіх ваших Кубернетісах, але він прям overkill, потребує достатньо багато додаткових налаштувань – IAM Role та VPC Endpoints, що коштує додаткових грошей, тому все ж вирішив робити через AWS EC2 Instance Connect.
Для доступу до WordPress на EC2 в систему додамо AWS Load Balancer, до якого потім ще можна буде підключити AWS WAF.
І не буде робитись EC2 AutoScaling – бо це теж трошки overkill для маленького блогу. Правда, RDS, у якого мінімум 20 гіг диск при базі RTFM в 1.2 гіга теж таке собі, але нехай буде – подивимось на “традиційний” сетап подібної інфраструктури.
Отже, план такий
AWS Availability Zones:
всі ресурси (EC2 та RDS) будуть в одній AZ, але мережа має бути мінімум в двох
Network – VPC та Subnets:
створимо одну AWS VPC з чотирма Subnets у двох Availability Zones:
Public Subnets: тут будуть жити сервіси, яким треба мати Public IP – Load Balancer, NAT Gateway
Private Subnets: тут буде жити EC2 з WordPress та RDS з MariaDB
налаштуємо AWS Load Balancer для доступу до WordPress
EC2:
один сервер з Amazon Linux та NGINX та PHP-FPM
RDS:
мінімальний інстанс RDS з MariaDB – буде жити в приватному сабнеті в власною Secuity Group та автоматичними бекапами
Route 53:
для доступу до бази даних створимо окрему локальну DNS zone, яка буде доступна тільки в межах VPC
Security:
перший рівень захисту – це мережа: всі робочі ресурси будуть в приватних сабнетах
до них додамо Security Groups – для самого EC2, для AWS RDS і для Load Balancer,
пізніше можна буде глянути на AWS VPC NACL і потрогати AWS WAF – дуже давно з ним не працював
SSH та VPN і доступ до EC2:
EC2 Instance Connect для SSH
пізніше встановлю WireGuard і підключу до мого домашнього MikroTik та зможу підключатись по SSH вже напряму
І кілька слів по самому AWS.
Перше – вибір регіону: тут в першу чергу звертаємо увагу на локацію і клієнтів – якщо у нас основні клієнти в USA, то, логічно, вибираємо регіони там.
Другий момент, на який звертаємо увагу – ціна, бо в кожному регіоні ціни трохи відрізняються, хоч і не в рази.
Тому для RTFM візьму Ірландію (eu-west-1) – там і тихо (не літають шахеди, як в ОАЕ у 2026 році), і з європейських AWS Regions вона сама дешева.
Поїхали.
AWS Costs
Питання костів, коли працюєш з AWS, актуальне завжди.
Описаний нижче сетап вийшов в 5 USD/day, тобто 150 баксів на місяць – і це ще без урахування трафіку і додаткових сервісів типу бекапів, SSM та WAF.
В кінці буде детальний розбір по костам.
Ну і хоча пост називається “сетап базової інфраструктури” – але за великим рахунком для якогось персонального блогу він дуже overengineered: спокійно можна обійтися без приватних сабнетів, без AWS Apliaction Load Balancer, і навіть без AWS RDS. І якби я робив для RTFM і не мав вільних кредитів – то робив би набагато простіше.
Втім, якщо будувати щось більш “production grade”, то описана нижче інфраструктура як раз і є базовою, або, радше, “традиційною” – з розділенням мережевого доступу, з винесенням баз даних на окремий інстанс, з Load Balancer.
Створення VPC
Починаємо з основи всього – VPC.
VPC дасть нам ізоляцію, дасть можливість отримати доступ до ресурсів в приватних сабнетах, дасть можливість зекономити на трафіку – бо зможемо ходити до ресурсів AWS S3 через внутрішні ендпоінти, а не через інтернет.
Нам треба зробити:
Private Subnets: для EC2 та RDS
ще можна і бази даних винести в окремі subnets – але це вже точно поза “сетап базової інфраструктури для WordPress”, тому не робимо
Public Subnets: для Load Balancer та NAT Gateway
AWS ALB потребує мінімум 2 subnets, тому робити будемо у двох Availability Zones, хоча всі ресурси будуть жити тільки в одній.
Основні налаштування
В панелі створення VPC багато чого змінилось з того часу, як я тут щось робив руками – додалась можливість через “VPC and more” створити відразу все – спробуємо, як це працює.
Єдиний, як на мене, недолік в цій можливості “все і одразу” – не так добре розумієш що і для чого створюється, а створення якихось ресурсів взагалі проходить повз уваги: я, наприклад, тільки через декілька днів згадав, що в AWS VPC для Public Subnets створюється ще і Internet gateway.
Тому якщо вперше знайомишся з AWS і VPC, то в старому підході “робити все руками” все ж є сенс.
Якщо хочеться зробити “по-старому” – то описував цей процес ще у 2016 році, якихось кардинальних змін в побудові нетворку не було:
Автогенерація імен ресурсів – теж прикольна штука, і генерує достатньо адекватні імена як раз в тому стилі, як я це завжди робив – з іменем subnet type та Availability Zone:
Вибір CIDR важливий, особливо, якщо планується мати кілька VPC і між ними будувати “мости” у вигляді VPC Peering – треба розрахувати так, щоб адреси не перетинались.
Крім того, конкретно в моєму кейсі, треба враховувати майбутній VPN, у якого власна мережа для клієнтів – 10.100.0.0/24.
AWS по дефолту пропонує 10.0.0.0/16 – можна так і залишити, хоча, звісно, для такого проекту адрес буде забагато.
Але, головне, щоб ця мережа не перетинається з 10.100.0.0/24, бо в 10.0.0.0/16 входять адреси від 10.0.0.0 до 10.0.255.255.
тут я вже почав писати про розрахунок адрес, але вийшло таке полотенце тексту, що вирішив його винести окремим матеріалом
Отже, залишаємо дефолтний блок 10.0.0.0/16:
IPv6 нам не треба, пропускаємо.
Tenancy – щось на дуже дорогому: можливість запускати всі свої EC2 на виділеному для AWS Account hardware серверах, зараз це точно не треба, див. Amazon EC2 Dedicated Instances.
VPC encryption control – щось нове, дозволяє включити контроль використання plaintext трафіку в мережі, нам не треба, пропускаємо.
Number of Availability Zones задаємо у 2, це мінімум для ALB:
Створення VPC Subnets
Далі треба налаштувати сабнети двох типів і створити по одному сабнету кожного типу в кожній Availability Zone.
Перший блок /24 я залишаю “резервним”, да і виглядає так красивіше:
Regional NAT Gateways – нова плюшка від AWS, не так давно з’явилась – дозволяє повністю автоматизувати створення NAT Gateways в нових Availablity Zones, не потребує Public Subnets, автоматично апдейтить Route Tables.
Але коштує, звісно, дорожче, та і взагалі в цьому сетапі не потрібна.
Створюємо класичний Zonal NAT Gateway і тільки в одній Availability Zone:
Завершення налаштувань VPC
VPC Endponts – залишаємо дефолтний S3, бо в мене, наприклад, до S3 пишуться бекапи блогу. Пізніше ще додамо новий, для EC2 Instance Connect.
DNS Options залишаємо включеними – штука корисна і грошей не просить:
DNS hostnames: чи створювати “локальні” імена, наприклад – ip-10-0-3-226.eu-west-1.compute.internal – потрібно, аби коректно працювали RDS, EFS та інші мережеві ресурси
І в результаті маємо таку картину (раніше теж не було, дуже зручно, і, здається, навіть route tables і маршрути створювались руками):
Все – поїхали створювати, займе трохи часу – міжна поки зробити чай.
За кілька хвилин – все готово:
Створення Security Groups
Зробимо три окремі Security Groups – для EC2, для RDS, та для Load Balancer:
В Security Group для EC2 дозволяємо SSH в межах VPC, дозволяємо HTTP від Public Subnets – там будуть інстанси Load Balancer (які, по суті, під капотом являють собою звичайні AWS EC2 – як і для AWS RDS):
Для SSH можна зробити більш суворі правила – дозволити тільки з VPN CIDR та VPC Private Subnet в eu-west-1a, де пізніше буде створюватись EC2 Instnace Connect Ednpoint – але це вже можна буде підтюнити пізніше, коли все буде працювати.
Аналогічно створюємо Security Group для Load Balancer – тут дозволяємо весь In на порти 80 та 443:
І для RDS – відкриваємо порт 3306 з Private Subnets, бо окрім EC2 сюди ніхто ходити не має:
Створення EC2 Instance Connect Endpoint
Коли планував робити з сервер на Debian, то думав доступ робити через AWS SSM – але для SSM потребує аж трьох VPC Endpoints, і за кожен треба платити гроші.
Вибираємо створену вище VPC. Опція “Preserve Client IP” – штука прикольна, передає клієнтський IP замість адреси самого Endpoint – можна буде спробувати пізніше, поки залишаємо в дефолтному “off”:
Створюється довго, хвилин 5 – як раз встигнемо зробити ще чаю та запустити EC2.
Створення EC2
Вибираємо Amazon Linux, задаємо ім’я інстансу:
Вибір типу інстансу та підрахунок потрібної пам’яті
Дуже коротко про типи, бо матеріал і так виходить великий, детальніше див. Amazon EC2 instance types.
Всі AWS EC2 діляться на кілька основних типів інстансів:
general purpose: збалансовані по CPU/RAM та вартості типи
сюди входять і burstable type, такі як t3/t4 – доступ до CPU більш обмежений, але на короткий час може, власне, burst – видаватись 100% процесорного часу, див. CPU Credsts, див. Key concepts for burstable performance instances
приклад general purpose:
t3.medium – 2 vCPU, 4 GiB RAM, ~ 30 USD/month
t3.large: 2 vCPU, 8 GiB RAM, ~ 60 USD/month
m5.large: 2 vCPU, 8 GiB RAM, ~ 69 USD/month
compute optimized: “заточені” під CPU – більше CPU, менше RAM:
Цифри 3/4/5/6 etc – покоління інстансів, чи вище цифра – тим новіше залізо “під капотом”, плюс можливості самого AWS (наприклад, в старих t2 нема підтримки підключення з serial console).
Плюс кожен тип має “підтипи”:
g: процесори Graviton – процесори від самого AWS на архітектурі – можуть бути не з усім сумісні, але використання ~20-30% дешевше, ніж у звичайних типів інстансів ARM при вищій швидкості виконання задач
i: процесори Intel – Intel Xeon, Intel Ice Lake
a: процесори AMD – AMD EPYC
n: окремий модифікатор, “network” – вищий network bandwidth, наприклад, інстанси R6in – Intel Network
Аби прикинути споживання пам’яті кожним можемо глянути в RSS (Resident Set Size), реальна фізична пам’ять процесу – але сюди включається пам’ять на shared бібліотеки, тобто якщо кілька PHP-FPM workers використовують одну і ту ж libc – RSS кожного включає її повністю і сумарний RSS буде завищений.
Втім – нехай буде завищена, бо ми прикидуємо “найгірший” варіант.
Видаляємо перший сертифікат, повторюємо процес створення та валідації – і тепер все готово:
Створення AWS Load Balancer
Використання Load Balancer дасть змогу, власне, балансувати навантаження – якщо планується мати кілька інстансів, то зручно мати один і той статичний URL, який можна використати як CNAME для домену, буде можливість додавати або замінювати інстанси без необхідності внесення змін в DNS домену, і дасть можливість використання AWS Web Application Firewall.
Плюс, ALB спрощує менеджмент SSL/TLS – один раз створюємо сертифікат, підключаємо його до ALB, і SSL termination буде на Load Balancer: клієнти до Load Balancer ходять з HTTPS, а Load Balancer до EC2 по HTTP – простіший конфіг NGINX, нема потреби в налаштуванні Let’s Encrypt.
Хоча за великим рахунком, якщо планується дійсно невеликий персональний сайт – то ALB теж overkill. Втім, якщо є зайві кредити, то його використання дійсно спрощує життя.
Створення Target Group
Load Balancer працює через Target Groups (TG), де кожна TG включає в себе один або декілька EC2 на які ALB буде слати трафік.
Створюємо нову Target Group:
В Type вказуємо Instance, задаємо ім’я групи та протокол, за яким буде відбуватись комунікація ALB з сервісами на EC2 в цій групі.
На EC2 у нас NGINX який приймає підключення на порт 80 – тому в Protocol та Port залишаємо дефолтні параметри:
Якщо на ALB плануємо використовувати і HTTP і HTTPS – то вказуємо HTTP/1, якщо тільки HTTPS – то можна HTTP/2.
Хоча зазвичай для HTTP просто налаштовується redirect на HTTPS і можна було б тут відразу вказати HTTP2 – але деякі клієнти все ще можуть використовувати HTTP v1 – тому залишимо їм право вибору і залишаємо дефолтну опцію HTTP1:
В Health checks можна залишити все як є – Health check path на NGINX буде “/”, Traffic port буде 80:
Вибираємо інстанс(и) для цієї Target Group:
Підтверджуємо створення:
Типи Load Balancers: ALB vs NLB vs GLB vs CLB
Amazon дозволяє створити кілька типів Load Balancer:
Application Load Balancer:
модно-молодьожно
працює на L7 (HTTP/HTTPS), може читати зміст HTTP-запиту і мати окремі налаштування по, наприклад, URI (/api/ – слати на одну Target Group, /users/ – слати на Auth0 тощо)
підтримує роботу з WebSocket, gRPC
Network Load Balancer:
працює на L4 (TCP/UDP) – дуже швидкий, чудовий вибір для high load applications, має можливість використання Static IP
Gateway Load Balancer:
доволі специфічна штука, для роутингу трафіку через сторонні network appliances (firewall, IDS/IPS), я ніколи не користувався
Окремо загадаємо про Classic Load Balancer – легасі, deprecated. Підтримує і L4 і L7 але гірше ніж ALB/NLB окремо.
Задаємо ім’я, тип Internet-facing (тип Internal – корисна опція, коли треба мати ALB, який доступний тільки всередині VPC):
Вибираємо VPC, Subnets та Secuiruty Group, яку робили на початку:
Для HTTP Listener налаштовуємо редірект на HTTPS:
А в HTTPS Listener підключаємо створену вище Target Group:
Підключаємо SSL сертифікат із ACM:
Перевіряємо, що все ОК і створюємо:
Створення займе кілька хвилин – робимо ще один чай.
Налаштування DNS для ALB
Поки створюється ALB – додамо новий запис в Route 53, який буде прив’язаний до створеного Load Balancer.
Тут приклад на іншому домені, але він в AWS ACM був доданий, тому буде працювати без помилок TLS.
Створюємо новий DNS Record, вибираємо тип Alias, знаходимо наш ALB, Routing policy залишаємо Simple (див. Choosing a routing policy):
Перевіряємо, чи все працює (може зайняти 5-10 хвилин на апдейт DNS):
Створення AWS Relational Database Service
Останній крок перед запуском WordPress – створити сервер даних.
З AWS RDS працюю дуже давно, сервіс класний, хоча, звісно, не безкоштовний. Але “перекласти відповідальність” за стабільність і бекапи на плечі AWS – чудове рішення для якогось production.
Плюс інтеграція з AWS IAM, CloudWatch Logs та Metrics, автоматичні бекапи, автоскейлінг – можливостей багато.
Створюємо новий сервер (хоча меню називається “Create database” – але створюється саме окремий інстанс):
В Credentails management можна залишити дефолтний AWS Secrets Manager – він вміє автоматично ротейтити пароль root для сервера, див. Set up automatic rotation for Amazon RDS:
Залишаємо опцію Password and IAM database authentication – хоча інтеграція IAM обмежує доступ тільки до самого серверу, а не баз даних, і все одно треба буде створювати юзера з власними правами доступу і паролем, див. AWS: RDS з IAM database authentication, EKS Pod Identities та Terraform.
Тип інстансу вибираємо мінімально доступний, db.t3.micro – хоча для RTFM і цього вистачить з великим запасом:
Розмір диску – мінімум 20 гігабайт, що при розмірі БД у RTFM в 1.2 гігабайти теж з головою.
Корисна штука для production – storage autoscaling: працює повністю непомітно для сервера і клієнтів:
В Connectivity можна автоматично налаштувати підключення до EC2 – створить всі необхідні параметри в VPC та Subnets, але давайте хоч тут зробимо вручну.
DB Subnet Group – створюємо нову, RDS сам вибере потрібні private subnets, бо далі, в Public access, ми задаємо “No” – сервер баз даних має жити тільки в приватних мережах, без доступу у світ.
У VPC Security Group вибираємо групу, яку створювали на початку:
В Additional monitoring settigns – інтересу раді можна включити Enhanced monitoring, це коштує додаткових грошей – але в production може дуже знадобитись, бо додає метрики по роботі операційної системи (CPU per process, RAM, disk I/O, network, file system), див. довгий пост по PostgreSQL: AWS RDS Performance and monitoring – був цікавий випадок, коли Enhanced monitoring знадобився:
Additional configuration – тут відразу можемо створити базу даних і налаштувати автоматичні бекапи.
Автоматичні бекапи (Periodic snapshots) – дуже рекомендована штука, рятувала не один раз: створює повний snapshot інстансу, і потім з цього снапшоту можна в будь-який момент створити новий інстанс з усіма даними.
До того для RDS є можливість налаштувати Continuous backups – для відновлення стану баз(и) на якийсь конкретний момент часу, див. Amazon Relational Database Service backups.
Базу створимо пізніше вручну, залишаємо бекапи:
І в кінці відразу бачимо приблизну вартість – раніше не було цього, зручно зробили:
DNS та Private hosted zone
Корисна з точки зору безпеки штука – приватні доменні зони, які доступні тільки всередині VPC, див. Working with private hosted zones.
Тому створимо окрему зону з DNS Records, які потрібні тільки в VPC, в нашому випадку – Database URL як раз чудовий приклад:
Знаходимо URL в самому RDS:
Додаємо його як value в CNAME нового запису:
Підключення до RDS
Підключаємось по SSH до EC2, шукаємо пакет mariadb:
[ec2-user@ip-10-0-3-146 ~]$ dnf search mariadb
...
mariadb114.x86_64 : A very fast and robust SQL database server
...
Аби встановити тільки клієнт – вибираємо mariadb без -server:
І підключаємось, використовуючи local DNS record, який створили вище:
[ec2-user@ip-10-0-3-146 ~]$ mysql -h db.rtfm.local -P 3306 -u rtfm_root -p
Enter password:
...
MariaDB [(none)]>
Або можна це трохи автоматизувати з AWS CLI – в RDS є приклад команди:
Запуск WordPress
Ну і нарешті – у нас все готово для запуску WordPress.
Що нам залишилось – це створити базу даних, юзера, і на EC2 встановити PHP.
Створення бази даних в RDS
Створюємо базу даних і юзера – для WordPress рекомендовано utf8mb4_unicode_ci (підтримка всяких емодзі):
MariaDB [(none)]> CREATE DATABASE test_wp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.064 sec)
MariaDB [(none)]> CREATE USER 'test_wp_user'@'%' IDENTIFIED BY 'test_wp_pass';
Query OK, 0 rows affected (0.058 sec)
MariaDB [(none)]> GRANT ALL PRIVILEGES ON test_wp_db.* TO 'test_wp_user'@'%';
Query OK, 0 rows affected (0.034 sec)
MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.027 sec)
Установка PHP та модулів
Встановлюємо PHP і модулі – хоча тут не всі модулі, не пам’ятаю, що треба ще для роботи RTFM, але це базовий набір для WordPress та, в принципі, будь-якого вебсайту:
Для перевірки PHP створюємо тестовий файл /var/www/html/index.php:
<?php phpinfo(); ?>
Виконуємо nginx check config && reload:
[root@ip-10-0-3-146 ~]# nginx -t && systemctl reload nginx
nginx: [warn] conflicting server name "_" on 0.0.0.0:80, ignored
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
І перевіряємо файл index.php в браузері:
Установка WordPress
Завантажуємо архів, розпаковуємо, міняємо власника та групу на nginx:nginx:
Відкриваємо в браузері – не завантажуються CSS та картинки.
Але це ОК, далі поправимо, не критично.
Критично буде далі з RDS – тому спочатку дочитайте цю частину:
Клікаємо Let’s go, задаємо параметри підключення до RDS:
І ловимо помилку “Error establishing a database connection“:
Ну… 🙂
WordPress users know that feeling 🙂
AWS RDS та WordPress “Error establishing a database connection”
Перше, що можна спробувати – це створити файл wp-config.php вручну і задати параметри явно:
...
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'test_wp_db' );
/** Database username */
define( 'DB_USER', 'test_wp_user' );
/** Database password */
define( 'DB_PASSWORD', 'test_wp_pass' );
/** Database hostname */
define( 'DB_HOST', 'db.rtfm.local' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
...
Але в цьому конкретному випадку це не допоможе.
Тому встановлюємо php-cli:
[root@ip-10-0-3-146 html]# dnf install -y php-cli
І спершу перевіряємо, чи працює DNS Resolver на нашу приватну DNS zone:
Тепер пробуємо MySQL connect – і от тут вже ловимо саму помилку – “Connections using insecure transport are prohibited“:
[root@ip-10-0-3-146 html]# php -r "mysqli_connect('db.rtfm.local', 'test_wp_user', 'test_wp_pass', 'test_wp_db') or die(mysqli_connect_error());"
PHP Fatal error: Uncaught mysqli_sql_exception: Connections using insecure transport are prohibited while --require_secure_transport=ON. in Command line code:1
Stack trace:
#0 Command line code(1): mysqli_connect()
#1 {main}
thrown in Command line code on line 1
Тут два варіанти вирішення – або в wp-config.php примусово включити SSL для підключення (рекомендується):
Болюча тема для будь-якого IaaS/PaaS провайдеру – будь-то Google Cloud Engine, Microslop Microsoft Azure чи AWS.
Коротко пройдемось по описаному вище сетапу – що і скільки в результаті коштує по грошам.
В Cost Explorer бачимо таку картину:
5 доларів на день, за 30 днів буде 150 доларів.
Ну – дуже не слабо, як для приватного блогу. Але і описана вище інфраструктура трохи завелика для такого проекту.
Подивимось що саме нам так дорого обходиться.
EC2-Other costs
Часте питання – “що за EC2 Other в Cost Explorer“, бо не дуже зрозуміла назва.
Фактично, сюди входять Public IP адреси, трафік, EBS volumes.
Подивитись що конкретно нам обходиться в $1.29 можна в тому ж Cost Explorer – у Filters > Service вибираємо EC2 Other, а в Group by > Dimension вибираємо Usage type або API Operation:
Власне, бачимо вартість NAT Gateway.
Деталі заходимо в документації Amazon VPC pricing, і рахуємо: $0.045 на годину, множимо на 24 і маємо 1.080 долари на добу, або 30+ доларів на місяць.
І це ще без трафіку через NAT Gateway, який рахується окремо – $0.048 за кожен гігабайт в cross-region або cross AvailabilityZone трафіку.
Власне, саме з цієї причини варто мати VPC Endpoint для S3: тип gateway безкоштовний, зато трафік буде йти не через NAT Gateway – а всередині VPC.
EBS Volumes costs
На скріншоті вище бачимо API Operation CreateVolume-Gp3 – це вартість EBS, який підключений до EC2, див. Amazon EBS pricing: 50 гігабайт диск дає нам $4.4 на місяць, або $0.147/день.
Диск для RDS рахується окремо:
EC2-Instances costs
Тут все просто – маємо один t3.medium, який коштує $0.0416 – маємо $0.99 в день.
Але до того ж і тут рахується трафік – він $0.04 до $0.09 за гігабайт outgoing в залежності від об’єму.
Вхідний трафік не оплачується.
Втім, є свої нюанси з трафіком:
ALB: трафік, який віддаємо клієнтам через Load Balancer – оплачується
NAT Gateway: тут взагалі платимо двічі:
NAT GW processing fee: це з costs самого NAT Gateway, аза кожен переданий через нього назовні гігабайт
EC2 Data Transfer Out: і додатково платимо за кожен гігабайт “у світ” з самого EC2
RDS: дані в межах одної Availability Zone не оплачуються, але якщо є cross-AZ або cross-region сетап – то платимо $0.01/GB за вхідний і вихідний трафік
платимо за LCU (Load Balancer Capacity Units) – навантаження на ALB, загальна вартість буде залежати від того, скільки ALB опрацював запитів від клієнтів (або під час DDoS :trollface: )
платимо за outgoing трафік – але тільки за трафік з ALB, бо трафік між EC2 та ALB в межах одної Availability Zone безкоштовний
Про що буде йти мова в цьому пості – як робилась автоматизація збору даних з Linux-хостів на NAS, трохи про підводні камені rsync, і як всі бекапи з самого NAS синхронізуються в Rclone remotes.
Всі описані тут скрипти і приклади конфігураційних файлів є в GitHub setevoy2/nas-backup.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
Взагалі, спочатку ідея була все робити з restic і NFS: мати на NAS окрему NFS share, яка б підключалась до хостів, потім на хостах в цю шару з restic робити бекапи, і після цього з rclone копіювати дані в Google Drive та/або AWS S3.
Але чим більше думав – тим більше розумів, що це не найкраще рішення:
по-перше – зав’язуватись на NFS, яка має бути постійно підключена – треба перевіряти, чи вона є, чи активна, ну і взагалі – це прив’язка до постійного network connection
по-друге – сам restic, як система бекапу для домашнього використання – трошки overkill:
snapshots – да, круто, але ті самі снапшоти робляться із ZFS
другий момент – це те, що restic працює виключно з crypted data у своїх репозиторіях – а я хотів мати можливість просто зайти в каталог бекапів, і подивитись що там є
Тому врешті-решт замість restic вирішив робити зі звичайним rsync, а замість restic remotes – взяти rclone, і заливати в клауди дані з ним.
Але і тут виникли свої нюанси і зміни в планах.
Спершу ідея була мати shell-скрипти з викликом rsync на Linux-хостах, ці скрипти запускати по cron, робити бекапи і заливати їх на NAS – і навіть написав такі скрипти на робочому ноутбуці.
Втім, коли вже почав збирати всю систему, то постало питання – а коли саме з NAS запускати rclone, щоб вже оновлені з rsync бекапи залити в клауд?
Власне, тоді і прийшло розуміння, що потрібен якийсь “control loop”, який буде і запускати копіювання даних з інших хостів, і на самому NAS, і після завершення копіювання даних – буде заливати апдейти в Google Drive та Backblaze, ще і виконувати якісь додаткові дії.
Тобто загальна схема тепер така: запускати rsync прямо з NAS, з цього “control loop бекап-скрипта”, з rsync по SSH підключатись до віддалених хостів, збирати дані, і в кінці, точно знаючи, що всі дані зібрані – вже можна спокійно запускати rclone.
Rsync: основна “робоча лошадка” для копіювання даних між хостами – збирає з Linux, Raspberry PI, DigitalOcean, та з самого NAS/FreeBSD
Rclone: займається синхронізацією даних в клауди
rclone робить sync в Google Drive та Backblaze з опцією --backup-dir – тому навіть якщо Syncthing щось наламає і видалить, а потім ці зміни синхронізуються в клауд – то все одно залишаться копії видалених даних.
І плюс в самому Syncthing для всіх shared директорій включена “Trash Can Versioning”.
Загальна схема виглядає так:
Як писав в попередньому пості – є кілька різних “класів даних” які зберігаються в окремих датасетах, і кожен датасет мапиться на власний rclone remote з власними налаштуваннями шифрування.
Якщо спростити схему і відобразити тільки потік даних – то це виглядає так:
Структура каталогів і файлів
Взагалі, в чорнетці був розписаний весь процес створення “утиліти”, але вирішив вже просто описати фінальне рішення (і то вийшло нічого собі тексту).
Всі операції виконуються кількома shell-скриптами, всі потрібні налаштування – описані в конфіг-файлах.
backup.sh: основний скрипт, який виконує перевірки, запускає rsync, запускає інші скрипти
validate-config.sh: перевіряє синтаксис файлів конфігурації з каталога config/
vmbackup-backup.sh: виконує бекап бази VictoriaMetrics з vmbackup
web-backup.sh: виконує бекап локального WordPress – файлів та бази даних MariaDB
Про каталог config/ – трохи далі, а каталоги excludes/ та includes/ містять файли для rsync, і для кожного хоста власний каталог з власними налаштуваннями include/exclude.
Тепер трохи про файли і організацію, а потім вже до скриптів.
rsync, include та exclude
Наприклад, файл includes/setevoy-work/user-home-Media.include описує дані, які треба скопіювати з хоста work.setevoy (робочий ноутбук) і каталогу /home/setevoy:
Сам rsync запускається з exclude=all, але про це детальніше буде далі, бо там є свої нюанси.
Каталог config та файли з налаштуваннями
Тут два файли: один для rsync – hosts.conf, другий, для rclone – rclone-remotes.conf.
Файли перевіряються валідатором – validate-config.sh, а потім парсяться основним скриптом backup.sh.
hosts.conf – параметри для rsync
Файл hosts.conf виглядає так:
root@setevoy-nas:~ # cat /opt/nas_backup/config/hosts.conf
##############
### Syntax ###
##############
# hostname|user|include_file|exclude_file|destination|delete=yes/no
# Notes:
# - include/exclude files can be in subdirectories (e.g., 'setevoy-work/user-home-Vault.include')
# - multiple lines for the same host are allowed (different sources to different destinations)
# - destination directories will be created automatically if they don't exist
# - delete field format: delete=yes or delete=no (explicit format required!)
# - delete=yes: rsync will use --delete-delay --delete-excluded (removes files on destination that don't exist on source)
# - delete=no: rsync will only copy/update files (no deletion)
# IMPORTANT! For system backups and multiple hosts with same username:
# - Always include hostname/machine identifier in the destination path
# - Example: /nas/systems/work.setevoy/thinkpad-t14-g5/ (not just /nas/systems/)
# - This prevents mixing configs from different machines
#############################
### work.setevoy - laptop ###
#############################
### HOME ###
# Syncthing:
# - Books/
# - Documents/
# - Music/
# - Photos/
# - Pictures
# - Videos
# Rsync:
# - Vault/ => /nas/vault/
# - Films/ => /nas/private/
# - Drobox/ => /nas/media
# - Ops/ => /nas/media
# - Projects/ => /nas/media
# - To-Sort => /nas/media
# - VMs => /nas/media
# - Work => /nas/media
# '/home/setevoy/ALL' => '/nas/media/home/setevoy/ALL/'
work.setevoy|setevoy|setevoy-work/user-home-Media.include|global.exclude|/nas/media/|delete=yes
...
#################################
### pi.setevoy - Raspberry PI ###
#################################
# '/opt/' => '/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/opt/'
pi.setevoy|root|setevoy-pi/opt.include|global.exclude|/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/|delete=yes
...
#############################
### nas.setevoy - FreeBSD ###
#############################
# '/opt/' => '/nas/systems/setevoy-nas/thinkcentre-10SUSCF000/opt/'
nas.setevoy|root|setevoy-nas/opt.include|global.exclude|/nas/systems/setevoy-nas/thinkcentre-10SUSCF000/|delete=yes
...
Власне, в ньому параметри для запуску rsync:
ім’я хоста, з якого буде виконуватись бекап
ім’я юзера для підключення – бо не всюди один, і до деяких взагалі треба root – коли бекапляться якісь системні файли
третім – відносний шлях до файлу include
четвертий параметр – exclude, якщо треба задати окремий
п’ятий – локальний каталог на самому NAS, в який будуть копіюватись дані (і який буде використовуватись для створення ZFS snapshots)
останній параметр – чи включати опцію rsync --delete, якщо треба в бекапах на NAS видаляти дані, які були видалені на source
rclone-remotes.conf – параметри для rclone
Синтаксис rclone-remotes.conf аналогічний:
root@setevoy-nas:~ # cat /opt/nas_backup/config/rclone-remotes.conf
# used for rclone sync only
##############
### Syntax ###
##############
# set as:
# dataset|rclone_remote
# - no leading and closing slashes on the 'dataset'
# - no closing ":" on the rclone_remote
# use commands:
# - rclone listremotes
# - rclone listremotes nas-google-drive-crypted-test
# - rclone config show nas-google-drive-crypted-test:
#############
### Media ###
#############
# Google
nas/media|nas-google-drive-media
# Backblaze
nas/media|nas-backblaze-crypted-media
...
Тут:
першим заданий ZFS dataset, з якого будуть копіюватись дані
другим – rclone remote config name, в який дані заливаються
Скрипти
Скрипти 4, поділені по функціональності:
backup.sh: основний скрипт, головний “control loop” – запускає всі інші скрипти та rsync && rclone
validate-config.sh: перевіряє синтаксис файлів конфігурації, про які писав вище
vmbackup-backup.sh: запускає vmbackup для VictoriaMetrics
web-backup.sh: створює архів файлів мого щоденнику на WordPress та mysqldump його бази
Скрипт backup.sh розглянемо останнім, аби спочатку подивитись на те, що він запускає, а вже потім – як він це запускає.
Запускається з backup.sh самим першим і виконує такий собі “preflight check”.
В глобальних змінних має два конфіг-файли, які йому треба перевірити.
Перевірки для hosts.conf та rclone-remotes.conf трохи відрізняються, бо у них вочевидь різний зміст:
для hosts.conf:
перевіряє чи в ньому вказані всі необхідні поля
виконує перевірку, що include/exclude файли, задані для хостів, реально існують
хост пінгується (якщо ні – просто видає WARNING, а не ERROR)
важливо – виконує перевірку синтаксису поля delete=yes/no, бо це найбільш “болюча” опція (хоча ще є zfs destroy :trollface: )
для rclone-remotes.conf:
перевіряє наявність ZFS dataset
перевіряє наявність rclone remote в його конфігу
Цей скрипт ніяких алертів не шле – це виконується в самому backup.sh, якщо валідатор повернув помилку.
Валідація файлу hosts.conf
Перевірка наявності всіх необхідних параметрів доволі проста – маємо файл, читаємо кожен рядок, маємо список полів.
Поля в файлі конфігурації розділені символом “|” – використовуємо його в while IFS='|'.
IFS – це вбудована змінна shell, Internal Field Separator, якій можна перевизначити символ, за яким буде розбиватись зміст строки чи файлу.
Якщо поле пусте – повертаємо помилку:
...
while IFS='|' read -r hostname user include_file exclude_file destination delete_field; do
LINE_NUM=$((LINE_NUM + 1))
# Skip comments and empty lines
case "$hostname" in
\#*|'') continue ;;
esac
echo "Validating line $LINE_NUM: $hostname"
# Check if all fields are present
if [ -z "$hostname" ] || [ -z "$user" ] || [ -z "$include_file" ] || [ -z "$exclude_file" ] || [ -z "$destination" ] || [ -z "$delete_field" ]; then
echo " ERROR: Missing field(s) in line $LINE_NUM"
ERRORS=$((ERRORS + 1))
continue
fi
...
Перевірка опції delete=yes/no розбита на дві окремі перевірки:
спершу перевіряємо, що опція задана саме як delete=, а не просто “yes” чи просто “delete“
потім перевіряємо значення після “=“, має бути або саме “yes“, або “no“
Виглядає це так:
...
# Validate delete field format
if ! echo "$delete_field" | grep -q '^delete='; then
echo " ERROR: Invalid delete field format. Expected 'delete=yes' or 'delete=no', got: $delete_field"
ERRORS=$((ERRORS + 1))
else
delete_value=$(echo "$delete_field" | cut -d'=' -f2)
if [ "$delete_value" != "yes" ] && [ "$delete_value" != "no" ]; then
echo " ERROR: Invalid delete value. Expected 'yes' or 'no', got: $delete_value"
ERRORS=$((ERRORS + 1))
fi
fi
...
Валідація файлу rclone-remotes.conf
Тут аналогічно: читаємо файл, перевіряємо, що отримали саме дві опції, які розділені символом “|“.
Потім перевіряємо ZFS dataset із zfs list "$dataset", і перевіряємо rclone remote з rclone listremotes:
...
while IFS='|' read -r dataset remote; do
...
# Check if all fields are present
if [ -z "$dataset" ] || [ -z "$remote" ]; then
echo " ERROR: Missing field(s) in rclone config line $RCLONE_LINE_NUM"
ERRORS=$((ERRORS + 1))
continue
fi
# Check if dataset exists
if ! zfs list "$dataset" > /dev/null 2>&1; then
echo " ERROR: Dataset $dataset does not exist"
ERRORS=$((ERRORS + 1))
fi
# Check if rclone remote exists
if ! rclone listremotes | grep -q "^${remote}:$"; then
echo " ERROR: Rclone remote $remote not found"
ERRORS=$((ERRORS + 1))
fi
...
В кінці скрипта рахуємо помилки і виходимо з помилкою, якщо $ERRORS більше нуля:
...
if [ $ERRORS -gt 0 ]; then
echo "=== Validation FAILED with $ERRORS error(s) ==="
exit 1
else
echo "=== Validation PASSED ==="
exit 0
fi
Скрипт vmbackup-backup.sh
“Під капотом” використовує власну утиліту VictoriaMetrcis vmbackup. Єдиний мінус утиліти – поки що не підтримує VictoriaLogs, але PR є, скоро, мабуть, додадуть.
Хоча особисто мені бекап логів і непотрібний, а от бекап бази – треба, бо в мене там дані мого “Self Monitoring Project”, де я записую дані по тому, як спав, який настрій – і ці дані записую з 2023 року, втратити їх не хочеться.
Скрипт виконує два типи бекапів – інкрементальний по буднях, і повний – в неділю, плюс видаляє старі бекапи.
На відміну від валідатора – тут вже свій обробник алертів, який шле нотифікації на ntfy.sh.
ntfy.sh – дуже класний сервіс для таких випадків, дуже простий, і, сподіваюсь, я таки запущу self hosted версію і напишу про нього окремо.
Для алертів в скрипті описана окрема функція, яка просто з curl шле POST-запит до сервісу:
VM_DATA_PATH використовується для того, щоб, власне, скопіювати дані, а через ендпоінт VM_SNAPSHOT_URL – vmbackup передає команду до VictoriaMetrics на “заморозку” операцій, аби створити консистентний snapshot.
Запуск самих бекапів і відправка алертів виглядають так:
...
vmbackup \
-storageDataPath="$VM_DATA_PATH" \
-snapshot.createURL="$VM_SNAPSHOT_URL" \
-dst="fs://$BACKUP_BASE/latest" >> "$LOGFILE" 2>&1
INCREMENTAL_EXIT=$?
if [ $INCREMENTAL_EXIT -ne 0 ]; then
echo "ERROR: Daily incremental backup failed with exit code $INCREMENTAL_EXIT" | tee -a "$LOGFILE"
send_alert "VMBackup: Incremental backup failed" "❌ VictoriaMetrics incremental backup failed on $HOSTNAME
Exit code: $INCREMENTAL_EXIT
Log: $LOGFILE"
FAILED=$((FAILED + 1))
else
echo "Daily incremental backup completed successfully" | tee -a "$LOGFILE"
fi
...
В результаті є кілька директорій – latest для інкрементальних бекапів, та <DATE> для weekly:
root@setevoy-nas:~ # tree -d -L 2 /nas/services/victoriametrics/
/nas/services/victoriametrics/
├── 20260222
│ ├── data
│ ├── indexdb
│ └── metadata
├── 20260301
│ ├── data
│ ├── indexdb
│ └── metadata
└── latest
├── data
├── indexdb
└── metadata
Видалення старих бекапів виконується з find, як і в інших скриптах.
В цьому прикладі спеціально залишаю першу, тестову версію – без реального rm -rf:
...
# Calculate cutoff date (RETENTION_WEEKS weeks ago, in YYYYMMDD format)
CUTOFF=$(date -v-${RETENTION_WEEKS}w +%Y%m%d 2>/dev/null || date -d "${RETENTION_WEEKS} weeks ago" +%Y%m%d)
find "$BACKUP_BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' | while read dir; do
DIR_DATE=$(basename "$dir")
if [ "$DIR_DATE" -lt "$CUTOFF" ]; then
echo "Deleting old weekly backup: $dir" | tee -a "$LOGFILE"
# TODO: uncomment when tested
#rm -rf "$dir"
echo "[DRY-RUN] would delete: $dir"
fi
done
...
Скрипт web-backup.sh
Тут задача – створити архів файлів та зробити дамп бази даних.
Дуже простий, бекапить тільки один сайт, але мені поки більше і не треба.
Також має власний алертинг.
Бекап файлів створюється з tar:
...
SITE_DIR="/usr/local/www/blog.setevoy"
DB_NAME="nas_blog_setevoy_production_db"
DB_CREDENTIALS="/root/.my.cnf.blog-setevoy"
FILES_DEST="$BACKUP_BASE/setevoy/files/${DATE}-blog-setevoy.tar.gz"
DB_DEST="$BACKUP_BASE/setevoy/databases/${DATE}-blog-setevoy.sql"
# Backup files
echo "Archiving files: $SITE_DIR -> $FILES_DEST" | tee -a "$LOGFILE"
tar -czf "$FILES_DEST" --exclude="$SITE_DIR/wp-content/updraft" "$SITE_DIR" >> "$LOGFILE" 2>&1
TAR_EXIT=$?
if [ $TAR_EXIT -ne 0 ]; then
echo "ERROR: Failed to archive blog.setevoy files" | tee -a "$LOGFILE"
send_alert "Web Backup: Failed" "❌ Failed to archive blog.setevoy files on $HOSTNAME
Log: $LOGFILE"
FAILED=$((FAILED + 1))
else
echo "Files archived successfully" | tee -a "$LOGFILE"
fi
...
А база MariaDB – з mysqldump:
...
DB_CREDENTIALS="/root/.my.cnf.blog-setevoy"
...
# Backup database
echo "Dumping database: mysqldump --defaults-file="$DB_CREDENTIALS" "$DB_NAME" > "$DB_DEST" 2>> "$LOGFILE""
mysqldump --defaults-file="$DB_CREDENTIALS" "$DB_NAME" > "$DB_DEST" 2>> "$LOGFILE"
DB_EXIT=$?
if [ $DB_EXIT -ne 0 ]; then
echo "ERROR: Failed to dump database $DB_NAME" | tee -a "$LOGFILE"
send_alert "Web Backup: Failed" "❌ Failed to dump database $DB_NAME on $HOSTNAME
Log: $LOGFILE"
FAILED=$((FAILED + 1))
else
echo "Database dumped successfully" | tee -a "$LOGFILE"
fi
...
Тут mysqldump без додаткових опцій, бо це чисто мій власний щоденник, де окрім мене нікого не буває.
Але взагалі варто мати на увазі такі параметри:
--single-transaction: тільки для InnoDB – виконати дамп одною транзакцією без блокування таблиць, бо це може заафектити юзерів
--routines та --triggers: бекапити процедури і тригери – це не WordPress case, але можуть бути корисним
--add-drop-table: дефолтне значення true, додає в sql-дампі DROP TABLE IF EXISTS перед кожним CREATE TABLE – спрощує відновлення в існуючу базу даних
В скрипті використовується опція --defaults-file, через яку передається шлях до файлу з юзером та паролем:
Ну і, нарешті, основний скрипт backup.sh, який, власне, і займається “оркестрацією” всього процесу.
Тут по черзі виконуються всі необхідні дії і запускаються скрипти, про які говорили вище.
Логіка виконання
створюємо lock-файл: корисно, якщо попередній запуск скрипта завис – щоб не запустити одночасно два процеси виконання
скриптом validate-config.sh виконується перевірка файлів hosts.conf та rclone-remotes.conf
по черзі запускаємо скрипти бекапів:
з web-backup.sh – бекапиться WordPress
з vmbackup-backup.sh – бекапиться VictoriaMetrics
далі читаємо конфіг hosts.conf для rsync, для кожного хоста визначаємо потрібні параметри, і в циклі для кожного хоста:
виконуємо rsync – спершу з --dry-run, потім вже реальний запуск
якщо rsync виконався без помилок – то створюємо ZFS снапшот
вже не в циклах – видаляємо старі ZFS снапшоти
читаємо конфіг rclone-remotes.conf для rclone
в циклі запускаємо rclone sync для кожного заданого в конфігу ZFS dataset та відповідного rclone remote
і в кінці з ntfy.sh відправляємо результат виконання
Step 1: створення lock file
...
LOCKFILE="/var/run/nas-backup.lock"
...
# Check if another instance is running
if [ -f "$LOCKFILE" ]; then
echo "ERROR: Another backup is already running (lock file exists: $LOCKFILE)" | tee -a "$LOGFILE"
send_alert "NAS Backup: Already running" "⚠️ Another backup instance is already running on $HOSTNAME
Lock file: $LOCKFILE"
exit 1
fi
# Create lock file
echo $$ > "$LOCKFILE"
# Remove lock file on exit
trap 'echo ""; echo "Caught interrupt, cleaning up..."; kill $(jobs -p) 2>/dev/null; rm -f $LOCKFILE; exit 130' INT TERM
trap 'rm -f $LOCKFILE' EXIT
..
Тут:
перевіряємо, що файлу зараз нема – тобто попередній запуск скрипта вже завершено
створюємо файл /var/run/nas-backup.lock, з $$ в файл записуємо PID процесу
запускаємо trap, який перехопить Ctrl+C (Interrupt) або SIGTERM і видалить lock file
Step 2: запуск валідатора validate-config.sh
Тут все просто – після створення lock file запускаємо validate-config.sh, з if перевіряємо його код виконання:
...
# Run validator first
echo "Running configuration validator..." | tee -a "$LOGFILE"
if ! /opt/nas_backup/validate-config.sh >> "$LOGFILE" 2>&1; then
echo "ERROR: Configuration validation failed" | tee -a "$LOGFILE"
send_alert "NAS Backup: Config validation failed" "❌ Config validation failed on $HOSTNAME
Script: backup.sh
Log: $LOGFILE"
exit 1
fi
echo "" | tee -a "$LOGFILE"
...
Steps 3 та 4: запуск бекапів Web та VictoriaMetrics
Аналогічно – запускаються з if:
...
echo "=== Starting web backups ===" | tee -a "$LOGFILE"
if ! /opt/nas_backup/web-backup.sh >> "$LOGFILE" 2>&1; then
echo "WARNING: web_backup.sh failed, continuing..." | tee -a "$LOGFILE"
send_alert "NAS Backup: Web backup failed" "⚠️ web_backup.sh failed on $HOSTNAME, continuing with rsync\nLog: $LOGFILE"
exit 1
fi
echo "" | tee -a "$LOGFILE"
# Step 2: VictoriaMetrics backup
echo "=== Starting VictoriaMetrics backup ===" | tee -a "$LOGFILE"
if ! /opt/nas_backup/vmbackup-backup.sh >> "$LOGFILE" 2>&1; then
echo "WARNING: vmbackup-backup.sh failed, continuing..." | tee -a "$LOGFILE"
send_alert "NAS Backup: VMBackup failed" "⚠️ vmbackup-backup.sh failed on $HOSTNAME, continuing with rsync\nLog: $LOGFILE"
fi
echo "" | tee -a "$LOGFILE"
...
Step 5: запуск циклу з hosts.conf
Сама, мабуть, важлива і цікава частина – тут починається процес збору даних з усіх хостів, які задані в hosts.conf.
Спершу в циклі читається файл конфігу, заповнюються всі “локальні” змінні:
...
while IFS='|' read -r hostname user include_file exclude_file destination delete_field; do
# Skip comments and empty lines
case "$hostname" in
\#*|'') continue ;;
esac
# Parse delete option
delete_value=$(echo "$delete_field" | cut -d'=' -f2)
...
Далі хост пінгується, і, якщо ping не пройшов – то цикл while переходить до наступного рядка з файлу конфігурації.
Реалізовано це за допомогою оператора continue, який є в тому числі в “\#*|'') continue ;;“: якщо в hosts.conf строка – це коментар, то скіпаємо її і переходимо до наступної.
Аналогічно continue використовується для ping:
...
# Check if host is reachable
if ! ping -c 3 "$hostname" > /dev/null 2>&1; then
echo "WARNING: Host $hostname is not reachable, skipping" | tee -a "$LOGFILE"
send_alert "NAS Backup: Host unreachable" "⚠️ Host $hostname is not reachable on $HOSTNAME
Skipping backup
Log: $LOGFILE"
echo "" | tee -a "$LOGFILE"
continue
fi
...
Тут – якщо ping повернув не success – то шлемо алерт і через continue переходимо до наступного хоста.
Аналогічно в наступній дії – перевіряється локальна директорія, в яку будуть копіюватись дані, якщо її нема – вона створюється, якщо створення завершилось з помилкою – то переходимо до наступного хоста:
...
# Create destination directory if it doesn't exist
if [ ! -d "$destination" ]; then
echo "Creating destination directory: $destination" | tee -a "$LOGFILE"
mkdir -p "$destination" >> "$LOGFILE" 2>&1
if [ $? -ne 0 ]; then
echo "ERROR: Failed to create destination directory" | tee -a "$LOGFILE"
send_alert "NAS Backup: Failed to create destination" "❌ Failed to create destination directory on $HOSTNAME
Host: $hostname
Destination: $destination
Log: $LOGFILE"
echo "" | tee -a "$LOGFILE"
continue
fi
fi
...
Step 6: запуск rsync
Після цього, власне, починається процес копіювання даних з кожного хоста – і тут є кілька підводних каменів.
rsync та опції –delete
Дуже важлива – бо небезпечна – опція: чи видаляти на NAS дані, які були видалені на source.
В самому backup.sh перевіряється її значення і, якщо delete=yes – то в змінну $RSYNC_DELETE_OPTS задається значення --delete-delay:
...
# Build rsync options based on delete setting
# default is empty, i.e. no delete
# IMPORTANT: DON NOT SET '--delete-excluded' if using multiply .includes: `rsync` is running with the `--exclude='*'` and will wipe all other data
RSYNC_DELETE_OPTS=""
if [ "$delete_value" = "yes" ]; then
RSYNC_DELETE_OPTS="--delete-delay"
fi
...
Тут в коментарі до перевірки записав, і ще раз підкреслю окремо – бо я з цим трохи мав проблему:
rsync запускається опцією --exclude='*' (про це трохи далі)
якщо в $RSYNC_DELETE_OPTS вказати --delete-excluded – то, відповідно, rsync на NAS почне видаляти всі дані, які явно не задані в include-файлі
А так як файли include можуть бути різними для різних даних на source, але при цьому на destination – тобто самому NAS, каталог може бути єдиним – то rsync при кожній ітерації видалить дані іншої строки з конфігу.
скопіює в /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/
перейде до наступної строки, візьме все з system.include
почне копіювати в /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/ – і видалить звідти те, що скопіював при запуску з opt.include
rsync та –exclude=’*’
Чому rsync запускається з --exclude='*'?
Бо, по-перше, особисто я віддаю перевагу підходу “заборонити все і копіювати тільки те, що дозволено явно“.
По-друге – простіший конфіг hosts.conf та сам скрипт backup.sh – достатньо передати тільки ім’я хоста, а далі rsync рекурсивно від / файлової системи проходиться по каталогам які явно дозволені в include-файлі, і копіює дані тільки з них.
Без exclude='*' довелось би або додавати виключення в файл global.exclude, або в include через “-“.
Виконання та опції rsync
Власне запуск самого rsync виглядає так – спочатку --dry-run, потім “running real backup” – те саме, тільки без --dry-run:
...
RSYNC_DELETE_OPTS=""
if [ "$delete_value" = "yes" ]; then
RSYNC_DELETE_OPTS="--delete-delay"
fi
echo "Rsync command: rsync -avh $RSYNC_DELETE_OPTS --prune-empty-dirs --itemize-changes --progress --exclude-from=$EXCLUDES_DIR/$exclude_file --include-from=$INCLUDES_DIR/$include_file --exclude='*' $user@$hostname:/ $destination" | tee -a "$LOGFILE"
echo "" | tee -a "$LOGFILE"
# Run rsync with dry-run first
echo "Running dry-run..." | tee -a "$LOGFILE"
rsync -avh \
--dry-run \
$RSYNC_DELETE_OPTS \
--prune-empty-dirs \
--itemize-changes \
--progress \
--exclude-from="$EXCLUDES_DIR/$exclude_file" \
--include-from="$INCLUDES_DIR/$include_file" \
--exclude='*' \
"$user@$hostname:/" "$destination" >> "$LOGFILE" 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "=== Dry-run FAILED with exit code $EXIT_CODE, skipping real backup ===" | tee -a "$LOGFILE"
send_alert "NAS Backup: Dry-run failed" "❌ Rsync dry-run failed on $HOSTNAME
Host: $hostname
Exit code: $EXIT_CODE
Log: $LOGFILE"
BACKUP_FAILED=$((BACKUP_FAILED + 1))
echo "" | tee -a "$LOGFILE"
continue
fi
echo "Dry-run successful, running real backup..." | tee -a "$LOGFILE"
# Run REAL rsync
rsync -avh \
$RSYNC_DELETE_OPTS \
--prune-empty-dirs \
--itemize-changes \
--progress \
--exclude-from="$EXCLUDES_DIR/$exclude_file" \
--include-from="$INCLUDES_DIR/$include_file" \
--exclude='*' \
"$user@$hostname:/" "$destination" >> "$LOGFILE" 2>&1
EXIT_CODE=$?
...
З корисних опцій тут:
-a (archive): зберігає права, власника, сімлінки, timestamps
-v (verbose): виводить в лог інформацію що саме виконується
-h (human): відображати розмір як 1G замість байт
--delete-delay: видаляти дані по завершенню передачі даних, а не в процесі
--prune-empty-dirs: якщо на source каталог пустий – не копіювати його
--itemize-changes: детальна інформація в лог що саме змінилось в файлі, які перезаписуються/видаляються
--progress: показує прогрес передачі кожного файлу
Порядок передачі опцій –include-from та –exclude-from
І окремо про exclude та include.
Має значення в якому порядку параметри передаються до rsync:
першим йде --exclude-from – аби rsync перед запуском копіювання вже “знав” що треба пропускати
далі через --include-from передаємо список каталогів та файлів, які дозволено прочитати та скопіювати
і останнім з --exclude='*' виключаємо бекапу все, що явно не задано в --include-from
Тут через “**” вказуємо “без різниці, де саме цей файл чи каталог буде знайдено“, тобто виключаємо і /root/some-dir/.git/ – і /home/setevoy/some-dir/.git/.
Приклад одного з include-файлів – тут трохи цікавіше:
Так як rsync запускається з --exclude='*' – то в include йому треба явно дозволити “зайти” в корневий каталог.
Тобто, при виконані rsync -avh [email protected]:/ – rsync зайде в корінь, “/“, потім – маючи /home/ в include-from – зможе “заглянути” в /home/, а далі вже завітати до /home/setevoy/.
І далі аналогічно дозволяємо доступ в /home/setevoy/Books/, де з “**” вказуємо “взяти тут все, що знайдеш” – окрім того, що було задано в exclude-file.
При цьому дані з, наприклад, каталогу /home/setevoy/Bob/ – rsync пропустить, бо не має явного дозволу їх читати і копіювати.
Step 7: створення ZFS snapshots
Після того як rsync для хоста завершився без помилок – запускається наступний if/else:
...
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo ""
echo "=== Backup from $hostname completed successfully ===" | tee -a "$LOGFILE"
BACKUP_SUCCESS=$((BACKUP_SUCCESS + 1))
# Create ZFS snapshot
SNAPSHOT_NAME="nas-backup-$(date +%Y-%m-%d-%H-%M-%S)"
# Get dataset name from destination path
DATASET=$(zfs list -H -o name "$destination" 2>/dev/null | head -1)
if [ -z "$DATASET" ]; then
echo "ERROR: Could not determine ZFS dataset for $destination" | tee -a "$LOGFILE"
send_alert "NAS Backup: Snapshot failed" "❌ Could not determine ZFS dataset on $HOSTNAME
...
else
echo ""
echo "Creating ZFS snapshot: $DATASET@$SNAPSHOT_NAME" | tee -a "$LOGFILE"
zfs snapshot "$DATASET@$SNAPSHOT_NAME" >> "$LOGFILE" 2>&1
if [ $? -eq 0 ]; then
echo "ZFS snapshot created successfully" | tee -a "$LOGFILE"
else
echo "ERROR: Failed to create ZFS snapshot" | tee -a "$LOGFILE"
send_alert "NAS Backup: Snapshot failed" "❌ Failed to create ZFS snapshot on $HOSTNAME
...
fi
fi
..
В BACKUP_SUCCESS=$((BACKUP_SUCCESS + 1)) просто інкрементиться значення, яке використовується виключно для фінального повідомлення через ntfy.sh.
Далі формуємо ім’я снапшоту, і в змінну $DATASET записуємо ім’я датасету.
Для цього беремо параметр $destination, який в hosts.conf заданий як повний шлях – /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/, а потім із zfs list отримуємо mountpoint:
root@setevoy-nas:/opt/nas_backup # zfs list -H -o name /nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/
nas/systems
А потім викликаємо zfs snapshot для, власне, створення снапшоту.
Step 8: видалення старих ZFS snapshots
Тут теж потенційно небезпечна операція, бо викликається zfs destroy – яка може дропнути повний ZFS dataset:
...
CUTOFF_DATE=$(date -v-${SNAPSHOT_RETENTION_DAYS}d +%Y-%m-%d 2>/dev/null || date -d "${SNAPSHOT_RETENTION_DAYS} days ago" +%Y-%m-%d)
zfs list -H -t snapshot -o name | grep '@nas-backup-' | while read snapshot; do
SNAP_DATE=$(echo "$snapshot" | sed 's/.*@nas-backup-\([0-9-]*\)-.*/\1/')
if [ "$SNAP_DATE" \< "$CUTOFF_DATE" ]; then
echo "Deleting old snapshot: $snapshot" | tee -a "$LOGFILE"
zfs destroy "$snapshot" >> "$LOGFILE" 2>&1
fi
done
...
Як працює:
в змінну $CUTOFF_DATE вносимо дату “сьогодні мінус 30 днів” – бо $SNAPSHOT_RETENTION_DAYS заданий в 30.
із zfs list -H -t snapshot -o name відображаємо список всіх наявних снапшотів і вибираємо тільки ті, які робились цим скриптом – grep '@nas-backup-'
потім в циклі для кожного снапшоту із zfs list -t snapshot отримуємо дату, коли цей снапшот був створений, записуємо в змінну $SNAP_DATE
порівнюємо $SNAP_DATE та $CUTOFF_DATE
і, якщо $SNAP_DATE старша за $CUTOFF_DATE – то виконуємо zfs destroy
Step 9: запуск rclone
Тут в цілому підхід аналогічний – читаємо кожну строку з конфігу:
...
while IFS='|' read -r dataset remote; do
...
В $dataset записуємо ім’я ZFS dataset, в $remote – ім’я rclone remote.
Ще раз приклад конфігу:
# used for rclone sync only
##############
### Syntax ###
##############
# set as:
# dataset|rclone_remote
# - no leading and closing slashes on the 'dataset'
# - no closing ":" on the rclone_remote
# use commands:
# - rclone listremotes
# - rclone listremotes nas-google-drive-crypted-test
# - rclone config show nas-google-drive-crypted-test:
#############
### Media ###
#############
# Google
nas/media|nas-google-drive-media
# Backblaze
nas/media|nas-backblaze-crypted-media
...
Тобто – беремо dataset nas/media – і копіюємо його зміст до nas-google-drive-media, а потім його ж – але до nas-backblaze-crypted-media.
Приклад rclone remote для Backblaze:
root@setevoy-nas:/opt/nas_backup # rclone config show nas-backblaze-crypted-media
[nas-backblaze-crypted-media]
type = crypt
remote = nas-backblaze-root-media:setevoy-nas-media
filename_encryption = off
directory_name_encryption = false
password = *** ENCRYPTED ***
Весь цикл виглядає так:
...
RCLONE_CONF="/opt/nas_backup/config/rclone-remotes.conf"
if [ ! -f "$RCLONE_CONF" ]; then
echo "WARNING: rclone config not found at $RCLONE_CONF, skipping cloud sync" | tee -a "$LOGFILE"
else
TS=$(date +%F-%H-%M)
while IFS='|' read -r dataset remote; do
# Skip comments and empty lines
case "$dataset" in
\#*|'') continue ;;
esac
echo "Syncing dataset $dataset to $remote" | tee -a "$LOGFILE"
# Get mount point for dataset
MOUNT_POINT=$(zfs get -H -o value mountpoint "$dataset" 2>/dev/null)
if [ -z "$MOUNT_POINT" ] || [ "$MOUNT_POINT" = "-" ]; then
echo "ERROR: Could not get mount point for dataset $dataset" | tee -a "$LOGFILE"
RCLONE_FAILED=$((RCLONE_FAILED + 1))
continue
fi
...
rclone sync "$MOUNT_POINT/" "${remote}:data" \
--backup-dir "${remote}:_archive/$TS" \
--progress \
--stats=30s \
--log-level INFO >> "$LOGFILE" 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "Rclone sync for $dataset completed successfully" | tee -a "$LOGFILE"
RCLONE_SUCCESS=$((RCLONE_SUCCESS + 1))
else
echo "ERROR: Rclone sync for $dataset failed with exit code $EXIT_CODE" | tee -a "$LOGFILE"
send_alert "NAS Backup: Rclone sync failed" "❌ Rclone sync failed on $HOSTNAME
...
RCLONE_FAILED=$((RCLONE_FAILED + 1))
fi
echo "" | tee -a "$LOGFILE"
done < "$RCLONE_CONF"
...
Сам rclone sync виконує саме синхронізацію: якщо на NAS файл або каталог був видалений – то він видалиться і на rclone remote.
Тому, для більш спокійного сну, rclone запускається з --backup-dir, куди копіює дані, які під час виконання sync видаляються або змінюються.
Як це виглядає на remote:
root@setevoy-nas:/home/setevoy # rclone tree --dirs-only --level 4 nas-backblaze-crypted-media:
/
├── _archive
...
│ ├── 2026-03-05-03-07
│ │ └── home
│ │ └── setevoy
│ └── 2026-03-07-03-07
│ └── home
│ └── setevoy
└── data
└── home
└── setevoy
├── Backups
├── Books
...
├── Videos
└── Work
Ну і, власне, на цьому все. Останнім виконується відправка повідомлення про те, як пройшов бекап:
...
# Send summary
if [ $BACKUP_FAILED -eq 0 ] && [ $RCLONE_FAILED -eq 0 ]; then
send_alert "NAS Backup: Completed successfully" "✅ All backups completed successfully on $HOSTNAME
Rsync successful: $BACKUP_SUCCESS
Rsync failed: $BACKUP_FAILED
Rclone successful: $RCLONE_SUCCESS
Rclone failed: $RCLONE_FAILED
Log: $LOGFILE" "white_check_mark,backup"
else
send_alert "NAS Backup: Completed with errors" "⚠️ Backups completed with errors on $HOSTNAME
Rsync successful: $BACKUP_SUCCESS
Rsync failed: $BACKUP_FAILED
Rclone successful: $RCLONE_SUCCESS
Rclone failed: $RCLONE_FAILED
Log: $LOGFILE"
fi
Як це все щастя виглядає в лог-файлі та повідомлення ntfy.sh.
Початок – робота валідатора:
Завершення – виконання rclone sync:
Повідомлення в ntfy.sh:
І на телефоні:
Що можна покращити
Скрипт(и), звісно, не ідеальні, і можна було б зробити ще:
запуск rsync та rclone для загальної картини можна винести окремим скриптами, як це зроблено для validate-config.sh та vmbackup-backup.sh
зараз весь цикл виконання виконується без можливості вказати “зроби мені тільки web” або “зроби мені тільки rclone” – можна було б додати getopt чи getopts, парсити аргументи, з якими запускається скрипт та вибирати, що саме виконувати
додати в аргументи можливість окремого запуску rsync чисто з --dry-run
для rclone зараз не використовується --ignore-from – можна було б додати
ну і “вішенка на торті” – писати метрики в VictoriaMetrics про те, скільки байт передано, скільки місця на диску було витрачено або звільнено – щось таке
Все.
Поки працює, як є – вже кілька тижнів, поки що без проблем.
Моніторинг в цілому вже налаштований в попередніх частинах, але залишилось налаштувати роботу з логами – бо робити це в консолі з tail -f /var/log/messages, звісно, можна – але є і більш зручні інструменти.
Використаємо VictoriaLogs – тим більш для метрик на моїй FreeBSD вже є стек VictoriaMetrics + VMAlert + Alertmanager.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
Пишемо свій файл /usr/local/etc/fluent-bit/fluent-bit.conf, поки додаємо збір тільки /var/log/messages:
[SERVICE]
flush 5
daemon Off
log_level info
parsers_file parsers.conf
plugins_file plugins.conf
[INPUT]
name tail
path /var/log/messages
tag freebsd.messages
db /var/db/fluent-bit/messages.db
[OUTPUT]
name loki
match *
host localhost
port 9428
uri /insert/loki/api/v1/push?_msg_field=log&_time_field=date
labels job=fluent-bit, host=setevoy-nas, logfile=messages
В полі uri вказуємо адресу VictoriaLogs, задаємо поле для _msg, в labels вказуємо набір тегів, які будуть додаватись до логів.
root@setevoy-nas:~ # service fluent-bit start
Starting fluent_bit.
VictoriaLogs та робота з логами з консолі
VictoriaLogs прям дуже зручна в плані роботи з даними з консолі, і, думаю, юзери FreeBSD це оцінять.
Для роботи у нас два варіанти – або робити запити з curl, а потім їх парсити – або використати vlogscli.
Запити з curl
Приклад з curl:
root@setevoy-nas:~ # curl -s 'http://localhost:9428/select/logsql/query?query=*'
{"_time":"2026-02-28T14:42:00.20468201Z","_stream_id":"0000000000000000782dd9afdaaf4d53bfb843de46a3d91b","_stream":"{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}","_msg":"Feb 28 16:42:00 setevoy-nas setevoy[36200]: test message from fluent-bit 2","host":"setevoy-nas","job":"fluent-bit","logfile":"messages"}
Результат отримуємо в JSON, тому можна передати в jq:
І робити всякі пайпи:
root@setevoy-nas:~ # curl -s http://localhost:9428/select/logsql/query -d 'query=test' | jq -r '._time + " " + ._msg'
2026-02-28T14:42:00.20468201Z Feb 28 16:42:00 setevoy-nas setevoy[36200]: test message from fluent-bit 2
2026-02-28T14:53:49.481172045Z Feb 28 16:48:41 setevoy-nas setevoy[36663]: test message from fluent-bit 3
2026-02-28T14:54:05.981200313Z Feb 28 16:54:05 setevoy-nas setevoy[37055]: test message from fluent-bit 3
2026-02-28T15:06:21.481220267Z Feb 28 17:06:21 setevoy-nas setevoy[37991]: test message from fluent-bit
2026-02-28T15:12:46.231202127Z Feb 28 17:12:46 setevoy-nas setevoy[38385]: test message from fluent-bit
2026-02-28T15:14:42.731213928Z Feb 28 17:14:42 setevoy-nas setevoy[38502]: test message for vlogscli
2026-02-28T15:15:48.981198786Z Feb 28 17:15:48 setevoy-nas setevoy[38569]: test message for vlogscli
2026-02-28T15:17:59.731198268Z Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli
Запити з vlogscli
Запускаємо vlogscli:
root@setevoy-nas:~ # vlogscli
sending queries to -datasource.url=http://localhost:9428/select/logsql/query
type ? and press enter to see available commands
;>
Для vmalert можна створити Recodring Rules – читати логи, генерувати метрики, а потім з цих метрик або можемо малювати графіки в Grafana – або створювати алерти.
Але для цього vmalert треба робити запити до двох datasources:
до VictoriaLogs на порт 9428 і URI /select/logsql/ – аби прочитати логи
до VictoriaMetrics на порт 8428 – аби записати метрики і виконати запити для створення алерту
Але два --datasource.url для vmalert задати не можна – але можна зробити базовий роутинг через vmauth, як я робив на робочому проекті, де в мене це все працює в Kubernetes – а потім для vmalert в --datasource.url вказати адресу vmauth.
Коли тільки починав робити свій NAS і думав про бекапи, то все здавалось доволі простим: є робочий ноутбук з даними, є сервер з FreeBSD під NAS – треба просто взяти, і скопіювати дані.
Тому перша задумка була мати backup-скрипт/и на Linux-хостах, які б з rsync заливали дані на NAS, а потім з NAS іншим скриптом заливати дані до Rclone remotes.
Але…
Але коли почав вже робити, то виникло питання:
rsync з хоста setevoy-work заливає дані на NAS
в цей жеж час rsync з хоста setevoy-home заливає свої дані
А коли запускати rclone => Google Drive? Як знати, що всі дані з хостів вже готові для копіювання?
І це була тільки верхівка айсбергу задачі по організації процесу.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
відносно статичні та однакові дані для ноутбуків/ПК – фото, відео, документи
часто змінюються, але в цілому однакові на ноутбуках/ПК, хоча можуть дещо відрізнятись – робочі дані по проектам, якісь власні скрипти
конфіденційні дані, але однакові на всіх хостах – SSH-ключі, бекапи KeePass/1Password, Recovery Codes тощо
колекція home video 😉
системні бекапи – різні конфіги з /etc, /usr/local/etc, ~/.ssh/config, всякі dotfiles налаштувань OpenBox або KDE Plasma
всі ці дані треба синхронізувати на декілька зовнішніх ресурсів (далі – просто “клауди”):
ZFS datasets на самому NAS
зберігати копії в клаудах – Google та/або Proton Drive, AWS S3 чи Backblaze
і до всього цього – мати резервні копії резервних копій на випадок випадкового видалення, для чого є:
ZFS snapshots – резервні копії на самому NAS
Syncthing Trash – резервні копії Syncthing
rclone --backup-dir – при копіюванні змін з NAS в клауди використовується окремий каталог для зберігання видалених або змінених даних
і все діло треба ще і автоматизувати, аби копіювання з NAS до Google Drive не виконувалось одночасно з копіюванням з хостів на NAS
тобто, просто запустити rsync з робочого ноута можна – але як знати, коли запускати rclone з NAS в Google Drive?
Відчуваєте, як в голові починається каша? 😉
Далі в цьому пості я всі remotes буду писати просто як “Google Drive” або “клауди” – але сюди ж входять і Proton Drive, і AWS S3, і, можливо, якісь інші, які почну використовувати пізніше – бо Rclone дає просто безліч варіантів (UPD: замість AWS S3 взяв собі Backblaze, див. Backblaze: знайомство з B2 Cloud Storage – перші враження).
Просто поки що Google Drive основний, хоча, можливо, я буду взагалі зав’язувати роботу з Google, бо їхній “AI”, який вони пхають в кожну дірку вже починає зайо*вати.
Схема моїх хостів та мереж
Для загальної картини схематично всі хости та мережі можна відобразити так:
В офісі знаходиться MikroTik RB4011, який грає таку собі роль “VPN-хаба” – на ньому налаштований WireGuard, який об’єднує офісну та домашню мережі і до якого підключений сервер в Digital Ocean, на якому зараз працює rtfm.co.ua.
Отже, питання мережі вирішено – тепер треба з усіх цих хостів почати збирати дані і якось продумати організацію їхнього збереження на NAS та процес бекапу з хостів на NAS – і з NAS в клауди.
Типи та класи даних для зберігання і бекапів
Коли я почав планувати організацію збереження даних на NAS, то в мене сформувався такий підхід:
всі дані ділимо на класи
для кожного класу маємо окремий ZFS dataset – маємо можливість налаштовувати власні квоти, окремі політики снапшотів та їхніх retention
кожен датасет має власний remote backend в Rclone – маємо можливість налаштувати власні параметри шифрування
Головний “source of truth” даних – це робочий ноутбук: тут 1 терабайт диск, тому на ньому зберігаються всі дані, які потім вже бекапляться на NAS з rsync, а частина даних синхронізується з домашнім ноутбуком та NAS через Syncthing.
Всі свої дані виділив в такі класи:
User Data: дані з /home/setevoy на ноутбуках на ПК:
Shared Static Unencrypted Data:
ці дані однакові на всіх хостах
сюди входять всякі ~/Books, ~/Music, ~/Photos
в Google зберігаються просто plaintext, аби можна було через Web щось подивитись або скачати без Rclone
дані на всіх хоста – однакові або з невеликими відмінностями
часто оновлюються та/або можуть мати багато “мусору” типу каталогів .git
в Google Drive зберігаються просто plaintext
сюди входять всякі ~/Work, ~/Projects (власні скрипти), ~/Opt (MikroTik WinBox, якісь локальні Prometheus Exporters, etc)
для таких даних source of truth – робочий ноут, а на NAS – mirror даних з робочої машини з rsync під час виконання бекапів
на домашній комп, при потребі, просто синхронізувати вручну з NAS
Shared Static Crypted Data:
дані, однакові на всіх хостах
але конфіденційні, тому в Google Drive будуть шифруватись
сюди входять ~/Vault (Recovery Codes, KeePass/1Pass, etc)
синхронізуються/бекапляться на NAS з rsync з робочого ноута
Non-Shared Static Crypted Data:
колекція приватних відео – зберігається тільки на робочому ( 🙂 ) ноуті та на самому NAS
в Google Drive будуть шифруватись і імена каталогів, і імена файлів, і самі дані
сюди входить ~/Films/Private/
синхронізуються/бекапляться на NAS з rsync з робочого ноута
System Data: різні dotfiles, конфіги з /etc, бекапи сервісів та блогів:
System Backups:
/boot, /etc, /usr/local/etc, ~/.ssh/config, /root, etc – з усіх хостів на NAS
різні конфіги і скрипти з самого FreeBSD
Services Backups:
тут в основному локальні дані самого NAS, на якому в мене WordPress з моїм приватним щоденником, VictoriaMetrics з метриками всіх хостів і моїм Self-Monitoring (див. InfluxDB: запуск на Debian з NGINX і підключення Grafana, але дані з Influx мігрував до VictoriaMetrics)
/usr/local/www – файли блогів
mysqldump – бекапи баз даних
vmbackup – дані VictoriaMetrics
бекапи блогу rtfm.co.ua – але в нього власна (дуже стара) система бекапів, яку поки не міняю – вона заливає дані в AWS S3, а вже звідти архіви нової системою зберігаються на NAS
Data Layers: діаграми збереження та передачі даних
Для кращого уявлення про те, як дані зберігаються та передаються – сформував такий собі концепт “Data Layers“:
Storage Layer: ZFS pool, datasets – тут визначаємо що і як зберігається
Snaphost Policy Layer: можна ввести додатковий рівень – тут визначаємо локальні ZFS snaphotting policy
Transport Layer: Rclone, Rsync – тут визначаємо що, чим, як і куди копіюється
Cloud Policy Layer: тут визначаємо які у нас є Rclone Remotes та encryption policy директорій і даних в них
Схема Transport Layer: передача та синхронізація даних
Тепер можна продумати та відобразити весь flow даних.
Про саму автоматизацію бекапів буду писати окремо, інакше пост буде кілометровий – там кілька shell-скриптів, які запускаються на NAS.
Але поки що можна схематично відобразити всі дані та процес збереження і передачі такою от діаграмою:
Syncthing: контролює дані, які змінюються не надто часто, мають мало “мусору”, та загальні для всіх хостів – постійно запущений процес на ноутбуках та телефоні
Rsync: для решти даних, має описані правила з include та exclude файлах – запускається з NAS, підключається до хостів, збирає з них дані
Rclone: займається копіювання даних в remotes, виконує шифрування – запускається з NAS після того, як rsync зібрав всі дані та сам backup-скрипт створив локальні (на NAS) бекапи WordPress та VictoriaMetrics
Схема Cloud Policy Layer: шифрування даних в Google Drive та Backblaze
Тут – для кращого розуміння самим собою – зібрав такий собі “mapping” даних із ZFS-датасетів на NAS до Rclone Remotes:
В Google Drive є окремий каталог Backups/Rclone, в якому створені окремі каталоги для кожного типу зберігання, і для кожного з них на NAS для Rclone налаштовані власні remotes:
/nas/services та /nas/systems: шифруємо самі дані, але імена каталогів та файлів plaintext, аби простіше було шукати
/nas/media: тут просто все відкритим текстом, бо нічого sensetive не маємо
/nas/vault та /nas/private: максимальний рівень конфіденційності – шифруються і імена каталогів та файлів, і їхній зміст
А для Backblaze – просто окремі корзини. Про Rclone remotes буде трохи далі.
NAS ZFS datasets: організація збереження даних
Далі для кожного класу даних визначив датасети:
Shared Static Unencrypted Data (~/Pictures, ~/Photos):
ZFS dataset: nas/media
Shared Dynamic Unencrypted Data (~/Work, ~/Projects, ~/Opt):
ZFS dataset: nas/media
Non-Shared Static Crypted Data (~/Films/Private/):
ZFS dataset: nas/private
Shared Static Crypted Data (~/Vault):
ZFS dataset: nas/vault
System Backups (/boot, /etc, /usr/local/etc):
ZFS dataset: nas/systems
Services Backups (WordPress databases та файли, VictoriaMetrics):
Тут є кілька додаткових датасетів, які не пов’язані бекапами:
/nas/backups-manual: просто окремі ручні бекапи з хостів або з самого NAS, коли роблю якісь потенційно небезпечні операції з даними
/nas/jellyfin: фільми для Jellyfin (про нього є чорнетка, може якось допишу) – це чисто фільми-серіали, тому в бекапи не включено і зберігається окремо
/nas/mobile: датасет для даних з мобільного телефона, які копіюються сюди з Syncthing на телефоні
/nas/to-sort: копії даних зі старих компів, які треба перебрати та включити в загальні бекапи
Приклад організації даних системних бекапів – все по окремим каталогам:
nas-google-drive-media та nas-backblaze-crypted-media: сюди заливаються дані з /nas/media
nas-google-drive-crypted-systems та nas-backblaze-crypted-systems: сюди – з /nas/systems
І так далі.
Кожен Rclone Remote має власну директорію в Google Drive або Backblaze бакет.
В Google Drive це виглядає так:
root@setevoy-nas:~ # rclone tree -d --max-depth 2 nas-google-drive-total-root:Backups/Rclone
/
└── nas
├── media
├── mobile
├── private
├── services
├── systems
└── vault
І приклад одного з бакетів в Backblaze:
root@setevoy-nas:~ # rclone tree -d --max-depth 4 nas-backblaze-crypted-media:
/
├── _archive
│ ├── 2026-02-22-14-53
│ ├── 2026-02-24-03-06
│ │ └── home
│ │ └── setevoy
│ ├── 2026-02-25-03-07
│ │ └── home
│ │ └── setevoy
│ └── 2026-02-27-12-18
│ └── home
│ └── setevoy
└── data
└── home
└── setevoy
├── Backups
├── Books
├── Documents
├── Downloads
├── Dropbox
├── Music
├── Opt
├── Photos
├── Pictures
├── Projects
├── VMs
├── Videos
└── Work
Ну, наче все описав.
Наступна частина – про сам shell-скрипт, який запускає rsync, створює бекапи WordPress та VictoriaMetrics, потім створює ZFS snapshots, а потім всі дані синхронізує в Rclone remotes.
І потім вже остання частина всієї цієї серії постів по Home NAS – з повним описом того, як це все будувалось, які сервіси, як все це моніториться, та як виглядає:
Думаю, всі юзери Opsgenie в курсі, що Atlassian вбиваєзакриває проект.
Я користувався Opsgenie з 2018 року, до нього звик, і він, в цілому, мав все те, що мені треба було від системи алертинга – хоч місцями і кривувато, але потрібні інтеграції працювали та налаштовувались достатньо легко.
Коли почав шукати альтернативи – натрапив на пост на Reddit – Anyone using Opsgenie? What’s your replacement plan, де дуже багато писали про incident.io – але це саме той випадок, коли мільйони мух таки можуть помилятись, бо більш ущєрбної системи я не бачив.
До речі, зрозумів одну річ: якщо знайомство з системою приводить тебе до думки піти на Youtube, аби глянути як люди цю систему налаштовують – то у цієї у системи явні проблеми або з UI/UX, або з документацією – і це 100% випадок incident.io, при чому по обом пунктам.
Прої… Провозившись з нею кілька днів в спробах таки змусити її відправляти текст в Slack так, як я того хочу – знов почав шукати альтернативи, і в тому ж треді на Reddit натрапив на ilert, і… Боже – це любов з першого погляду.
Все завелось просто за 15 хвилин і без всякого додаткового геморою з налаштуванням того, як повідомлення будуть виглядати в Slack.
Якісь косяки/незручності 100% зустрінуться, але поки що система виглядає саме так, як це має бути – без зайвих свістопєрдєлок, з простим, зручним і інтуїтивно зрозумілим (інтуїтивно зрозумілим, блт – чуєш, incident.io?!) інтерфейсом.
Отже сьогодні подивимось на основні можливості і налаштуємо ilert на відправку алертів.
Власне – що особисто мені треба від системи алертів? Ну… алерти. Відправка алертів. Зручний UI для перегляду алертів, і можливість налаштування шаблонів для повідомлень в Slack, бо це у нас основний канал доставки.
З інтеграцій треба вміти приймати алерти від стандартного Alertmanager та від AWS SNS.
Ну і наявність адекватної (адекватної, incident.io!) документації.
Суть його в тому, що створюється кілька Escalation policices, яким задається Routing key – просто якесь string значення.
Далі в Alert source вмикається Dynamic routing та вказується поле алерта, з якого отримується значення – і, використовуючи це значення, до нового алерта автоматично підключається Escalation policy.
А в Alert Action використовується значення не (alert.labels.component in ["devops"]), як робили вище – а з roting_id, через який підключається відповідна політика.
Цей підхід краще в плані того, що відразу налаштовується не тільки куди слати алерт – а і як його escalate.
Пробуємо.
Переходимо в On-Call > Escalation policies, створюємо нову політику:
Задаємо Routing key:
Аналогічно для Backend:
Тепер переходимо в Alert sources > Alert actions і задаємо фільтр по alert.escalationPolicy.id:
І для бекенду:
Дал редагуємо сам Alert source, включаємо Dynamic routing вказуємо label з алерта, яку треба читати – в моєму прикладі це {{ alerts[0].labels.routing }}:
по цьому значенню динамічно підключить Escalation policy
передать алерт в Alert Actions
а кожен екшен з Alert Actions перевірить свій фільтр alert.escalationPolicy.id – і спрацює той Action і його Connector, який “підключений” (чи “замаплений”) саме до slack-alerts-backend-prod або slack-alerts-devops-ops
В алертах додаємо нову лейблу – routing: alerts-devops-ops та routing: alerts-backend-prod:
Тут, в принципі, все доволі просто – задаємо поля з alert payload, можемо використовувати різні Functions – наприклад, [{{ commonLabels.severity.upperCase() }}].
В полях маленька кнопочка “play” справа внизу дозволяє відразу перевірити, як шаблон буде працювати:
В мене вже налаштована автоматизація бекапів (про неї теж будуть пости), і зараз дані з NAS раз на добу заливаються до Google Drive.
Але, по-перше – з сервісів Google я потроху позбавляюся, по-друге – хочеться мати друге місце для бекапів в клауді, а не тримати всі яйки в одній корзині.
Взагалі планував другим сторейджем взяти AWS S3, але потім подивився на альтернативи та відкрив для себе Backblaze, який мені настільки сподобався, що я прямо в той жеж день оформив підписку і налаштував Rclone на копіювання Backblaze теж.
У Backblaze є два основні продукти – Computer Backup та B2 Cloud Storage.
І якщо до Computer Backup у багатьох є питання (див., наприклад Reddit) – то Cloud Storage прям дуже класна штука.
Головна перевага Backblaze – ціна за зберігання і простота UI (при цьому з усіма необхідними можливостями), а до мінусів можна віднести хіба що доволі невеликий вибір регіонів – але USA та Europe є.
Ну і ще з мінусів, мабуть, відсутність можливості перегляду файлів – але це і не Google Drive, а просто сторейдж, тому ОК.
За вартість детальніше поговоримо трохи нижче, але головне – ціна зберігання: у Backblaze це 6 USD за терабайт, тоді як в AWS S3 Standart Storage це було б 23.55 бакси, плюс ще і вартість за скачування даних з корзин.
Коротко про можливості та плюшки Backblaze:
дуже простий в використанні – просто той базовий набір утиліт, які потрібні Cloud Storage – без зайвого ускладнення
є можливість налаштувати реплікацію бакетів між регіонами
для аутентифікації є Application keys, яким можна здавати окремі scope
алерти – базові, по костам і використанню ресурсів
дашборда зі статистикою API-запитів і використанню storage
можна включити server-side encryption
можливість створення снапшотів даних (але тільки якщо не включений SSE)
трафік upload в бакет – безкоштовний, download – дуже великий безкоштовний ліміт
$ rclone config
...
e) Edit existing remote
n) New remote
d) Delete remote
...
Enter name for new remote.
name> setevoy-backblaze-testing
...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
...
5 / Backblaze B2
\ (b2)
...
Account ID or Application Key ID.
Enter a value.
account> 003***001
...
Application Key.
Enter a value.
key> K00***MXQ
...
Option hard_delete.
Permanently delete files on remote removal, otherwise hide files.
Enter a boolean value (true or false). Press Enter for the default (false).
hard_delete>
Edit advanced config?
y) Yes
n) No (default)
y/n>
Configuration complete.
Options:
- type: b2
- account: 003f07593a16f1d0000000001
- key: K003+sr5NBQhJvsTdlCnfdt9UYHqMXQ
Keep this "setevoy-backblaze-testing" remote?
y) Yes this is OK (default)
...
Тут hard_delete залишив в дефолтному значенні, відключеним, але в prodution включив – бо інакше файли будуть не видалятись, а переміщатись в таку собі trash, ховатись.
Мені це не потрібно, бо цим всім займається rclone через --backup-dir.
Перевіряємо як новий ремоут спрацює – створюємо файл, копіюємо в бакет:
І він в UI – для цього бакету я включав SSE, відразу відображає, що файл зашифрований:
І створення снапшоту для цього файлу недоступне:
Налаштування rclone crypt remote
Тут аналогічно до AWS S3 або Google Drive – створюємо новий remote з типом crypt, підключаємо його до основного remote.
Ще раз запускаємо rclone config, тут приклад з шифруванням тільки даних – імена файлів і директорій будуть plaintext:
$ rclone config
...
name> setevoy-backblaze-testing-crypted
...
Option Storage.
Type of storage to configure.
...
Storage> crypt
Option remote.
Remote to encrypt/decrypt.
...
Enter a value.
remote> setevoy-backblaze-testing:setevoy-test-1
Option filename_encryption.
How to encrypt the filenames.
...
Press Enter for the default (standard).
/ Encrypt the filenames.
1 | See the docs for the details.
\ (standard)
2 / Very simple filename obfuscation.
\ (obfuscate)
/ Don't encrypt the file names.
3 | Adds a ".bin", or "suffix" extension only.
\ (off)
filename_encryption> 3
Option directory_name_encryption.
...
Press Enter for the default (true).
1 / Encrypt directory names.
\ (true)
2 / Don't encrypt directory names, leave them intact.
\ (false)
directory_name_encryption> 2
Option password.
...
y) Yes, type in my own password
g) Generate random password
y/g> y
Enter the password:
password:
Confirm the password:
password:
Option password2.
Password or pass phrase for salt.
...
n) No, leave this optional password blank (default)
y/g/n>
...
Configuration complete.
Options:
- type: crypt
- remote: setevoy-backblaze-testing:setevoy-test-1
- filename_encryption: off
- directory_name_encryption: false
- password: *** ENCRYPTED ***
...
Реальні дані та помилка “Cannot upload files, storage cap exceeded”
Взагалі думав на цьому і закінчувати, але не втримався – налаштував вже всю систему повністю, і тут ще є трохи шо показати.
Перше – коли почав заливати вже великі обсяги даних, то спіймав помилку “storage cap exceeded“:
2026/02/22 12:39:37 NOTICE: Failed to sync with 4 errors: last error was: Cannot upload files, storage cap exceeded. See the Caps & Alerts page to increase your cap. (403 storage_cap_exceeded)
ERROR: Rclone sync for nas/vault failed with exit code 7
Бо на безкоштовному акаунті маємо ліміт на 10 ГБ upload:
Додаємо картку – отримуємо повний доступ.
Backblaze pricing
Ну і раз вже дійшло до платежів – то трохи про вартість Backblaze.
Платимо тільки за сам storage – $6 за кожен терабайт, і за завантаження даних з бакетів – але тільки за той обсяг, який в 3 рази перевищує розмір даних, який зберігається в бакетах.
Тобто, зберігаємо 1 терабайт – можемо на місяць безкоштовно скачати 3 терабайти. Вище цього обсягу буде рахуватись по $0.01/GB.
Крім того, оплачується частина API-операцій, при цьому операції поділені на три окремі класи:
Class A: безкоштовні – створення бакетів, завантаження файлів (upload), видалення
Class B: скачування файлів (download), сюди входить b2_download_file_*, b2_get_file_info, наприклад – rclone sync з бакета до себе на машину
$0.004 за 10,000 операцій.
Class C: listing файлів, перевірка метаданих – сюди входять b2_list_file_names, b2_list_file_versions, наприклад – коли ми робимо rclone sync від себе в бакет
$0.004 за 1,000 операцій
Але при цьому маємо 2500 бескшотовних API-викликів на день.
Для моєї схеми найбільше буде Class C транзакцій, бо rclone при кожному sync перевіряє всі файли і їх modification time, тобто читає метадані (хоча це наче тюниться).
Ну і можна використати опцію rclone --fast-list – менше операції, але більше RAM.
Подивимось на суму, коли прийде перший рахунок 🙂
В Reports є повна інформація по кількості викликів кожного типу:
Власне, додаємо картку, через пару хвилин запускаємо копіювання даних – і все пройшло:
Але через помилку залишилось кілька incomplete uploads (“started large file” на скріншоті) – можна почистити з rclone cleanup або просто видалити вручну:
Upload speed і порівняння з Google Drive та AWS S3
Коли запустив завантаження свої бекапів швидкість виглядала так – але тут частково були невеликі файли:
Пізніше зробив окремий “бенчмарк” – завантаження файлу в 50 гігабайт з rclone copy.
Перший результат – Google Drive, максимум було 322 Mb/s, Backblaze розкачався до 417 Mb/s, а AWS видав цілих 516 Mb/s:
Ну а результат завантаження взагалі всього мого бекапу виглядає так:
Ще одна з (багатьох) приємних можливостей MikroTik – вбудована підтримка WireGuard (хоча вона є навіть на дешевому TP-Link Archer).
В моєму сетапі MikroTik RB4011 грає роль такого собі “VPN Hub” – всі клієнти підключаються до нього і об’єднуються в єдину мережу, і роль VPN трохи перебільшена тут дійсно важлива – бо це такий собі gateway, через які всі хости комунікують один з одним, і саме через VPN-тунелі з NAS мій скрипт для створення бекапів (про автоматизацію бекапів буде окремим великим постом, вже є в чернетках) підключається до всіх хостів, аби запустити rsync і стягнути до себе дані.
Момент з адресами для дроплетів в DigitalOcean: в мене підключений Reserved IP 67.207.75.157, який використовується на DNS – але для WireGuard використовуємо саме “дефолтний” Public IP від DigitalOcean – 46.101.201.123:
Перевіряємо наш новий address-list:
/ip firewall address-list print where list=wg-allowed
Тепер створюємо правило з цим списком в chain=input:
/ip firewall filter print where comment~"wg|office|home"
MikroTik Routes
MikroTik автоматично створює dynamic connected route для мережі WireGuard (10.100.0.0/24) після створення інтерфейсу wg0, і вже має routes для своїх локальних мереж, які безпосередньо підключені до його інтерфейсів (bridge, ether):
/ip route print
Або виберемо тільки ті, що нам зараз цікаві:
/ip route print where dst-address~"192.168.|10.100"
Тут 10.100.0.0/24 – мережа WireGuard, 192.168.0.0/24 – активна мережа DHCP-серверу на MikroTik, 192.168.88.0/24 – дефолтна мережа DHCP-серверу, зараз не використовується.
Але мережа дома – 192.168.100.0/24, і аби ходити з дому в офіс і навпаки – треба додати ще один роут:
/ip route add dst-address=192.168.100.0/24 gateway=wg0 comment="route to home via wg"
І роути тепер:
Можна переходити до підключення клієнтів.
WireGuard Peer на Linux
Сервер rtfm.co.ua хоститься в DigitalOcean, працює на Debian 12.
Сетап на Arch Linux такий самий, тільки пакет встановлюємо wireguard-tools – pacman -S wireguard-tools.
root@setevoy-do-2023-09-02:/etc/wireguard# cd /etc/wireguard/
root@setevoy-do-2023-09-02:/etc/wireguard# wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
Тут privatekey будемо використовувати для локального інтерфейсу, а publickey потім додамо на MikroTik WireGuard.
В allowed-address на MikroTik задаємо дозвіл на доступ в мережі – перевіряється і для src-addr, і для dst-addr.
Власне – на цьому все.
На Linux запускаємо підключення:
root@setevoy-do-2023-09-02:/etc/wireguard# wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.100.0.10/32 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 192.168.100.0/24 dev wg0
[#] ip -4 route add 192.168.0.0/24 dev wg0
[#] ip -4 route add 10.100.0.0/24 dev wg0
Перевіряємо статус:
root@setevoy-do-2023-09-02:/etc/wireguard#wg show
interface: wg0
public key: x+Pr/***TE=
private key: (hidden)
listening port: 59014
peer: hxz***50o=
endpoint: 178.***.184:51820
allowed ips: 10.100.0.0/24, 192.168.0.0/24, 192.168.100.0/24
latest handshake: 1 minute, 51 seconds ago
transfer: 16.21 KiB received, 21.75 KiB sent
persistent keepalive: every 25 seconds
На що звертаємо увагу – це latest handshake, який відображає, що підключення активне, і піри один з одним змогли зв’язатись.
Перевіряємо peers на MikroTik:
/interface wireguard peers print where comment="setevoy-rtfm"
Перевіряємо підключення з Linux на офісний ноутбук:
[setevoy@setevoy-work ~] $ ping -c 1 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
64 bytes from 192.168.100.100: icmp_seq=1 ttl=63 time=107 ms
--- 192.168.100.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 106.714/106.714/106.714/0.000 ms
А на домашньому ноуті слухаємо весь ICMP:
root@setevoy-home:~ # tcpdump -i any icmp and host 192.168.100.100
tcpdump: WARNING: any: That device doesn't support promiscuous mode
(Promiscuous mode not supported on the "any" device)
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
14:59:17.433334 wg0 In IP work.setevoy > setevoy-home: ICMP echo request, id 25635, seq 1, length 64
14:59:17.433381 wg0 Out IP setevoy-home > work.setevoy: ICMP echo reply, id 25635, seq 1, length 64
Якщо треба оновити параметри peer – виконуємо через interface wireguard peers set.
Для домашнього ноута при створені не задав 192.168.100.0/24 в allowed-address, треба було оновити параметр:
/interface wireguard peers set [find comment="setevoy-home"] allowed-address=10.100.0.3/32,192.168.100.0/24,192.168.0.0/24
А через те, що не було 192.168.100.0/24 в allowed-address – не було прямого підключення із 192.168.0.0/24 – бо пакет йшов через WireGuard тунель, приходив на домашній ноутбук на інтерфейс wg0, потім відправлявся на інтерфейс WiFi з адресою 192.168.100.100, але так як цього не було в allowed-address – то пакет дропався.
Вже потроху наближаюсь до завершення історії з налаштування домашнього NAS на FreeBSD.
Вже є ZFS pool, є датасети, є моніторинг – можна починати налаштування автоматизації бекапів.
Але якщо на початку все здавалось доволі просто – “просто скопіювати потрібні каталоги з робочого ноутбука”, то чим далі – тим цікавішою виявлялась задача.
Більш детальний опис планування та автоматизації бекапів опишу окремо, а сьогодні познайомимось з ще одною класною утилітою – Syncthing.
Всі частини серії по налаштуванню домашнього NAS на FreeBSD:
Отже, для чого вона мені: є кілька хостів (робочий та домашній ноутбуки, ігровий ПК), між якими треба синхронізувати загальні дані.
Загальні дані – це каталоги з фотками, музикою, картинками – все те, що змінюється не дуже часто, і де нема “мусора” типу каталогів .git, logs або tmp.
Такі каталоги повинні бути однаковими між ноутами та ПК і самим NAS, і коли я почав думати як жеж це все синхронізувати – то вперся в проблему того, що дані на будь-якому хості можуть і додатись і видалитись – і треба це все діло відстежувати і копіювати всі зміни.
Rsync чи Rclone тут не дуже підходять, бо у них принцип роботи “master-slave” – є один source of truth, і його зміст контролюється з Rsync/Rclone.
А в данному випадку, коли хостів декілька і кожен може робити власні зміни, які треба “відзеркалити” на інші – треба і інший інструмент, який зможе сам моніторити і копіювати всі зміни.
До того ж є і мобільний телефон з фотками, які хочеться бекапити напряму до NAS, а не в Goolge чи Proton Drive.
Власне, тут на сцену і виходить Syncthing:
підключається до кількох хостів
для кожного хоста налаштовується які саме локальні каталоги синхронізувати з іншими хостами та які каталоги з інших хостів синхронізувати локально
передає дані з шифруванням трафіку
До того ж має зручний Web UI, конфіг зберігає в файлі, який легко бекапити та має клієнтів під Android та iOS, і має чудову документацію.
Трохи забігаючи наперед (бо схема с наступного поста FreeBSD: Hone NAS, part 12: планування бекапів) – роль Syncthing в моєму сетапі виглядає так:
Отже, сьогодні установимо Syncthing на NAS з FreeBSD та на ноутбук з Arch Linux, і подивимось як це все працює.
Більшість налаштувань виконуються через Web (хоча є і CLI), але по дефолту Syncthing запускається на localhost.
А так як це FreeBSD без X-серверу – то і браузеру там нема.
Тому редагуємо файл і задаємо IP зовнішнього інтерфейсу, в мене це 192.168.0.2 (хоча адресацію буду перероблювати, коли доберусь до MikroTik та його DHCP):
Тепер додаємо першого “клієнта” – хоча Syncthing все ж peer-to-peer архітектура, але конкретно в моєму випадку є окремий сервер чи хаб, а інші хости – це клієнти.
І додати базову перевірку в Uptime Kuma (про неї теж скоріш за все буду писати ще окремо, в мене Kuma крутиться на окремому хості для “міні-моніторинга” на Raspberry PI):
Є у Syncthing і Prometheus метрики, див. Prometheus-Style Metrics – можна буде додати до VictoriaMetrics і створити Grafana dashboard та алерти.
І варто налаштувати бекапи для файлів Syncthing:
[setevoy@setevoy-work ~] $ ll ~/.local/state/syncthing
total 40
-rw-r--r-- 1 setevoy setevoy 623 Feb 13 19:14 cert.pem
-rw------- 1 setevoy setevoy 11236 Feb 13 20:15 config.xml
...
-rw------- 1 setevoy setevoy 119 Feb 13 19:14 key.pem
...
Для тих, хто не слідкує за апдейтами в Telegram-каналі rtfmcoua або просто перший раз зайшов на мій блог – нагадаю, що останні пару місяці збираю такий собі “self-hosted home stack”, в якому вже є пара MikroTik і ThinkCentre з FreeBSD.
До цього всього щастя вирішив ще додати окрему машинку під mini-monitoring, плюс там захостити системи типу Glance (див. Glance: налаштування self-hosted home page для браузера), бо ThnikCentre під час довгих блекаутів виключаю (хоча там споживання всього близько 20 Вт-год).
Ну і… Колись пробував Arduino – класно штука, але далі “Hello, World” діло не пішло (принаймні поки що), да і якісь системи типу Uptime Kuma на Arduino не захостиш.
Давно хотів спробувати погратись з Raspberry Pi, тільки раніше не міг придумати “а нахуа?” – і ось, нарешті, з’явилась відповідь на це велике питання.
Вибір Raspberry Pi
Чесно – я особо не вибирав 🙂
Точніше, вибирав, бо “вау, кросівєнько!” – десь випадково побачив Raspberry Pi Compute Module 4 PoE Mini-Computer, уявив, як він класно став би в мою серверну шафу – і вирішив взяти його.
Виглядає він ось так:
Є більш нові Compute Module версії 5 – але для моїх цілей, до того ж для першого досвіду, 4 версії вистачить з головою.
Купував в магазині https://minicomp.com.ua – не реклама, але магазин наче нормальний, відправили швидко, підтримка по телефону/Telegram працює, нарікань нуль.
[setevoy@setevoy-work ~] $ cd ~/Downloads/Rasp/
[setevoy@setevoy-work ~] $ tar xfp debian-13-raspi-arm64-daily.tar.xz
В архіві лежить disk.raw – це повний образ диска з вже готовим GPT/MBR та boot partition:
[setevoy@setevoy-work ~] $ fdisk -l ~/Downloads/Rasp/disk.raw
Disk /home/setevoy/Downloads/Rasp/disk.raw: 3 GiB, 3221225472 bytes, 6291456 sectors
...
Disklabel type: gpt
Disk identifier: 580A523C-E6C1-4021-8A56-D664D3C75FA2
Device Start End Sectors Size Type
/home/setevoy/Downloads/Rasp/disk.raw1 1048576 6289407 5240832 2.5G Linux root (ARM-64)
/home/setevoy/Downloads/Rasp/disk.raw15 2048 1048575 1046528 511M EFI System
Partition table entries are not in disk order.
Підключення USB до ноутбука
Отуто теж трохи витратив часу, бо дуже незвична і ніфіга не очевидна схема перемикання на завантаження по USB.
Навіть якась ностальгія по часам, коли на HDD перемикав джампери Primary/Slave.
Картинка для тих, хто не бачив це наживо
На моємо CM4 піни для включення завантаження з USB знайшлись отут:
Зайвого джамперу не було, але можна взяти з FAN/VDD:
Перемикаємо джампер, підключаємо звичайним USB-кабелем до ноутбука, перевіряємо девайси – має з’явитись Broadcom:
[setevoy@setevoy-work ~] $ lsusb | grep Broa
Bus 003 Device 024: ID 0a5c:2711 Broadcom Corp. BCM2711 Boot
Встановлюємо rpiusbboot – утиліта підключиться до Raspberry Pi Compute Module та змонтує її eMMC (embedded MultiMediaCard) диск до ноутбука як звичайну флешку:
[setevoy@setevoy-work ~] $ yay -S rpiusbboot
Запускаємо:
[setevoy@setevoy-work ~] $ sudo rpiusbboot
RPIBOOT: build-date Feb 12 2026 version 20221215~105525 b41ab04a
Waiting for BCM2835/6/7/2711...
Loading embedded: bootcode4.bin
Sending bootcode.bin
Successful read 4 bytes
Waiting for BCM2835/6/7/2711...
Loading embedded: bootcode4.bin
Second stage boot server
Cannot open file config.txt
Cannot open file pieeprom.sig
Loading embedded: start4.elf
File read: start4.elf
Cannot open file fixup4.dat
Second stage boot server done
Тепер маємо новий диск в системі:
[setevoy@setevoy-work ~] $ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 1 29.1G 0 disk
[setevoy@setevoy-work ~] $ ssh 192.168.0.61
...
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
setevoy@raspberrypi:~ $
setevoy@raspberrypi:~ $ nmcli device status
DEVICE TYPE STATE CONNECTION
eth0 ethernet connected Wired connection 1
lo loopback connected (externally) lo
І виконуємо або sudo nmcli device reapply eth0, або sudo nmcli device disconnect eth0 && sudo nmcli device connect eth0, або просто reboot – і тепер можемо підключатись за новою адресою:
[setevoy@setevoy-work ~] $ ssh 192.168.0.5
...
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Feb 12 11:54:41 2026 from 192.168.0.3
setevoy@raspberrypi:~ $
Можна відразу на MikroTik додати новий DNS record:
/ip dns static add name=pi.setevoy address=192.168.0.5 ttl=1d