AWS: ALB та Cloudflare – налаштування mTLS та AWS Security Rules
5 (2)

15 Березня 2026

Під час підготовки інфраструктури для міграції RTFM з серверу в DigitalOcean до AWS (див. AWS: сетап базової інфраструктури для WordPress) вирішив заодно спробувати AWS ALB – ALB mutual authentication (мені чомусь здавалось, що цю фічу запустили на останньому re:Invent, в кінці 2024, але вона є з кінця 2023 року – див. Mutual authentication for Application Load Balancer reliably verifies certificate-based client identities).

Ідея полягає в тому, щоб дозволити підключення до ALB тільки тим клієнтам, які пройдуть аутентифікацію, і в випадку Cloudflare+AWS ALB – Cloudflare має підписувати всі свої запити з TLS-сертифікатом (див. Cloudflare Authenticated Origin Pulls), а AWS ALB буде їх перевіряти – таким чином доступ до Load Balancer URL буде можливий тільки для Cloudflare.

Надихнула мене на цю ідею нещодавня історія з TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death” – коли TCP-конекти відкривались напряму на порт 443 на сервері в DigitalOcean, а не йшли через Cloudflare.

Правда, окрім mTLS я пізніше все одно вирішив додати ще і обмеження по IP – налаштував доступ до ALB тільки для адрес Cloudflare, які задані в AWS Security Rules, бо mTLS – це всеж про аутентифікацію, а Security Rules – це вже саме захист на рівні мережі.

Хоча спочатку думав робити чисто mTLS – бо і цікаво було подивитись, як це в ALB працює – і лінь було робити автоматизацію для оновлення AWS Security Rules.

Приклад тут робився на тестовому домені – тому заодно додамо його на Cloudflare, але обидва рішення вже працюють і для самого rtfm.co.ua.

Для чого, власне, обмежувати доступ до AWS Application Load Balancer:

  • на Cloudflare є багато Security rules, які блокують небажаний трафік, якщо ж запити йдуть напряму на ALB – то вони дойдуть до NGINX, і тоді частину правил треба робити і там (в мене так і було)
  • вартість при DDoS: поки блог жив на сервері в DigitalOcean, де просто був Public IP для дроплету, то запити до цього IP на вартість не впливали (тільки додатковий трафік), але в AWS та ALB кількість підключень впливає на вартість

P.S. І знов наче намагаєшся писати стисло – а вийшло багато тексту 🙁

Вартість AWS ALB та LCU

Вартість AWS Load Balancer включає в себе погодинну оплату за сам інстанс, стандартну плату Data Transfer Charge, за виділені Public IPs, та окремо – Load Balancer Capacity Units (LCU).

Для Network Load Balancer є власний юніт – Network Load Balancer Capacity Unit (NLCU), а для Gateway Load Balancer, відповідно, GLCU.

Див. Elastic Load Balancing pricing.

LCU не дуже очевидний юніт, тому кілька слів про нього.

1 LCU – це:

  • New connections: нові підключення до ALB – 25 підключень в секунду “з’їдають” 1 LCU
  • Active connection: кожні 3000 активних підключень (або 1500 при використанні mTLS) на хвилину
  • Processed bytes: 1 LCU покриває 1 GB трафіку на годину (або 0.4 GB для Lambda targets) – рахується сума ingress + egress
  • Rule evaluations: перевірка 1.000 правил в секунду – це один LCU

Під Rule evaluations маються на увазі Listener rules:

І якщо маємо багато правил по типу “IF path is /api/* THEN forward to target-group-api” – платимо за кожну перевірку кожного нового HTTP request.

При визначені того, з якого саме параметра вище рахувати LCU, AWS бере той, який перший досяг потрібного значення: тобто, як тільки маємо 26 запитів на секунду, але при цьому Processed bytes був лише 10 мегабайт – то нам порахуються 1 витрачений LCU за рахунок New connections.

Отже, у випадку DDoS (а у RTFM кілька раз траплялись, хоча і не сильні) – ми легко можемо “попасти на гроші”.

Є калькулятор Load Balancer Capacity Unit Reservation Calculator – можна прикинути, скільки LCU буде коштувати навантаження.

AWS Load Balancer mTLS use cases

Насправді якщо використовувати обмеження доступу до ALB через Security Group зі списком тільки дозволених IP – то mTLS не треба взагалі, бо ми вже “ріжемо” підключення на рівні мережі AWS, ще до того, як запит взагалі дійде до самого Load Balancer.

Що дійсно може бути корисним – це:

  • аутентифікація між сервісами:
    • наприклад, у нас є Internal ALB, за яким живуть різні сервіси моніторингу, такі як VictoriaLogs
    • в логах може бути чутлива інформація типу токенів або навіть паролів, тому до цього ендпоінта треба мати обмежений доступ
    • vmauth з парольною аутентифікацію – це добре, але mTLS додасть ще один рівень захисту (див. VictoriaMetrics: VMAuth – проксі, аутентифікація та авторизація)
  • аутентифікація мобільних клієнтів на API:
    • в клієнтах “зашиті” сертифікати (certificate pinning), які вони використовують для доступу до API
    • але тут треба мати на увазі можливі проблеми з ротацією сертифікатів – або у випадку компрометації, або просто коли закінчується строк дії
  • IoT: у нас (поки що) нема, але в цілому – дуже корисний варіант

Налаштування ALB mutual TLS з Cloudflare

Спершу подивимось варіант з обмеженням доступу з mTLS, бо штука цікава і корисна.

А потім додамо вже з AWS Security Rules та автоматизацією оновлень правил в ній.

Налаштування Cloudflare

Пост писався ще до міграції RTFM, робив на тестовому домені, тому швиденько пройдемось по процесу додавання і налаштування домену в Cloudflare.

Додавання нового домену до Cloudflare

Переходимо в Domains, клікаємо Onboard a domain:

Вказуємо ім’я, решту налаштувань можна з дефолтними значеннями:

При імпорті записів з AWS DNS пропустило кілька записів по Let’s Encrypt validation, але зараз це ОК:

Отримуємо список нових Name Servers:

Міняємо у реєстратора домену:

Чекаємо на оновлення – час залежить від адміністратора доменної зони:

Для kiev.ua (в мене вже є і setevoy.kyiv.ua) це зайняло з півгодини:

Переходимо в SSL/TLS, включаємо опцію Authenticated Origin Pulls.

Як тільки ми включимо цю опцію, Cloudflre при кожному новому запиті до origin – в нашому випадку AWS ALB – почне додавати свій клієнтський сертифікат.

Тут дуже хотілось трохи детальніше описати за SSL/TLS handshake та ключі-сертифікати – але подумав, що в рамках цього посту це буде зайвим, тим більш, що колись розбирався з деталями в пості What is: SSL/TLS в деталях.

Хоча, може, і зроблю новий пост на цю тему, тим більш зараз вже TLS 1.3, а там описаний актуальний на той момент TLS 1.2.

Або можна почитати вже згаданий на початку Introducing mTLS for Application Load Balancer – там непогано описано базові концепції по самому TLS.

Отже, Cloudflre тільки передає клієнтський сертифікат, а далі вже справа origin – що з ним робити, і наступним кроком ми як раз і налаштуємо його перевірку на самому ALB.

Поки просто вмикаємо опцію – грошей вона не просить (окрім того, що вартість 1 LCU по Active connections буде 1500 замість 3000), значного навантаження на ALB чи додаткового трафіку не зробить:

Для домену додаємо запис CNAME з value == ALB URL:

Перевіряємо, що зараз все працює – бо хоч Authenticated Origin Pulls на Cloudflare ми включили, але на ALB ще ніяких перевірок не виконується:

І запит на URL самого ALB все ще працює, тільки з помилкою SSL:

Налаштування Application Load Balancer

Тепер нам треба налаштувати перевірку клієнтського сертифіката від Cloudflare на нашому ALB.

Отримання Cloudflare Certificate Authority

ALB для перевірки буде використовувати публічний Certificate Authority сертифікат від Cloudflare, який нам треба додати в Trust Store самого Load Balancer, а потім підключити його до ALB Listener.

Майте на увазі, що використання Trust Store теж платне – “$0.0056 per hour per Trust Store Associated with Application Load Balancer when using Mutual TLS“, тобто ~ 4 USD на місяць.

Завантажуємо сертифікат:

$ curl -o cloudflare-origin-pull-ca.pem https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem

Сертифікат загальний для всього Cloudflare – хоча є можливість додати свій власний і проходити перевірку тільки з ним.

Але для випадку, коли нам просто треба закрити доступ всяким китайським ботам – сертифіката самого Cloudflare вистачить.

Перевіряємо скачаний файл:

$ file cloudflare-origin-pull-ca.pem
cloudflare-origin-pull-ca.pem: PEM certificate

Або можна глянути шо в ньому:

$ openssl x509 -in cloudflare-origin-pull-ca.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 6310029703491235425 (0x5791ba9556c22e61)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net
        Validity
            Not Before: Oct 10 18:45:00 2019 GMT
            Not After : Nov  1 17:00:00 2029 GMT
        Subject: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net
...

З важливого тут – Not After : Nov 1 17:00:00 2029:

  • можна в налаштуваннях ALB для mTLS включити опцію “Allow expired client certificates”
  • або просто в кінці 2029 року додати в Trust Store новий сертифікат та видалити старий

Створення Trust Store

Для додавання сертифіката до ALB нам треба буде створити Trust Store, але в Trust Store додати сертифікат можна тільки з AWS S3 – тому спочатку завантажуємо в якусь свою корзину:

Переходимо да EC2 > Load Balancing > Trust Stores, створюємо новий:

Вказуємо шлях до файлу сертифіката в S3:

Налаштування mTLS для ALB Listener

Редагуємо HTTPS Listener – бо mTLS, логічно, відбувається на ньому:

Включаємо опцію mTLS, і маємо дві опції:

  • Passthrough: передаємо сертифікат на таргети в Target Group – валідацію виконує бекенд
  • Verify with trust store: виконуємо перевірку на самого ALB з сертифікатом із Trust Store – наш випадок

Переключаємо на “Verify with trust store”:

Тепер пробуємо пряме підключення до ALB URL:

$ curl -kI https://rtfm-alb-1984146384.eu-west-1.elb.amazonaws.com
curl: (56) Recv failure: Connection reset by peer

І в браузері:

Але через Cloudflare все працює:

$ curl -I https://test-tls.setevoy.kiev.ua
HTTP/2 200

 

Ну і сам rtfm.co.ua вже теж на цьому ALB і має включений mTLS.

Захист з AWS Security Group та Cloudflare IP ranges

Але тільки mTLS для повноцінного захисту ALB все ж недостатньо:

  1. при DDoS ми все-одно будемо витрачати LCU на підключення
  2. запити, які пройдуть не через Cloudflare треба фільтрувати на бекенді – в моєму випадку NGINX

Тому самий надійний спосіб – це обмежити трафік на рівні AWS Security Group, яка підключена до Load Balancer: тоді ніяких LCU не буде витрачатись, бо пакети до ALB не дійдуть взагалі.

Є готові Terraform-модулі, наприклад – cloudflare-security-group, але він працює тільки з одною Security Group, є більш просунутий варіант – cloudflare-sg-updater, який шукає SG по тегам.

Втім, особисто я не дуже люблю AWS Lambda і, принаймні поки що, для RTFM не використовую Terraform – то зробив просто з shell-скриптом, який використовує AWS CLI.

Як раз до цього було багато практики, аби згадати як писати скрипти – див. FreeBSD: Home NAS, part 15: автоматизація бекапів – скрипти, rsync, rclone.

AWS CLI на Amazon Linux вже йде в комплекті, треба тільки або налаштувати AWS Profile – або підключити EC2 Instance Profile з IAM-політикою, яка дасть дозвіл на внесення змін в Security Group.

Єдиний нюанс, який треба мати на увазі, якщо пишете скрипт самі: IP іноді змінюються, хоч і рідко, і їх треба видаляти з Security Group. Але при цьому просто видалити всі старі записи, а потім додати нові – не варіант, бо в цей момент ALB втратить підключення до Cloudflare.

Тому в скрипті виконується порівняння старих і нових адрес, і видаляються тільки ті, яких нема в останньому апдейті від Cloudflare.

Створення IAM Role для EC2 Instance profile

Рано чи пізно все одно треба буде додавати IAM Role для інстансу, тому замість використання AWS CLI профайл з ключами краще відразу робити доступ до редагування Security Group через EC2 Instance profile.

Для ролі потрібно дати права на виконання ec2:DescribeSecurityGroups, ec2:AuthorizeSecurityGroupIngress та ec2:RevokeSecurityGroupIngress – створимо окрему IAM Policy.

Створюємо нову Security Group для ALB – правила Inbound залишаємо пустими, в Outbound – залишаємо дефолтний “All to All”:

Створюємо нову IAM Policy, обмежуємо доступ тільки цією Security Group.

Для ec2:DescribeSecurityGroups задати обмеження не можна, бо правило глобальне для всіх SG, тому описуємо двома окремими Statement – один з  Resource": "*", другий вже з конкретною SG.

Робимо з обмеженням по Resource: вказуємо обмеження доступу конкретною групою, бо скрипт буде виконувати destructive дії – видалення правил, і для таких дій на випадок “щось пішло не так” завжди краще обмежувати blust radius:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ec2:DescribeSecurityGroups",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupIngress"
            ],
            "Resource": "arn:aws:ec2:eu-west-1:ACCOUNT_ID:security-group/sg-029ffc5f56be700ea"
        }
    ]
}

 

Задаємо ім’я політики, зберігаємо:

Створюємо IAM Role, в Use Case вибираємо EC2:

Підключаємо політику, яку створили вище:

Задаємо ім’я нової ролі, зберігаємо (хоча ім’я тут вибрав не найкраще – бо 100% якісь ще доступу в цю роль додаватись будуть):

В Security > Modify IAM Role підключаємо роль до EC2 – можна робити без зупинки інстансу:

Підключаємо нашу нову роль:

Підключаємось по SSH до інстансу, перевіряємо, що в нього тепер є доступ до, наприклад, ec2:DescribeSecurityGroups:

[ec2-user@ip-10-0-3-146 ~]$ aws ec2 describe-security-groups --group-ids sg-029ffc5f56be700ea
{
    "SecurityGroups": [
        {
            "GroupId": "sg-029ffc5f56be700ea",
            "IpPermissionsEgress": [
                {
                    "IpProtocol": "-1",
                    "UserIdGroupPairs": [],
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": []
                }
            ],
...

Shell script для оновлення ALB Security Group

Додаємо скрипт, який буде:

  1. отримувати список актуальних Cloudflare IP CIDR (і IPv4, і IPv6)
  2. з AWS CLI отримувати список поточних правил в Security Group
  3. порівнювати нові адреси і значення з Security Group і додавати тільки ті адреси, яких зараз нема
  4. і аналогічна перевірка для видалення адрес з Security Group – якщо IP нема в актуальному списку від Cloudflare, то видаляємо

Security Group одна, тому її просто задаємо на початку в змінну $SG_ID:

#!/bin/bash

# Update ALB security group with current Cloudflare IP ranges
SG_ID="sg-029ffc5f56be700ea"
PORT=443

# Fetch current Cloudflare IP ranges
CF_IPS=$(curl -s https://www.cloudflare.com/ips-v4; echo; curl -s https://www.cloudflare.com/ips-v6)

# Get current IPs from security group
CURRENT_IPS=$(aws ec2 describe-security-groups --group-ids $SG_ID --query "SecurityGroups[0].IpPermissions[?FromPort==\`$PORT\`].[IpRanges[].CidrIp, Ipv6Ranges[].CidrIpv6]" --output text)

# Add missing IPs
for IP in $CF_IPS; do
    if ! echo "$CURRENT_IPS" | grep -q "$IP"; then
        echo "Adding $IP"
        if echo "$IP" | grep -q ":"; then
            aws ec2 authorize-security-group-ingress --group-id $SG_ID --ip-permissions "[{\"IpProtocol\":\"tcp\",\"FromPort\":$PORT,\"ToPort\":$PORT,\"Ipv6Ranges\":[{\"CidrIpv6\":\"$IP\"}]}]"
        else
            aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port $PORT --cidr $IP
        fi
    fi
done

# Remove IPs no longer in Cloudflare list
for IP in $CURRENT_IPS; do
    if ! echo "$CF_IPS" | grep -q "$IP"; then
        echo "Removing $IP"
        if echo "$IP" | grep -q ":"; then
            aws ec2 revoke-security-group-ingress --group-id $SG_ID --ip-permissions "[{\"IpProtocol\":\"tcp\",\"FromPort\":$PORT,\"ToPort\":$PORT,\"Ipv6Ranges\":[{\"CidrIpv6\":\"$IP\"}]}]"
        else
            aws ec2 revoke-security-group-ingress --group-id $SG_ID --protocol tcp --port $PORT --cidr $IP
        fi
    fi
done

echo "Done"

Задаємо права на запуск скрипта:

[root@ip-10-0-3-146 ~]# chmod +x /opt/update-alb-sg/update-alb-sg-from-cloudflare.sh

Запускаємо вручну для перевірки:

Перевіряємо в Security Group:

Заміна Security Group на ALB

Редагуємо список Security Groups для Load Balancer:

Додаємо нову, видаляємо стару:

Перевіряємо, що все працює:

$ curl -I https://rtfm.co.ua/
HTTP/2 200 

Але з дому напряму до ALB тепер доступу нема:

$ curl --connect-timeout 5 -kI rtfm-alb-1984146384.eu-west-1.elb.amazonaws.com
curl: (28) Connection timed out after 5002 milliseconds

Додаємо в cron.

Запуск з crontab

В Amazon Linux версії AL2023 стандартний cron випиляли – встановлюємо його:

# yum install -y cronie
# systemctl enable crond
# systemctl start crond

Запускаємо, наприклад, раз на добу в 3 ночі:

0 3 * * * /opt/update-alb-sg/update-alb-sg-from-cloudflare.sh >> /var/log/update-cf-sg.log 2>&1

Можна ще підписатись на Cloudflare changelog/RSS, аби отримувати нотифікації коли щось міняється в IP діапазонах.

Але зміна IP ranges прям дуже рідко буває – останній раз у 2023 році, див IP Ranges:

Власне, на цьому все.

Тепер можна спокійно спати: “frontend” у нас Cloudflare, де є Security rules, WAF і захист від DoS/DDoS, а на “бекенді” ми захищені на рівні мережі, L3/L4 – і Load Balancer та EC2 живуть спокійно.

Див. мій пост по TCP/IP: моделі OSI та TCP/IP, TCP-пакети, Linux sockets і порти та документацію самого AWS – Network security.

P.S. Документація AWS – одна з тих речей, за що його дійсно люблю. Колись довелось багато працювати з Azure – і там це дійсно велика проблема. До речі, див. Azure: почему никогда.

Loading

AWS: власний EC2 в ролі NAT Gateway замість AWS Managed NAT Gateway
5 (1)

12 Березня 2026

Подивився я на costs по інфраструктурі, яка описана в попередньому пості AWS: сетап базової інфраструктури для WordPress, і тяжко зітхнув:

Один 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:

  1. 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
  2. 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:

Задаємо Stop:

Налаштування Linux в ролі NAT Gateway

Підключаємось до EC2 (EC2 Instance Connect робили в попередньому пості):

[setevoy@setevoy-work ~]  $ aws --region eu-west-1 --profile setevoy ec2-instance-connect ssh --instance-id i-0e54f36ce6bb90da4 --connection-type eice
...
[ec2-user@ip-10-0-1-79 ~]$

Перевіряємо параметр net.ipv4.ip_forward, який задає ядру системи дозвіл на пересилання пакетів між мережевими інтерфейсами – саме це і є основою будь-якого NAT і роутингу:

[ec2-user@ip-10-0-1-79 ~]$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

0 – відключений, вмикаємо його:

[ec2-user@ip-10-0-1-79 ~]$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

Зміни з sysctl -w застосовуються зараз, але після перезавантаження системи зникнуть, тому додаємо в конфіг /etc/sysctl.conf:

[ec2-user@ip-10-0-1-79 ~]$ echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
net.ipv4.ip_forward = 1

Перечитуємо конфіг, застосовуємо зміни:

[ec2-user@ip-10-0-1-79 ~]$ sudo sysctl -p
net.ipv4.ip_forward = 1

Перевіряємо ще раз:

[ec2-user@ip-10-0-1-79 ~]$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

Налаштування iptables та MASQUERADE

Можна зробити з nftables, але iptables частіше зустрічається, та і більш звично.

Детальніше про iptables писав ще у 2014 році (OMG!), див. Linux: IPTABLES – руководство: часть 1 – основы IPTABLES – кардинальних змін в основах не було, хоча сам iptables вже поступово замінюється на nftables.

Перевіряємо імена інтерфейсів:

[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
...

Встановлюємо пакет iptables:

[ec2-user@ip-10-0-1-79 ~]$ sudo dnf install -y iptables

Додаємо правило:

[ec2-user@ip-10-0-1-79 ~]$ sudo iptables -t nat -A POSTROUTING -o enX0 -j MASQUERADE

Тут:

  • -t nat: вносимо зміни в таблицю nat
  • -A POSTROUTING: додаємо правило в POSTROUTING chain – тобто правило спрацьовує після того, як ядро вже вирішило куди відправити пакет – але ще не відправило отримувачу
  • -o enX0: правило тільки для пакетів що виходять через інтерфейс enX0 (наш публічний інтерфейс)
  • -j MASQUERADE: підміняємо source IP пакету (Private IP інстансу EC2, на якому буде блог) на IP інтерфейсу enX0 – публічний IP нашого інстансу EC2 з NAT Gateway

Перевіряємо правила:

[ec2-user@ip-10-0-1-79 ~]$ sudo iptables -t nat -L POSTROUTING -n -v
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      enX0    0.0.0.0/0            0.0.0.0/0

Аби правило зберігалось при рестартах інстансу – додаємо (вже має бути) пакет iptables-services, зберігаємо правила у файл /etc/sysconfig/iptables:

[ec2-user@ip-10-0-1-79 ~]$ sudo dnf install -y iptables-services

[ec2-user@ip-10-0-1-79 ~]$ sudo service iptables save

[ec2-user@ip-10-0-1-79 ~]$ sudo systemctl enable iptables

Зміни в AWS VPC Route Tables

У нас є окремі таблиці маршрутизації на кожну Private Subnet:

В яких зараз маршрут до інтернету, тобто 0.0.0.0/0, заданий через AWS Managed NAT Gateway:

Додаємо нове правило, вказуємо новий EC2 інстанс:

Видаляємо маршрут через NAT gateway – тут можливий короткий downtime, бо активні підключення із VPC в інтернет будуть розірвані:

Повторюємо зміни для обох Route Tables, і перевіряємо.

Підключаємось на EC2, дивимось результат ifconfig.me – і отримуємо Public IP нашого нового self-managed NAT Gateway:

[ec2-user@ip-10-0-3-146 ~]$ curl -s ifconfig.me
108.130.182.54

Власне, на цьому і все.

Можна видаляти AWS Managed NAT Gateway:

Bonus: Amazon Linux auto-upgrades

Ну і аби не ходити на інстанс з ручними апгрейдами – налаштуємо аналог unattended-upgrades з Debian.

В RHEL/CentOS/Fedora/Amazon Linux це пакет dnf-automatic:

[ec2-user@ip-10-0-1-79 ~]$ sudo dnf install -y dnf-automatic

Редагуємо конфіг /etc/dnf/automatic.conf, мінімально – задаємо apply_updates = yes.

В 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.

Додаємо запуск по крону за розкладом:

[ec2-user@ip-10-0-1-79 ~]$ sudo systemctl enable --now dnf-automatic.timer
Created symlink /etc/systemd/system/timers.target.wants/dnf-automatic.timer → /usr/lib/systemd/system/dnf-automatic.timer.

Готово.

Loading

AWS: сетап базової інфраструктури для WordPress
0 (0)

11 Березня 2026

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

По ходу діла знайшов непогане порівняння – Amazon Linux vs Debian: What are the differences?

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 я залишаю “резервним”, да і виглядає так красивіше:

  • дві публічні:
    • 10.0.1.0/24
    • 10.0.2.0/24
  • дві приватні:
    • 10.0.3.0/24
    • 10.0.4.0/24

Створення NAT Gateway

Тут буде описано створення звичайного, AWS Managed NAT Gateway, але пізніше я його замінив на “NAT Gateway для бідних” – просто окремий EC2, див. AWS: власний EC2 в ролі NAT Gateway замість AWS Managed NAT Gateway.

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.

Про VPC Endpoints трохи детальніше писав в пості Terraform: створення EKS, частина 1 – VPC, Subnets та Endpoints.

DNS Options залишаємо включеними – штука корисна і грошей не просить:

  • DNS hostnames: чи створювати “локальні” імена, наприклад – ip-10-0-3-226.eu-west-1.compute.internal – потрібно, аби коректно працювали RDS, EFS та інші мережеві ресурси
  • DNS resolution: чи зможуть сервіси всередині VPC використовувати його внутрішній DNS – теж штука корисна і зручна, хоча має свої обмеження (див, наприклад, Kubernetes: нагрузочное тестирование и high-load тюнинг – проблемы и решения)

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

Тому зробив простіше – через EC2 Instance Connect Endpoint.

Переходимо в 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:
    • приклад: c5.large – 2 vCPU, 4 GiB RAM, ~61 USD/month
  • memory optimized: і навпаки – більше RAM і менше CPU
    • приклад: r5.large – 2 vCPU, 16 GiB RAM, ~90 USD/month
  • storage optimized: мають NVMe диски з високим IOPS
    • приклад: i3.large – 2 vCPU, 15.25 GiB RAM, ~112 USD/month

Цифри 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

Для вибору користуємось сервісами типу https://instances.vantage.sh або https://calculator.holori.com/aws.

Зараз сервер для RTFM в DigitalOcean має 2 vCPU і 4 GB RAM:

При цьому навантаження на процесор в середньому в районі 5%, а пам’ять зайнята на 60%:

Але зараз на цьому ж сервері живе MariaDB Server:

Тобто, якщо винести базу даних в AWS RDS, то на новому інстансі основним “споживачем” пам’яті буде PHP-FPM.

Можна подивитись скільки пам’яті процеси php-fpm використовують зараз:

root@setevoy-do-2023-09-02:~# ps aux --sort=rss | grep php-fpm | awk '{print $6}' | awk '{sum+=$1} END {print sum/1024 " MB total"}'
410.477 MB total

І кількість процесів:

root@setevoy-do-2023-09-02:~# ps aux --sort=rss | grep php-fpm | grep 'master\|rtfm.co.ua' | grep -v grep
root     1157320  0.0  0.3 264156 13092 ?        Ss   Mar03   0:55 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
rtfm     1238997  1.3  3.1 362608 126980 ?       S    15:23   0:07 php-fpm: pool rtfm.co.ua
rtfm     1237462  1.7  3.2 360912 129788 ?       S    12:16   3:26 php-fpm: pool rtfm.co.ua
rtfm     1237598  1.7  3.3 364440 132484 ?       S    12:34   3:04 php-fpm: pool rtfm.co.ua

Прикинемо скільки йому треба.

Параметри PHP-FPM pool для RTFM зараз:

...
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
...

Див. PHP-FPM: Process Manager – dynamic vs ondemand vs static (2018 рік) та NGINX: настройка сервера и PHP-FPM (2014 рік).

Тобто максимум може бути 5 FPM workers – і з цією кількістю воркерів блог спокійно переживав TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death”, бо більшість запитів оброблюються на CloudFlare:

Аби прикинути споживання пам’яті кожним можемо глянути в RSS (Resident Set Size), реальна фізична пам’ять процесу – але сюди включається пам’ять на shared бібліотеки, тобто якщо кілька PHP-FPM workers використовують одну і ту ж libc – RSS кожного включає її повністю і сумарний RSS буде завищений.

Втім – нехай буде завищена, бо ми прикидуємо “найгірший” варіант.

Дивимось скільки пам’яті на кожен воркер зараз:

root@setevoy-do-2023-09-02:~# ps aux | grep php-fpm | grep 'pool rtfm.co.ua' | awk '{print $6/1024 " MB - " $13}'
126.773 MB - rtfm.co.ua
133.355 MB - rtfm.co.ua
126.168 MB - rtfm.co.ua

І якщо маємо pm.max_children = 5 – то максимум пам’яті буде ~150 MB * 5 == 750 MB.

Можна для початку взяти t3.medium – хоча це прям запас з головою:

Решта налаштувань

Створюємо ключ для доступу по SSH:

Зберігаємо собі на робочу машину, відразу задаємо права:

$ chmod 600 ~/.ssh/rtfm-al-2026-03.pem

Вибираємо VPC, сабнет eu-west-1a та Securty Group, яку створили вище:

Сторейдж – одного диску на 50 гігабайт вистачить з запасом:

В Advanced details включаємо Termination protection – дуже корисна для production ресурсів опція.

І додатково можна поки що додати Detailed CloudWatch monitoring – але за нього доведеться платити додаткові гроші, тому потім краще відключити:

Запускаємо інстанс.

Поки робили це – EC2 Instance Connect Endpoint вже готовий:

Сам EC2 інстанс стартує дуже швидко – перевіряємо підключення:

Вибираємо EC2 Instance Connect, вибираємо “Connect using Private IP”:

І ми в системі:

Аби підключитись з ноутбука – використовуємо AWS CLI:

$ aws --region eu-west-1 --profile setevoy ec2-instance-connect ssh --instance-id i-026523e8f29147e3e --connection-type eice

А пізніше вже буде пряме підключення через VPN.

Поки робимо апгрейд і встановлюємо для тесту NGINX:

# dnf update -y
# dnf install -y nginx
# systemctl enable nginx
# systemctl start nginx

Перевіряємо:

# curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Тут все готове – можна переходити до SSL/TLS та Load Balancer, а потім встановити PHP і вже перевірити роботу WordPress.

Отримання SSL/TLS сертифікату з AWS Certificate Manager

Дуже зручна штука – бо один раз отримати, підключити до Load Balancer, і забути – далі AWS сам менеджить всі renew.

Переходимо в ACM, клікаємо Request a certificate:

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

Залишаємо дефолту опцію DNS validation:

Клікаємо Create records in Route 53 – це, звісно, тільки для тих доменів, які обслуговуються на Route 53, див. далі приклад з Cloudflare Name Servers:

Перевіряємо чи додався новий запис для домену в Route 53:

AWS ACM Certificate та DNS validation для домену на Cloudflare Nameservers

Домен rtfm.co.ua обслуговується серверами Cloudflare:

$ whois rtfm.co.ua | grep "Name Server"
Name Server:ETHAN.NS.CLOUDFLARE.COM
Name Server:NOVALEE.NS.CLOUDFLARE.COM

Тому в панелі керування DNS Cloudflare додаємо новий запис з типом CNAME:

Але…

Фікс помилки AWS ACM Certificate DNS validation failed

Але валідація сфейлилась:

Читаємо документацію Certification Authority Authorization (CAA) problems, перевіряємо записи з типом CAA (Certification Authority Authorization) для домену – хто може видавати сертифікати для цього домену:

$ dig rtfm.co.ua CAA +short
0 issuewild "digicert.com; cansignhttpexchanges=yes"
0 issuewild "letsencrypt.org"
0 issuewild "pki.goog; cansignhttpexchanges=yes"
0 issuewild "ssl.com"
0 issue "comodoca.com"
0 issue "digicert.com; cansignhttpexchanges=yes"
0 issue "letsencrypt.org"
0 issue "pki.goog; cansignhttpexchanges=yes"
0 issue "ssl.com"
0 issuewild "comodoca.com"

І дійсно – AWS тут нема.

Додаємо два CAA записи – один для сертифікатів для корневого домену:

І один для wildcard сертифікатів:

Ще раз перевіряємо:

$ dig @ethan.ns.cloudflare.com rtfm.co.ua CAA +short
0 issue "amazon.com"
...
0 issuewild "amazon.com"
...

Видаляємо перший сертифікат, повторюємо процес створення та валідації – і тепер все готово:

Створення 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 окремо.

Детальніше див. What’s the Difference Between Application, Network, and Gateway Load Balancing?

Налаштування AWS Load Balancer

Переходимо до створення Load Balancer:

 

Вибираємо тип Application Load Balancer:

 

Задаємо ім’я, тип 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” – сервер баз даних має жити тільки в приватних мережах, без доступу у світ.

Згадав про цікаву історію – MySQL/MariaDB: like Petya ransomware для баз данных и ‘root’@’%’: клієнт створив штук 10 серверів БД з публічним доступом, доступом root з інтернету, і… Без пароля.

Результат прєдсказуємий 🙂

У 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:

[ec2-user@ip-10-0-3-146 ~]$ sudo dnf install -y mariadb114

В AWS Secrets Manager знаходимо пароль RDS root:

І підключаємось, використовуючи 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 та, в принципі, будь-якого вебсайту:

[root@ip-10-0-3-146 ~]# dnf install -y php-fpm php-mysqlnd php-json php-mbstring php-xml php-gd

Додаємо в автостарт та запускаємо сервіс PHP-FPM:

[root@ip-10-0-3-146 ~]# systemctl enable php-fpm
Created symlink /etc/systemd/system/multi-user.target.wants/php-fpm.service → /usr/lib/systemd/system/php-fpm.service.
[root@ip-10-0-3-146 ~]# systemctl start php-fpm

Дефолтний конфіг – /etc/php-fpm.d/www.conf, для RTFM буде окремий, але це не зараз.

Перевіряємо файл сокету, щоб впевнитись, що FPM готовий приймати підключення:

[root@ip-10-0-3-146 ~]# ll /run/php-fpm/www.sock
srw-rw----+ 1 root root 0 Mar  8 11:34 /run/php-fpm/www.sock

Створення NGINX virtualhost

Додаємо файл налаштувань тестового сайту – /etc/nginx/conf.d/test.conf.

Каталог /etc/nginx/conf.d/ включається в конфіг через основний файл налаштувань /etc/nginx/nginx.conf:

...
 include /etc/nginx/conf.d/*.conf;
...

У файлі /etc/nginx/conf.d/test.conf описуємо HTTP сервер на порту 80 з fastcgi_pass на PHP-FPM socket:

server {
    listen 80;
    server_name test-alb.setevoy.org.ua;

    root /var/www/html;
    index index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Для перевірки 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:

[root@ip-10-0-3-146 ~]# cd /var/www/html
[root@ip-10-0-3-146 html]# wget https://wordpress.org/latest.tar.gz
...
[root@ip-10-0-3-146 html]# tar -xzf latest.tar.gz
[root@ip-10-0-3-146 html]# mv wordpress/* .
mv: overwrite './index.php'? y
[root@ip-10-0-3-146 html]# rm -rf wordpress latest.tar.gz
[root@ip-10-0-3-146 html]# chown -R nginx:nginx /var/www/html

Відкриваємо в браузері – не завантажуються 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:

[root@ip-10-0-3-146 html]# php -r "echo gethostbyname('db.rtfm.local');"
10.0.3.53

Да, все чудово.

Тепер пробуємо 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 для підключення (рекомендується):

define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);

Або змінити параметр require_secure_transport в RDS (не рекомендується):

Після додавання MYSQLI_CLIENT_SSL установка вже пішла нормально – картинки зараз поправимо:

Фікс CSS та картинок

Проблема виникає через змішаний трафік – до ALB ми ходимо по HTTPS, а між ALB та EC2 маємо простий HTTP.

Для фіксу в wp-config.php додаємо:

$_SERVER['HTTPS'] = 'on';
define('WP_HOME', 'https://test-alb.setevoy.org.ua');
define('WP_SITEURL', 'https://test-alb.setevoy.org.ua');

І блог працює без проблем:

AWS Costs Breakdown: а шо по грошам?

Болюча тема для будь-якого 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 трафіку.

Вартість трафіку в AWS це взагалі окрема тема, тут вже розбирати не буду, але колись писав пост AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS.

Власне, саме з цієї причини варто мати 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 за вхідний і вихідний трафік

А, і це ще не згадував On-Demand, Reserved, Spot інстанси. Але це теж окрема тема, див. Amazon EC2 billing and purchasing options.

А ще окремо оплачуються CPU Credits, $0.05 per vCPU-Hour 🙂 (для RDS теж)

Elastic Load Balancing costs

Див. Elastic Load Balancing pricing.

  • платимо за кону годину роботи ALB
  • платимо за LCU (Load Balancer Capacity Units) – навантаження на ALB, загальна вартість буде залежати від того, скільки ALB опрацював запитів від клієнтів (або під час DDoS :trollface: )
  • платимо за outgoing трафік – але тільки за трафік з ALB, бо трафік між EC2 та ALB в межах одної Availability Zone безкоштовний

Relational Database Service costs

Див. Amazon RDS for MariaDB pricing.

Вже бачили на скріншоті вище – EU-InstanceUsage:db.t3.micro, EU-RDS:GP3-Storage, EU-DataTransfer-In-Bytes, EU-DataTransfer-Out-Bytes.

  • за db.t3.micro в одній Availability Zone платимо $0.018, або $0.43 на добу, або ~13 в місяць
  • CPU Credits для t3 – $0.075 per vCPU-hour
  • Storage: $0.115/GB-month
  • Backup snapshots: безкоштовний в розміні 100% від розміру диска для RDS instance

Route 53 costs

Див. Amazon Route 53 pricing.

Тут платимо за:

  • $0.50 за кожну домену зону
  • і окремо за запити до DNS, але там дуже багато безкоштовних запитів, ще і різні типи – кожну вже окремо описувати не буду

VPC costs

Див. Amazon VPC pricing.

Тут багато свої особливостей – VPC Peering, IPAM, Encryption.

Конкретно в нашому випадку платиться тільки за Public IP (бо у Load Balancer та NAT Gateway власні Elastic IP addresses):

При чому за IP платимо два рази:

  • AllocateAddressVPC: просто за те, що нам дозволили користуватись адресою IPv4
  • AssociateAddressVPC: за прив’язку адреси до інстансу

Перевірити які адреси до чого підключені можна в VPC.

Тут дві адреси для ALB:

І одна для NAT Gateway:

І одна знайшлась unused – а я за неї плачу.

Ну і, власне, це основне по вартості AWS.

Платимо за “кожний чіх”. Втім, як в інших подібних провайдерах.

Loading

FreeBSD: Home NAS, part 15: автоматизація бекапів – скрипти, rsync, rclone
0 (0)

7 Березня 2026

Фактично, це вже остання велика задача – налаштувати автоматичне створення бекапів.

В пості FreeBSD: Home NAS, part 13: планування зберігання даних та бекапів описав загальну ідею детальніше – як і що бекапиться, де, що, як зберігається, а сьогодні – вже суто технічна частина про саму реалізацію.

Про що буде йти мова в цьому пості – як робилась автоматизація збору даних з 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.

Коротко про мою мережу і хости взагалі – детальніше було в пості FreeBSD: Home NAS, part 13:

Архітектура реалізації

Є три системи, які керують даними:

  • Syncthing: синхронізує частину даних з /home/setevoy між ноутбуками, NAS і телефоном (див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing)
  • Rsync: основна “робоча лошадка” для копіювання даних між хостами – збирає з Linux, Raspberry PI, DigitalOcean, та з самого NAS/FreeBSD
  • Rclone: займається синхронізацією даних в клауди

rclone робить sync в Google Drive та Backblaze з опцією --backup-dir – тому навіть якщо Syncthing щось наламає і видалить, а потім ці зміни синхронізуються в клауд – то все одно залишаться копії видалених даних.

І плюс в самому Syncthing для всіх shared директорій включена “Trash Can Versioning”.

Загальна схема виглядає так:

Як писав в попередньому пості – є кілька різних “класів даних” які зберігаються в окремих датасетах, і кожен датасет мапиться на власний rclone remote з власними налаштуваннями шифрування.

Якщо спростити схему і відобразити тільки потік даних – то це виглядає так:

Структура каталогів і файлів

Взагалі, в чорнетці був розписаний весь процес створення “утиліти”, але вирішив вже просто описати фінальне рішення (і то вийшло нічого собі тексту).

Всі операції виконуються кількома shell-скриптами, всі потрібні налаштування – описані в конфіг-файлах.

Структура файлів та каталогів:

root@setevoy-nas:~ # tree -L 3 /opt/nas_backup/
/opt/nas_backup/
├── backup.sh
├── config
│   ├── hosts.conf
│   └── rclone-remotes.conf
├── excludes
│   └── global.exclude
├── includes
│   ├── setevoy-nas
│   │   ├── etc.include
│   │   ├── opt.include
│   │   ├── root.include
│   │   ├── user-home-Data.include
│   │   ├── usr-local-bin.include
│   │   ├── usr-local-etc.include
│   │   └── var.include
│   ├── setevoy-pi
│   │   ├── opt.include
│   │   └── system.include
│   ├── setevoy-work
│   │   ├── user-home-Films.include
│   │   ├── user-home-Media.include
│   │   └── user-home-Vault.include
│   └── template.include
├── validate-config.sh
├── vmbackup-backup.sh
└── web-backup.sh

Скрипти тут:

  • 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:

root@setevoy-nas:~ # cat /opt/nas_backup/includes/setevoy-work/user-home-Media.include

# Syncthing:
# - Books/     => /nas/media
# - Documents/ => /nas/media
# - Music/     => /nas/media
# - Photos/    => /nas/media
# - Pictures   => /nas/media
# - Videos     =>/nas/media
# 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
# - Backups/   => /nas/media

############
### ROOT ###
############

/home/
/home/setevoy/

### Backups ###

/home/setevoy/Backups/
/home/setevoy/Backups/**

...

### Work ###

/home/setevoy/Work/

# <COMPANY_NAME>
/home/setevoy/Work/<COMPANY_NAME>/
/home/setevoy/Work/<COMPANY_NAME>/**

..

Файл exclude – один на всіх із загальними шаблонами того, що треба виключити з даних, які включені в include:

root@setevoy-nas:~ # cat /opt/nas_backup/excludes/global.exclude
######################
### Exclude Global ###
######################

# Syncthing
**/.stversions/

**/.git/
**/logs/
**/log/

# Vim temp files
**/*.swp
**/*.swo
**/*.swx
**/.*.sw?

...

Сам rsync запускається з exclude=all, але про це детальніше буде далі, бо там є свої нюанси.

Каталог config та файли з налаштуваннями

Тут два файли: один для rsynchosts.conf, другий, для rclonerclone-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 розглянемо останнім, аби спочатку подивитись на те, що він запускає, а вже потім – як він це запускає.

Скрипт validate-config.sh: перевірка синтаксису config-файлів

Запускається з 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 розбита на дві окремі перевірки:

  1. спершу перевіряємо, що опція задана саме як delete=, а не просто “yes” чи просто “delete
  2. потім перевіряємо значення після “=“, має бути або саме “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-запит до сервісу:

...
# Alerts configuration (same as backup.sh)
NTFY_TOPIC="my-alerts"
NTFY_URL="https://ntfy.sh/$NTFY_TOPIC"
NTFY_TOKEN_FILE="/root/ntfy.token"
HOSTNAME=$(hostname)

...

NTFY_TOKEN=$(cat "$NTFY_TOKEN_FILE" | tr -d '\n')

send_alert() {
  TITLE="$1"
  MESSAGE="$2"
  TAGS="${3:-warning,backup}"

  curl -s \
    -H "Authorization: Bearer $NTFY_TOKEN" \
    -H "Title: $TITLE" \
    -H "Tags: $TAGS" \
    -d "$MESSAGE" \
    "$NTFY_URL" >/dev/null
}
...

В параметрах vmbackup задаються дві опції:

...
# VictoriaMetrics settings
VM_DATA_PATH="/var/db/victoria-metrics"
VM_SNAPSHOT_URL="http://localhost:8428/snapshot/create"
...

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

root@setevoy-nas:~ # cat /root/.my.cnf.blog-setevoy
[mysqldump]
user=mysql-username
password="mysql-password"
host=localhost

Видалення старих бекапів – аналогічно до попереднього скрипту, просто з find:

...

find "$BACKUP_BASE" -type f \( -name "*.tar.gz" -o -name "*.sql" \) -mtime "+$RETENTION_DAYS" | while read f; do
  echo "Deleting old backup: $f" | tee -a "$LOGFILE"
  # TODO: uncomment when tested
  #rm -f "$f"
  ls -l "$f"
done

...

І самі бекапи виглядають так:

root@setevoy-nas:/opt/nas_backup # tree -L 3 /nas/services/web/
/nas/services/web/
└── setevoy
    ├── databases
        ...
    │   ├── 2026-03-05-03-00-blog-setevoy.sql
    │   ├── 2026-03-06-03-00-blog-setevoy.sql
    │   └── 2026-03-07-03-00-blog-setevoy.sql
    └── files
        ...
        ├── 2026-03-05-03-00-blog-setevoy.tar.gz
        ├── 2026-03-06-03-00-blog-setevoy.tar.gz
        └── 2026-03-07-03-00-blog-setevoy.tar.gz

Скрипт backup.sh

Ну і, нарешті, основний скрипт backup.sh, який, власне, і займається “оркестрацією” всього процесу.

Тут по черзі виконуються всі необхідні дії і запускаються скрипти, про які говорили вище.

Логіка виконання

  1. створюємо lock-файл: корисно, якщо попередній запуск скрипта завис – щоб не запустити одночасно два процеси виконання
  2. скриптом validate-config.sh виконується перевірка файлів hosts.conf та rclone-remotes.conf
  3. по черзі запускаємо скрипти бекапів:
    1. з web-backup.sh – бекапиться WordPress
    2. з vmbackup-backup.sh – бекапиться VictoriaMetrics
  4. далі читаємо конфіг hosts.conf для rsync, для кожного хоста визначаємо потрібні параметри, і в циклі для кожного хоста:
    1. виконуємо rsync – спершу з --dry-run, потім вже реальний запуск
    2. якщо rsync виконався без помилок – то створюємо ZFS снапшот
  5. вже не в циклах – видаляємо старі ZFS снапшоти
  6. читаємо конфіг rclone-remotes.conf для rclone
    1. в циклі запускаємо rclone sync для кожного заданого в конфігу ZFS dataset та відповідного rclone remote
  7. і в кінці з 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.

В hosts.conf вона задається в кінці строки:

...
work.setevoy|setevoy|setevoy-work/user-home-Media.include|global.exclude|/nas/media/|delete=yes
...

В самому 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 при кожній ітерації видалить дані іншої строки з конфігу.

Ось приклад з 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

# '/etc/systemd/' => '/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/etc/'
pi.setevoy|root|setevoy-pi/system.include|global.exclude|/nas/systems/setevoy-pi/raspberry-pi-cm4-rev11/|delete=yes
...

І тут rsync:

  • візьме все, що дозволено в opt.include
  • скопіює в /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

Формат файлів –include-from та –exclude-from

Exclude-файл поки що один глобальний:

######################
### Exclude Global ###
######################

# Syncthing
**/.stversions/

**/.git/
**/logs/
**/log/

# Vim temp files
**/*.swp
**/*.swo
**/*.swx
**/.*.sw?

**/node_modules/

# Python
**/.venv/
**/venv/
**/__pycache__/

...

Тут через “**” вказуємо “без різниці, де саме цей файл чи каталог буде знайдено“, тобто виключаємо і /root/some-dir/.git/ – і /home/setevoy/some-dir/.git/.

Приклад одного з include-файлів – тут трохи цікавіше:

############
### ROOT ###
############

/home/
/home/setevoy/

### Books ###

/home/setevoy/Books/
/home/setevoy/Books/**

### Backups ###

/home/setevoy/Backups/
/home/setevoy/Backups/**

### Downloads ###

/home/setevoy/Downloads/
/home/setevoy/Downloads/Books/
/home/setevoy/Downloads/Books/**

...

Так як 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

Запускається скрипт з crontab:

root@setevoy-nas:~ # crontab -l
...
0 3 * * * /opt/nas_backup/backup.sh

Приклад результату виконання

Як це все щастя виглядає в лог-файлі та повідомлення 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 про те, скільки байт передано, скільки місця на диску було витрачено або звільнено – щось таке

Все.

Поки працює, як є – вже кілька тижнів, поки що без проблем.

Loading

FreeBSD: Home NAS, part 14 – логи з VictoriaLogs і алерти з VMAlert
0 (0)

28 Лютого 2026

Продовження серії по налаштуванню домашнього NAS.

Моніторинг в цілому вже налаштований в попередніх частинах, але залишилось налаштувати роботу з логами – бо робити це в консолі з tail -f /var/log/messages, звісно, можна – але є і більш зручні інструменти.

Використаємо VictoriaLogs – тим більш для метрик на моїй FreeBSD вже є стек VictoriaMetrics + VMAlert + Alertmanager.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Установка VictoriaLogs

Є в репозиторії, просто встановлюємо з pkg:

root@setevoy-nas:~ # pkg install -y victoria-logs

Глянемо, які файлу додасть в систему:

root@setevoy-nas:~ # pkg info -l victoria-logs
victoria-logs-1.43.1_2:
  /usr/local/bin/victoria-logs
  /usr/local/bin/vlogscli
  /usr/local/etc/rc.d/victoria-logs
...

Глянемо, що в скрипті rc.d:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/victoria-logs
...
rcvar="victoria_logs_enable"
...
victoria_logs_user="victoria-logs"
...

Додаємо до /etc/rc.conf:

root@setevoy-nas:~ # sysrc victoria_logs_enable="YES"
victoria_logs_enable:  -> YES

Запускаємо сервіс:

root@setevoy-nas:~ # service victoria-logs start

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep logs
victoria-logs victoria-l 33088 5  tcp4 *:9428              *:*

Відкриваємо в браузері на порту 9428:

Поки тут пусто – додаємо збір логів.

Установка Fluent Bit

Хотів взяти Vector.dev – але його нема в репозиторії і портах FreeBSD, і нема навіть в списку підтримуваних систем.

Є відкрита GitHub issue – ще у 2020 році.

Що є з інших рішень:

  • Promtail: від Grafana – не хочу, і вони його наче вже депрікейтять
  • Filebeat: від Elastic, на Go – але пам’ятаю, що трохи важкуватий по ресурсам
  • Fluent Bit: на C, швидкий, легкий, хоча конфіг може показатись незручним
  • Logstash: Java – nuff said
  • Rsyslog: ну от де конфіг справді незручний, тому ні (див. rsyslog: добавление наблюдения за файлом в конфигурацию – 2014 рік)

Отже, візьмемо Fluent Bit.

Глянемо, чи є в репозиторії:

root@setevoy-nas:~ # pkg search fluent
fluent-bit-4.2.2_2             Fast and lightweight data forwarder

Встановлюємо:

root@setevoy-nas:~ # pkg install -y fluent-bit

Перевіряємо, які файли додає в систему:

root@setevoy-nas:~ # pkg info -l fluent-bit | grep etc
  /usr/local/etc/fluent-bit/fluent-bit.conf.sample
  /usr/local/etc/fluent-bit/parsers.conf.sample
  /usr/local/etc/fluent-bit/plugins.conf
  /usr/local/etc/rc.d/fluent-bit

Дефолтний конфіг /usr/local/etc/fluent-bit/fluent-bit.conf.

Перевіряємо, як запускається:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/fluent-bit
...
# fluent_bit_enable (bool):	Set to YES to enable fluent-bit
# 				Default: NO
# fluent_bit_config (str):	config files to use
#				Default: /usr/local/etc/fluent-bit/fluent-bit.conf
# fluent_bit_flags (str):	Extra flags passed to fluent-bit
# fluent_bit_user (str):	Default run as user nobody
# fluent_bit_group (str):	Default run as group nogroup
...

: ${fluent_bit_enable:="NO"}
: ${fluent_bit_user:="nobody"}
: ${fluent_bit_group:="nogroup"}
: ${fluent_bit_config:="/usr/local/etc/fluent-bit/fluent-bit.conf"}

pidfile=/var/run/${name}.pid
procname="/usr/local/bin/fluent-bit"
command="/usr/sbin/daemon"
command_args="-H -p ${pidfile} -o /var/log/${name}/${name}.log -t ${name} ${procname} --quiet --config ${fluent_bit_config} ${fluent_bit_flags}"
...

Що нам треба буде – додати fluent_bit_enable в /etc/rc.conf. І звертаємо увагу на fluent_bit_user та fluent_bit_group.

Створюємо каталог для його бази – fluent-bit буде сюди записувати позиції в файлах логів:

root@setevoy-nas:~ # mkdir -p /var/db/fluent-bit
root@setevoy-nas:~ # chown nobody:nogroup /var/db/fluent-bit/

Видаляємо (переносимо) дефолтний конфіг:

root@setevoy-nas:~ # mv /usr/local/etc/fluent-bit/fluent-bit.conf /usr/local/etc/fluent-bit/fluent-bit.conf-default

Пишемо свій файл /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:~ # vim /usr/local/etc/fluent-bit/fluent-bit.conf ^C
root@setevoy-nas:~ # fluent-bit -c /usr/local/etc/fluent-bit/fluent-bit.conf
Fluent Bit v4.2.2
* Copyright (C) 2015-2025 The Fluent Bit Authors
* Fluent Bit is a CNCF graduated project under the Fluent organization
* https://fluentbit.io

______ _                  _    ______ _ _             ___   _____
|  ___| |                | |   | ___ (_) |           /   | / __  \
| |_  | |_   _  ___ _ __ | |_  | |_/ /_| |_  __   __/ /| | `' / /'
|  _| | | | | |/ _ \ '_ \| __| | ___ \ | __| \ \ / / /_| |   / /
| |   | | |_| |  __/ | | | |_  | |_/ / | |_   \ V /\___  |_./ /___
\_|   |_|\__,_|\___|_| |_|\__| \____/|_|\__|   \_/     |_(_)_____/

             Fluent Bit v4.2 – Direct Routes Ahead
         Celebrating 10 Years of Open, Fluent Innovation!
...
[2026/02/28 16:38:18.797199771] [ info] [output:loki:loki.0] configured, hostname=localhost:9428
...

Записуємо повідомлення в /var/log/messages:

root@setevoy-nas:~ # logger "test message from fluent-bit"

Перевіряємо ім’я:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/fluent-bit | grep name
name="fluent_bit"
rcvar=${name}_enable
...

Додаємо в автостарт:

root@setevoy-nas:~ # sysrc fluent_bit_enable="YES"

Запускаємо:

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
;>

І, наприклад, запустити \tail:

;> \tail *;
executing [*]...; duration: client 9.003s
{
  "_msg": "Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli",
  "_stream": "{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}",
  "_stream_id": "0000000000000000782dd9afdaaf4d53bfb843de46a3d91b",
  "_time": "2026-02-28T15:17:59.731198268Z",
  "host": "setevoy-nas",
  "job": "fluent-bit",
  "logfile": "messages"
}

Або використовувати різні LogsQL filters та pipes, наприклад – Time filter:

;> _time:5m;
executing [_time:5m]...; duration: server 0.000s
{
  "_msg": "Feb 28 17:17:59 setevoy-nas setevoy[38684]: test message for vlogscli",
  "_stream": "{host=\"setevoy-nas\",job=\"fluent-bit\",logfile=\"messages\"}",
  "_stream_id": "0000000000000000782dd9afdaaf4d53bfb843de46a3d91b",
  "_time": "2026-02-28T15:17:59.731198268Z",
  "host": "setevoy-nas",
  "job": "fluent-bit",
  "logfile": "messages"
}

Або включити compact mode:

;> \c
compact output mode

І тоді результат буде таким:

vmalert та алерти з логів

Для vmalert можна створити Recodring Rules – читати логи, генерувати метрики, а потім з цих метрик або можемо малювати графіки в Grafana – або створювати алерти.

Див. VictoriaLogs: створення Recording Rules з VMAlert.

Але для цього vmalert треба робити запити до двох datasources:

  • до VictoriaLogs на порт 9428 і URI /select/logsql/ – аби прочитати логи
  • до VictoriaMetrics на порт 8428 – аби записати метрики і виконати запити для створення алерту

Але два --datasource.url для vmalert задати не можна – але можна зробити базовий роутинг через vmauth, як я робив на робочому проекті, де в мене це все працює в Kubernetes – а потім для vmalert в --datasource.url вказати адресу vmauth.

Див. VictoriaMetrics: VMAuth – проксі, аутентифиікація та авторизація.

Налаштування vmauth

vmauth в мене вже встановлена з пакету vmutils, зараз треба просто додати конфіг з роутами та rc.d скрипт, бо в комплекті vmutils його нема:

root@setevoy-nas:~ # pkg info -l vmutils | grep vmauth
  /usr/local/bin/vmauth
  /usr/local/share/doc/vmutils/vmauth.md
  /usr/local/share/doc/vmutils/vmauth_flags.md

Створюємо конфіг /usr/local/etc/vmauth.yml з двома роутами – для VictoriaLogs та VictoriaMetrics:

unauthorized_user:
  url_map:
    - src_paths:
        - "/select/logsql/.*"
      url_prefix: "http://127.0.0.1:9428"
    - src_paths:
        - "/.*"
      url_prefix: "http://127.0.0.1:8428"

Пишемо rc.d скрипт – /usr/local/etc/rc.d/vmauth:

#!/bin/sh

# PROVIDE: vmauth
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="vmauth"
rcvar="vmauth_enable"

load_rc_config $name

: ${vmauth_enable:="NO"}
: ${vmauth_user:="victoria-metrics"}
: ${vmauth_logfile:="/var/log/vmauth.log"}
: ${vmauth_args:="-auth.config=/usr/local/etc/vmauth.yml -httpListenAddr=:8427"}

pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
procname="/usr/local/bin/vmauth"

command_args="-f -o ${vmauth_logfile} -p ${pidfile} ${procname} ${vmauth_args}"

start_cmd="vmauth_start"
stop_cmd="vmauth_stop"

vmauth_start()
{
  echo "Starting vmauth"
  touch ${vmauth_logfile}
  chown ${vmauth_user} ${vmauth_logfile}
  ${command} ${command_args}
}

vmauth_stop()
{
  echo "Stopping vmauth"
  kill `cat ${pidfile}`
}

run_rc_command "$1"

Задаємо execution права:

root@setevoy-nas:~ # chmod +x /usr/local/etc/rc.d/vmauth

Додаємо запуск в /etc/rc.conf:

root@setevoy-nas:~ # sysrc vmauth_enable="YES"
vmauth_enable:  -> YES

Запускаємо:

root@setevoy-nas:~ # service vmauth start
Starting vmauth

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep vmauth
root     vmauth     42277 4   tcp4   *:8427                *:*

Додаємо vmalert_args:

root@setevoy-nas:~ # sysrc vmalert_args="--datasource.url=http://127.0.0.1:8427 --notifier.url=http://127.0.0.1:9093 --rule=/usr/local/etc/vmalert/*.yml --remoteWrite.url=http://127.0.0.1:8428"

Тут:

  • --datasource.url=http://127.0.0.1:8427: адреса vmauth, який буде роутити запити по URI до VictoriaLogs або VictoriaMetrics
  • --notifier.url=http://127.0.0.1:9093: адреса Alertmanager
  • --remoteWrite.url=http://127.0.0.1:8428: адреса VictoriaMetrics, в яку будемо писати згенеровані метрики

Створення vmalert Recording Rule та алерту

І приклад метрики та алерту – файл /usr/local/etc/vmalert/freebsd-system-alerts.yml:

groups:

  - name: freebsd-logs-records
    type: vlogs
    interval: 1m
    rules:
      - record: freebsd:messages:errors_per_minute
        expr: 'error | stats count() as errors_count'

  - name: freebsd-logs-alerts
    rules:
      - alert: FreeBSDTooManyErrors
        expr: freebsd:messages:errors_per_minute > 1
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Too many errors in logs"

Перезапускаємо vmalert:

root@setevoy-nas:~ # service vmalert restart
Stopping vmalert
Starting vmalert

Запускаємо для тесту запис “error” до /var/log/messages:

root@setevoy-nas:~ # while true; do logger "error test message"; sleep 1; done

Перевіряємо в VictoriaMetircs:

Перевіряємо в VMAlert:

Отримуємо алерт в Alertmanager:

І алерт в Telegram (бот робився для алертів EcoFlow, тому ім’я таке):

Готово.

Loading

FreeBSD: Home NAS, part 13: планування зберігання даних та бекапів
0 (0)

28 Лютого 2026

Коли тільки починав робити свій NAS і думав про бекапи, то все здавалось доволі простим: є робочий ноутбук з даними, є сервер з FreeBSD під NAS – треба просто взяти, і скопіювати дані.

Тому перша задумка була мати backup-скрипт/и на Linux-хостах, які б з rsync заливали дані на NAS, а потім з NAS іншим скриптом заливати дані до Rclone remotes.

Але…

Але коли почав вже робити, то виникло питання:

  • rsync з хоста setevoy-work заливає дані на NAS
  • в цей жеж час rsync з хоста setevoy-home заливає свої дані

А коли запускати rclone => Google Drive? Як знати, що всі дані з хостів вже готові для копіювання?

І це була тільки верхівка айсбергу задачі по організації процесу.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

“Технічне завдання”: складність організації даних та бекапів

Отже, виявилось, що тут є купа нюансів:

  • хостів, з яких треба робити бекапи – не один, і не два, і навіть не три:
    • є робочий ноутбук з Arch Linux
    • є домашній ноутбук з Arch Linux
    • є ігровий ПК, на якому Windows для ігор і Arch Linux для роботи – бо раніше (до “сезону блекаутів”) це був мій робочий комп
    • є власне сервер з FreeBSD під NAS
    • нещодавно з’явився Raspberry Pi (див. Raspberry Pi: перший досвід і установка Raspberry Pi OS Lite)
    • а ще і телефон з власними даними типу фоток
  • на кожному хості є різні типи даних:
    • відносно статичні та однакові дані для ноутбуків/ПК – фото, відео, документи
    • часто змінюються, але в цілому однакові на ноутбуках/ПК, хоча можуть дещо відрізнятись – робочі дані по проектам, якісь власні скрипти
    • конфіденційні дані, але однакові на всіх хостах – 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.

Про MikroTik писав в пості MikroTik: перше знайомство та Getting Started, про WireGuard на ньому – MikroTik: налаштування WireGuard та підключення Linux peers, і там жеж є більш детальна схема мережі.

Отже, питання мережі вирішено – тепер треба з усіх цих хостів почати збирати дані і якось продумати організацію їхнього збереження на 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
      • синхронізуються між NAS, setevoy-work та setevoy-home з Syncthing (див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing)
    • Shared Dynamic Unencrypted Data:
      • дані на всіх хоста – однакові або з невеликими відмінностями
      • часто оновлюються та/або можуть мати багато “мусору” типу каталогів .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):
    • ZFS dataset nas/services

Всі ZFS datasets на NAS зараз виглядають так:

root@setevoy-nas:~ # zfs list
NAME                 USED  AVAIL  REFER  MOUNTPOINT
nas                 2.24T  1.27T   128K  /nas
nas/backups-manual   593G  1.27T   593G  /nas/backups-manual
nas/jellyfin        9.20G  1.27T  9.20G  /nas/jellyfin
nas/media            369G  1.27T   341G  /nas/media
nas/mobile          52.3G  1.27T  52.3G  /nas/mobile
nas/private          208G  1.27T   208G  /nas/private
nas/services         133G  1.27T   133G  /nas/services
nas/systems         4.82G  1.27T  3.78G  /nas/systems
nas/to-sort          560G  1.27T   560G  /nas/to-sort
nas/vault           56.2G  1.27T  56.2G  /nas/vault

Тут є кілька додаткових датасетів, які не пов’язані бекапами:

  • /nas/backups-manual: просто окремі ручні бекапи з хостів або з самого NAS, коли роблю якісь потенційно небезпечні операції з даними
  • /nas/jellyfin: фільми для Jellyfin (про нього є чорнетка, може якось допишу) – це чисто фільми-серіали, тому в бекапи не включено і зберігається окремо
  • /nas/mobile: датасет для даних з мобільного телефона, які копіюються сюди з Syncthing на телефоні
  • /nas/to-sort: копії даних зі старих компів, які треба перебрати та включити в загальні бекапи

Приклад організації даних системних бекапів – все по окремим каталогам:

root@setevoy-nas:~ # tree -d -L 3 /nas/systems/
/nas/systems/
├── setevoy-nas
│   └── thinkcentre-10SUSCF000
│       ├── boot
│       ├── etc
│       ├── home
│       ├── opt
│       ├── root
│       ├── usr
│       └── var
├── setevoy-pi
│   └── raspberry-pi-cm4-rev11
│       ├── etc
│       └── opt
└── setevoy-work
    └── thinkpad-t14-g5-21ML003URA
        ├── etc
        ├── home
        ├── root
        ├── usr
        └── var

І дані сервісів:

root@setevoy-nas:~ # tree -d -L 3 /nas/services/
/nas/services/
├── victoriametrics
│   ├── 20260222
│   │   ├── data
│   │   ├── indexdb
│   │   └── metadata
│   └── latest
│       ├── data
│       ├── indexdb
│       └── metadata
└── web
    └── setevoy
        ├── databases
        └── files

База VictoriaMetrics бекапиться з vmbackup, дані в web – скриптом, який створює tar.gz з файлами та запускає mysqldump для бекапів баз даних.

Дані на диску виглядають так:

root@setevoy-nas:~ # tree -L 4 /nas/services/
/nas/services/
├── victoriametrics
│   ├── 20260222
│   │   ├── backup_complete.ignore
│   │   ├── backup_metadata.ignore
│   │   ├── data
│   │   │   ├── indexdb
│   │   │   └── small
│   │   ├── indexdb
│   │   │   └── 1882B20388AAC712
│   │   └── metadata
│   │       └── minTimestampForCompositeIndex
│   └── latest
│       ├── backup_complete.ignore
│       ├── backup_metadata.ignore
│       ├── data
│       │   ├── indexdb
│       │   └── small
│       ├── indexdb
│       │   └── 1882B20388AAC712
│       └── metadata
│           └── minTimestampForCompositeIndex
└── web
    └── setevoy
        ├── databases
        │   ├── 2026-02-19-18-05-blog-setevoy.sql
        │   ├── 2026-02-21-18-47-blog-setevoy.sql
        │   ├── 2026-02-22-00-00-blog-setevoy.sql
        ...
        └── files
            ├── 2026-02-19-18-05-blog-setevoy.tar.gz
            ├── 2026-02-21-18-47-blog-setevoy.tar.gz
            ├── 2026-02-22-00-00-blog-setevoy.tar.gz
        ...
...

А дані в /nas/media – просто всі в одному каталозі:

root@setevoy-nas:~ # tree -L 3 /nas/media/
/nas/media/
└── home
    └── setevoy
        ├── Backups
        ├── Books
        ├── Documents
        ├── Downloads
        ├── Dropbox
        ├── Music
        ├── Opt
        ├── Photos
        ├── Pictures
        ├── Projects
        ├── VMs
        ├── Videos
        └── Work

Rclone remotes та пов’язані ZFS datasets

Для Rclone створені remotes під кожен датасет, з якого треба бекапити дані в клауди:

root@setevoy-nas:~ # rclone listremotes
nas-aws-s3-setevoy-backups-root:
nas-google-drive-total-root:
nas-google-drive-root:
nas-google-drive-media:
nas-google-drive-mobile:
nas-google-drive-crypted-systems:
nas-google-drive-crypted-services:
nas-google-drive-crypted-vault:
nas-google-drive-crypted-private:
nas-backblaze-root-media:
nas-backblaze-crypted-media:
nas-backblaze-root-mobile:
nas-backblaze-crypted-mobile:
nas-backblaze-root-systems:
nas-backblaze-crypted-systems:
nas-backblaze-root-services:
nas-backblaze-crypted-services:
nas-backblaze-root-vault:
nas-backblaze-crypted-vault:
nas-backblaze-root-private:
nas-backblaze-crypted-private:

Кожен remote “мапиться” на ZFS datasets:

  • 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 – з повним описом того, як це все будувалось, які сервіси, як все це моніториться, та як виглядає:

Loading

ilert: альтернатива Opsgenie – перше знайомство, Alertmanager, Slack
5 (1)

24 Лютого 2026

Думаю, всі юзери 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!) документації.

Все!

Поїхали.

Посилань на документацію ilert буде багато, почати можна з Opsgenie to ilert Migration Guide або VictoriaMetrics Integration.

ilert overview та основні можливості

Після реєстрації попадаємо в дашборд – і ви просто зацініть, як тут все класно виглядає – просто, функціонально, зрозуміло:

Основні можливості:

  • є Terraform provider
    • є експорт конфігурації відразу в Terraform
  • алертинг – SMS, Voice calls, Slack, Telegram, web-hooks і упасібоже MS Teams
  • стандартний On-call management  – ротації, розклад, ескалації
  • ChatOps – управління алертами зі Slack
  • AI SRE – ще не трогав, але спробую, хоча це наче ще в Beta
  • postmortems, incidents
  • REST API
  • десь бачив можливість збирати метрики з Prometheus/VictoriaMetrics, але це ще тестив
  • MCP для LLM
  • мобільна апка (теж ще не дивився)
  • Status Pages

Ну і овер 100 інтеграцій – All Integrations.

Pricing

Див. Pricing.

Є Free Plan, в який до того ж входить Heaerbeat, є Status Page, On-call/voice/SMS – з обмеженнями, але включено.

Є в дашборді:

Початок роботи

Основне – це, звісно, алерти, тому глянемо що тут є.

Головні концепти ilert по алертам – це Alert sources та Alert actions:

  • Alert sources: власне, джерела алертів – Alertmanager, AWS SNS, etc
  • Alert actions: правила того, що з алертами робити – відправити повідомлення, оновити статус Status Pages, пушнути webhook

З чого почнемо, і що мені треба з основного:

  • почати приймати алерти від Alertmanager
  • налаштувати відправку в Slack
    • подивитись на роутинг алертів – аби в Slack відправляти по різним каналам
    • подивитись на шаблони повідомлень

Підключення Alertmanager

Переходимо в Alert sources, додаємо новий:

Вибираємо Prometheus – це, власне, і буде Alertmanager:

Документація – Prometheus Integration, і взагалі посилання на документацію є майже всюди, та і сама документація чудова.

Задаємо ім’я:

Задаємо Esclation Policy – далі трохи про них ще поговоримо:

Grouping – none, в мене цим займається Alertmanager:

Тут жеж можна налаштувати Templaing, але поки не робимо – далі буде трохи детальніше:

Тут жеж можна задати фільтри – при яких умовах алерт буде відправлений сюди, але поки теж не включаємо – теж буде трохи далі:

Клікаємо внизу Finish setup, отримуємо ключ і повний URL:

Налаштування Alertmanager

Переходимо до конфіга Alertmanager, додаємо новий роут:

...
      routes:

        - matchers:
            - component="devops"
          receiver: ilert-notifications
          continue: true
...

Та reciever:

...
    receivers:

      - name: 'ilert-notifications'
        webhook_configs:
        - url: 'https://api.ilert.com/api/v1/events/prometheus/il1prom***c94'
...

Можна відправити тестовий алерт з curl:

$ curl -X POST https://api.ilert.com/api/v1/events/prometheus/*** -H "Content-Type: application/json" -d '{"receiver":"ilert-default","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"Test","severity":"warning"},"annotations":{"summary":"Test alert"},"startsAt":"2026-02-24T00:00:00Z","endsAt":"0001-01-01T00:00:00Z","fingerprint":"test123"}],"groupLabels":{"alertname":"Test"},"commonLabels":{"alertname":"Test","severity":"warning"},"commonAnnotations":{"summary":"Test alert"},"externalURL":"http://localhost:9093","version":"4","groupKey":"test"}'

І дуже корисні логи:

В яких буде видно весь payload, і як його розпарсив ilert:

Ну і простий і дуже зручний UI з алертами:

Налаштування алертів в Slack

Переходимо в Alert Actions, додаємо новий:

Вибираємо Slack, не включаємо галочку “Use webhook“:

Підключаємо Alert source, який буде сюди слати алерти, вибираємо на які події слати повідомлення:

Ну і фільтри – але поки пропускаємо:

Задаємо ім’я Action, вибираємо канал, в який будуть йти повідомлення:

Можна тицнути Test:

І посміятись 🙂

Чекаємо на алерт з Alertmanager – і ви просто подивіться на цю красу з коробки!

Всі Connectors трохи сховали – шукаємо в Settings:

Роутинг повідомлень в Slack channels

Тепер, як загальний алертинг працює – починаємо тюнити.

Перше, що треба – це роутинг алертів:

  • є кілька оточень – dev, staging, prod, ops
  • є різні команди – devops, backend, web, data

Для кожної команди у нас є окремі Slack channels – #alerts-backend-prod, #alerts-devops-ops і так далі.

Що треба зробити – щоб алерти Backend Prod йшли в канал #alerts-backend-prod, а алерти для девопсів, відповідно, в канал #alerts-devops-ops.

Для Opsgenie це в мене було реалізовано через роутинг в самому Opsgenie:

А labels задаються в алертах:

...
      - alert: Kubernetes Pod UnHealthy
        expr: k8s:pod:unhealthy{namespace="prod-backend-api-ns"} > 0
        for: 15m
        labels:
          severity: warning
          component: backend
          environment: prod
...

І вже по ним Opsgenie  вибирає через яку Slack-інтеграцію слати алерт.

Власне, в ilert можна зробити таким самим чином, бо аналогічно до Opsgenie – кожен Slack Connector прив’язується до конкретного каналу.

А тому:

  • маємо Alert Source: Alertmanager
  • для нього створюємо кілька Alert actions:
    • slack-alerts-backend-prod: використовує Slack connector, налаштований на #alerts-backend-prod
    • slack-alerts-devops-ops: використовує Slack connector, налаштований на #alerts-devops-ops

Ще є дуже прикольна штука з динамічними роутами через Escalation Policy – далі подивимось.

Static alert rounting

Спершу дивимось, в якому вигляді ilert отримує алерт – переходимо в Alert source > Alert logs, відкриваємо якийсь алерт:

Далі переходимо в Alert actions і додаємо новий (чи редагуємо старий) Action.

Тут все аналогічно до того, як підключали Slack вище – але тепер включаємо Conditional execution і задаємо умову:

Або пишемо кодом – див. ICL – ilert condition language:

(alert.labels.component in ["devops"])

Задаємо канал:

Фільтр готовий:

Тепер алерти з label component="devops" підуть в канал #alerts-devops-ops.

Dynamic alert routing

Інший прикольний варіант – через Dynamic escalation policy routing.

Суть його в тому, що створюється кілька 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 }}:

Тепер:

  • Alert source розпарсить {{ alerts[0].labels.routing }}
  • отримає значення “devops
  • по цьому значенню динамічно підключить 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: Route Test => alerts-devops-ops (alertname) 5
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-devops-ops
        annotations:
          summary: "Route Test (summary) 5"
          description:

      - alert: Route Test => alerts-backend-prod (alertname) 5
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-backend-prod
        annotations:
          summary: "Route Test (summary) 5"
          description:

...

І отримуємо алерти в різні канали.

Девопси:

Бекенди:

Блін – я в захваті від системи 🙂

Slack messages templating

Документація – Alert template.

Тут, в принципі, все доволі просто – задаємо поля з alert payload, можемо використовувати різні Functions – наприклад, [{{ commonLabels.severity.upperCase() }}].

В полях маленька кнопочка “play” справа внизу дозволяє відразу перевірити, як шаблон буде працювати:

Додамо новий алерт:

...
      - alert: Template Test (alertname) 1
        expr: sum(kube_pod_info{namespace="ops-monitoring-ns", pod=~".*grafana.*"}) by (cluster, namespace, pod) >= 0
        for: 1s
        labels:
          severity: warning
          component: devops
          environment: ops
          routing: alerts-devops-ops
        annotations:
          summary: "Template Test (summary) 1"
          description: "Template Test (description)"
          grafana_pod_overview_url: 'https://{{ .Values.monitoring.root_url }}/d/kubernetes-pod-overview/kubernetes-pod-overview?orgId=1&var-namespace={{ "{{" }} $labels.involvedObject_namespace }}&var-pod={{ "{{" }} $labels.pod }}'
...

Результат:

Heartbeat monitors

Heartbeat monitors – теж прикольна штука, але тут, в принципі, все просто – створюємо Alert source, отримуємо URL, і періодично до нього шлемо запит.

Як тільки пропустили відправку сигналу – спрацьовує алерт.

Пробуємо з curl:

$ curl -v https://beat.ilert.com/api/pings/ih2***
* Host beat.ilert.com:443 was resolved.
...
> GET /api/pings/ih2:25*** HTTP/1.1
...

І через заданий таймаут він стане Expired, і спрацює Alert action.

Deployment events – не тестив, але виглядає цікаво.

Summary або загальні враження

Просто система алертів, якою вона має бути.

Чим мені сподобався Backblaze – це простим і інтуїтивно зрозумілим інтерфейсом (див. Backblaze: знайомство з B2 Cloud Storage – перші враження).

Чому я просто закохався в ilert – простий інтерфейс, інтуїтивно зрозумілий інтерфейс і чудова документація.

Можливо, ще випливуть якісь проблеми вже під час реального використання, але після першого дня налаштувань – враження виключно позивні.

Loading

Backblaze: знайомство з B2 Cloud Storage – перші враження
5 (1)

23 Лютого 2026

По факту, цей пост – частина серій по сетапу домашнього NAS на FreeBSD (див. початок тут – FreeBSD: Home NAS, part 1 – налаштування ZFS mirror), але винесу його окремо.

В мене вже налаштована автоматизація бекапів (про неї теж будуть пости), і зараз дані з NAS раз на добу заливаються до Google Drive.

Але, по-перше – з сервісів Google я потроху позбавляюся, по-друге – хочеться мати друге місце для бекапів в клауді, а не тримати всі яйки в одній корзині.

Взагалі планував другим сторейджем взяти AWS S3, але потім подивився на альтернативи та відкрив для себе Backblaze, який мені настільки сподобався, що я прямо в той жеж день оформив підписку і налаштував Rclone на копіювання Backblaze теж.

Про Rclone детальніше писав у FreeBSD: Home NAS, part 9 – backup даних з rclone до AWS S3 та Google Drive, сьогодні тут теж про нього згадаємо, ну і познайомимось з Backblaze в цілому – подивимось на можливості, прайсінг, налаштуємо Rclone remotes та глянемо на швидкість.

Backblaze overview

Загальна документація – Backblaze Documentation, та по Cloud Storage – Get Started with the Web Console.

У 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 – дуже великий безкоштовний ліміт
  • підтримка базових lifecycle політик
  • є офіційний мобільний клієнт – дуже простенький

Створення тестової корзини

Реєстрація проста, через пошту, після логіну бачимо чудову мінімалістичну дашборду:

Створимо тестовий бакет – звертаємо увагу на попередження про шифрування:

Тобто, як писав вище – не буде можливості створювати снапшоти.

Хоча трохи незрозуміло чому, бо в тому ж попередженні пишуть, що “Backblaze creates, manages key“.

Але для мене це в будь-якому випадку не дуже актуально, бо шифруванням буде займатись Rclone.

Документація по шифруванню – Enable Encryption on a Bucket.

Завершуємо створення бакету – буквально пара кліків:

Створення Application key

В моєму кейсі працювати з Backblaze буде Rclone, тому для нього створимо окремий ключик, див. Create and Manage App Keys:

Задаємо ліміт на конкретну корзину:

Відразу зберігаємо, бо більше його не побачимо:

Налаштування rclone B2 remote

Документація Rclone – Backblaze B2.

Запускаємо rclone config, створюємо новий remote:

$ 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.

Перевіряємо як новий ремоут спрацює – створюємо файл, копіюємо в бакет:

[setevoy@setevoy-work ~]  $ echo "test" > /tmp/test-b2.txt
[setevoy@setevoy-work ~]  $ rclone copy  /tmp/test-b2.txt setevoy-backblaze-testing:setevoy-test-1

[setevoy@setevoy-work ~]  $ rclone ls setevoy-backblaze-testing:setevoy-test-1
        5 test-b2.txt

І він в 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 ***

...

Копіюємо тестовий файл в цей ремоут:

$ rclone copy  /tmp/test-b2.txt setevoy-backblaze-testing-crypted

І маємо цей жеж файл, але вже як .bin:

Реальні дані та помилка “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.

Дательніше див. Backblaze Product and Pricing Updates.

Крім того, оплачується частина 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-викликів на день.

Див. Backblaze B2 Cloud Storage Frequent Questions.

Для моєї схеми найбільше буде 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:

Ну а результат завантаження взагалі всього мого бекапу виглядає так:

В цілому враження – принаймні поки що – чудові.

Подивимось, як це буде працювати.

Loading

MikroTik: налаштування WireGuard та підключення Linux peers
5 (1)

17 Лютого 2026

Ще одна з (багатьох) приємних можливостей MikroTik – вбудована підтримка WireGuard (хоча вона є навіть на дешевому TP-Link Archer).

В моєму сетапі MikroTik RB4011 грає роль такого собі “VPN Hub” – всі клієнти підключаються до нього і об’єднуються в єдину мережу, і роль VPN трохи перебільшена тут дійсно важлива – бо це такий собі gateway, через які всі хости комунікують один з одним, і саме через VPN-тунелі з NAS мій скрипт для створення бекапів (про автоматизацію бекапів буде окремим великим постом, вже є в чернетках) підключається до всіх хостів, аби запустити rsync і стягнути до себе дані.

Крім того, Syncthing теж працює виключно в межах внутрішніх мереж і синхронізує дані між FreeBSD/NAS, ноутбуках з Arch Linux та мобільним телефоном – див. FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing.

Власне сьогодні – налаштування WireGuard на самому MikroTik та на Debian (сервер rtfm.co.ua).

На Arch Linux (домашній ноутбук) все аналогічно до Debian, а для телефона є офіційний клієнт WireGuard, і там все в принципі схоже.

Див. також перший пост по MikroTik – MikroTik: перше знайомство та Getting Started, і далі буде ще, мабуть, ціла серія – вже пачка чернеток є.

Що будемо робити:

  • на MikroTik створимо інтерфейс для WireGuard, назначимо йому IP
  • налаштуємо MikroTik Firewall для трафіку між всіма хостами
  • налаштуємо роути – додамо в домашню мережу
  • створимо WireGuard ключі, додамо WireGuard Peer на MikroTik

Хоча пост вийшов довгий – але насправді все доволі просто.

Головне не плутати приватні і публічні ключі в конфігах – в мене на початку з цим було трохи складностей.

Архітектура мереж і хостів

Схематично вся мережа виглядає так:

Тут:

  • 192.168.0.0/24: мережа офісу, основні хости тут:
    • 192.168.0.1: MikroTik RB4011 – основний роутер
    • 192.168.0.2: FreeBSD з NAS
    • 192.168.0.3: робочий ноутбук з Arch Linux
  • 192.168.100.0/24: домашня мережа
    • 192.168.100.100: домашній ноутбук
  • 10.100.0.0/24: WireGuard VPN
    • 10.100.0.1: MikroTik RB4011
    • 10.100.0.3: домашній ноутбук з Arch Linux
    • 10.100.0.10: сервер rtfm.co.ua в DigitalOcean з Debian Linux

Адресацію буду перероблювати, але поки що так.

Конфігурація WireGuard MikroTik

Офіційна документація – WireGuard.

Отже, мережа VPN 10.100.0.0/24, адреса самого MikroTik в ній – 10.100.0.1.

Додаємо інтерфейс, на якому буде працювати WireGuard – задаємо ім’я, порт, задаємо MTU:

/interface wireguard add name=wg0 listen-port=51820 mtu=1420

(місцями вже без скріншотів, бо робив давненько)

MTU задаємо 1420 – бо дефолтний MTU в Ethernet 1500 байт, а WireGuard додає свої заголовки:

  • IP header: 20 байтів (IPv4) – задається операційною системою роутера
  • UDP header: 8 байтів – аналогічно, роутер
  • WireGuard header: 32 байти – задається WireGuard, де вказується тип повідомлення, індекс піра, аутентифікація

Разом під headers 60 байт: 20 (IP) + 8 (UDP) + 32 (WireGuard), що залишає нам 1440 байт для корисних даних (payload), і ще -20 “запас”.

Див. Header / MTU sizes for Wireguard та мій пост TCP/IP: моделі OSI та TCP/IP, TCP-пакети, Linux sockets і порти.

Перевіряємо:

/interface wireguard print detail where name=wg0

Задаємо адресу інтерфейсу:

/ip address add address=10.100.0.1/24 interface=wg0 comment=wg-hub

Перевіряємо:

/ip address print where interface=wg0

Конфігурація MikroTik Firewall

Нам треба додати дозволи на доступ з інтернету до самого WireGuard на MikroTik, а потім налаштувати доступи між мережами.

Детальніше про фаєрвол на MikroTik теж напишу окремо, теж є в чернетках.

Доступ з інтернету до WireGuard

Включаємо доступ з будь-якого хосту, правило в chain=input:

/ip firewall filter add chain=input protocol=udp dst-port=51820 action=accept comment="allow wireguard"

Або, краще, створюємо список дозволених адрес:

/ip firewall address-list add list=wg-allowed address=46.101.201.123 comment=setevoy-rtfm
/ip firewall address-list add list=wg-allowed address=178.***.236 comment=setevoy-home
/ip firewall address-list add list=wg-allowed address=64.***.***.83 comment=kyiv-work-office

Момент з адресами для дроплетів в 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 add chain=input protocol=udp dst-port=51820 src-address-list=wg-allowed action=accept comment="allow wireguard from whitelist" place-before=0

Перевіряємо:

/ip firewall filter print where src-address-list~"wg-allowed"

Доступ між внутрішніми мережами

Налаштовуємо доступ між мережами – додаємо правила в chain=forward.

Дозволяємо доступ з мережі VPN 10.100.0.0/24 в локальну мережу офісу 192.168.0.0/24:

/ip firewall filter add chain=forward src-address=10.100.0.0/24 dst-address=192.168.0.0/24 action=accept comment="wg to lan"

І обратно – з офісної мережі в мережу VPN:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=10.100.0.0/24 action=accept comment="lan to wg"

Далі – доступ з VPN до домашньої мережі 192.168.100.0/24:

/ip firewall filter add chain=forward src-address=10.100.0.0/24 dst-address=192.168.100.0/24 action=accept comment="wg to home"

І обратно, з дому в мережу VPN:

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=10.100.0.0/24 action=accept comment="home to wg"

Аналогічно – доступ вже між офісною 192.168.0.0/24  домашньою 192.168.100.0/24.

З офісу в домашню:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=192.168.100.0/24 action=accept comment="office to home"

І обратно:

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=192.168.0.0/24 action=accept comment="home to office"

Перевіряємо:

/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-toolspacman -S wireguard-tools.

На Debian встановлюємо пакет wireguard з apt:

root@setevoy-do-2023-09-02:~# apt update && apt install -y wireguard

Переходимо в /etc/wireguard/, створюємо ключі:

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.

На Debian отримуємо значення приватного ключа:

root@setevoy-do-2023-09-02:/etc/wireguard# cat privatekey 
ML+***dmk=

На MikroTik отримуємо public-key:

/interface wireguard print

На Debian створюємо конфіг /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = ML+0***mk=
Address = 10.100.0.10/32

[Peer]
PublicKey = hxz***0o=
Endpoint = 178.***.***.184:51820

AllowedIPs = 10.100.0.0/24,192.168.0.0/24,192.168.100.0/24
PersistentKeepalive = 25

Тут:

  • [Interface]
    • PrivateKey: приватний ключ на самому Debian
    • Address: IP-адреса цього Peer, буде використана для локального інтерфейсу wg0
  • [Peer]
    • PublicKey: публічний ключ з MikroTik
    • Endpoint: зовнішня адреса за якою доступний MikroTik, та порт, на якому WireGuard приймає підключення
    • AllowedIPs: в які мережі може ходити цей peer і для яких будуть створені локальні роути

На Debian отримуємо публічний ключ:

root@setevoy-do-2023-09-02:/etc/wireguard# cat publickey 
x+Pr***0TE=

На MikroTik додаємо новий peer:

/interface wireguard peers add interface=wg0 public-key="x+Pr***0TE=" allowed-address=10.100.0.10/32,192.168.0.0/24,192.168.100.0/24 comment=setevoy-rtfm

В 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 на офісний ноутбук:

root@setevoy-do-2023-09-02:~# ssh [email protected] -i .ssh/setevoy-work 
[setevoy@setevoy-work ~]$ 

І на домашній ноут – який теж є VPN peer, і у нього дві адреси – 10.100.0.3 в мережі VPN, і 192.168.100.100 в мережі домашнього роутера.

Пробуємо його адресу в VPN:

root@setevoy-do-2023-09-02:~# ping -c 1 10.100.0.3
PING 10.100.0.3 (10.100.0.3) 56(84) bytes of data.
64 bytes from 10.100.0.3: icmp_seq=1 ttl=63 time=116 ms

--- 10.100.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 115.925/115.925/115.925/0.000 ms

І на його домашню адресу:

root@setevoy-do-2023-09-02:~# 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=36.2 ms

--- 192.168.100.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 36.181/36.181/36.181/0.000 ms

І статус всіх клієнтів на MikriTik зараз:

WireGuard Connection Troubleshooting

Для дебагу може бути корисним додати логування проходження пакетів на фаєрволі MikroTik:

/ip firewall filter add chain=forward src-address=192.168.0.0/24 dst-address=192.168.100.0/24 action=log log-prefix="office->home" place-before=0

/ip firewall filter add chain=forward src-address=192.168.100.0/24 dst-address=192.168.0.0/24 action=log log-prefix="home->office" place-before=0

Потім пінгуємо з офісу додому – тут ще була проблема:

$ ping 192.168.100.100
PING 192.168.100.100 (192.168.100.100) 56(84) bytes of data.
^C
--- 192.168.100.100 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3090ms

І дивимось логи на MikroTik – тут клієнт вдома на Arch Linux вже підключений, пофіксив:

/log print where message~"office->home|home->office"
 ...
 2026-02-17 14:53:32 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84
 2026-02-17 14:53:33 firewall,info office->home forward: in:bridge out:wg0, connection-state:established src-mac c8:a3:62:2e:fd:cb, proto ICMP (type 8, code 0), 192.168.0.3->192.168.100.100, len 84
 2026-02-17 14:53:33 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84
 2026-02-17 14:53:34 firewall,info office->home forward: in:bridge out:wg0, connection-state:established src-mac c8:a3:62:2e:fd:cb, proto ICMP (type 8, code 0), 192.168.0.3->192.168.100.100, len 84
 2026-02-17 14:53:34 firewall,info home->office forward: in:wg0 out:bridge, connection-state:established proto ICMP (type 0, code 0), 192.168.100.100->192.168.0.3, len 84

Потім видаляємо, бо буде багато писати:

/ip firewall filter remove [find log-prefix="office->home"]

/ip firewall filter remove [find log-prefix="home->office"]

Або використовуємо tcpdump на хостах.

Наприклад, з офісного ноута пінгуємо домашній:

[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 – то пакет дропався.

Готово.

Loading

FreeBSD: Home NAS, part 12: синхронізація даних з Syncthing
0 (0)

14 Лютого 2026

Вже потроху наближаюсь до завершення історії з налаштування домашнього NAS на FreeBSD.

Вже є ZFS pool, є датасети, є моніторинг – можна починати налаштування автоматизації бекапів.

Але якщо на початку все здавалось доволі просто – “просто скопіювати потрібні каталоги з робочого ноутбука”, то чим далі – тим цікавішою виявлялась задача.

Більш детальний опис планування та автоматизації бекапів опишу окремо, а сьогодні познайомимось з ще одною класною утилітою – Syncthing.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Syncthing overview

Отже, для чого вона мені: є кілька хостів (робочий та домашній ноутбуки, ігровий ПК), між якими треба синхронізувати загальні дані.

Загальні дані – це каталоги з фотками, музикою, картинками – все те, що змінюється не дуже часто, і де нема “мусора” типу каталогів .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, і подивимось як це все працює.

Установка Syncthing на FreeBSD

Syncthing є в репозиторії, встановлюємо його:

root@setevoy-nas:~ # pkg install syncthing

Додаємо до /etc/rc.conf:

root@setevoy-nas:~ # sysrc syncthing_enable="YES"
syncthing_enable:  -> YES
root@setevoy-nas:~ # sysrc syncthing_user="setevoy"
syncthing_user:  -> setevoy

Файл налаштувань – /usr/local/etc/syncthing/config.xml.

Більшість налаштувань виконуються через Web (хоча є і CLI), але по дефолту Syncthing запускається на localhost.

А так як це FreeBSD без X-серверу – то і браузеру там нема.

Тому редагуємо файл і задаємо IP зовнішнього інтерфейсу, в мене це 192.168.0.2 (хоча адресацію буду перероблювати, коли доберусь до MikroTik та його DHCP):

...
    <gui enabled="true" tls="false" sendBasicAuthPrompt="false">
        <address>192.168.0.2:8384</address>
        <metricsWithoutAuth>false</metricsWithoutAuth>
        <apikey>L2P***eAk</apikey>
        <theme>default</theme>
    </gui>
...

Запускаємо сервіс:

root@setevoy-nas:~ # service syncthing start
Starting syncthing.

Перевіряємо порт:

root@setevoy-nas:~ # sockstat -4 -l | grep 8384
setevoy  syncthing  34083 18  tcp4   192.168.0.2:8384   *:*

Відкриваємо дашборду:

Створення ZFS dataset

Поки знайомлюсь з системою – зробив окремий датасет:

root@setevoy-nas:~ # zfs create nas/syncthing-test
root@setevoy-nas:~ # zfs list nas/syncthing-test
NAME                 USED  AVAIL  REFER  MOUNTPOINT
nas/syncthing-test    96K  2.24T    96K  /nas/syncthing-test

Задаємо власника – бо Syncthing запускається від юзера setevoy (який заданий через syncthing_user="setevoy" в /etc/rc.conf):

root@setevoy-nas:~ # chown setevoy:setevoy /nas/syncthing-test
root@setevoy-nas:~ # ls -ld /nas/syncthing-test
drwxr-xr-x  2 setevoy setevoy 2 Feb 13 19:05 /nas/syncthing-test

Додавання каталогу

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

Вказуємо ім’я та локальний шлях.

Folder ID залишаємо – це просто унікальний ідентифікатор для використання між хостами:

Тут жеж є налаштування Versioning – зберігання копій, далі подивимось детальніше:

І цікаві опції в Advanced – але це вже іншим разом:

Після додавання нового каталогу він буде збережений в .../syncthing/config.xml:

root@setevoy-nas:~ # cat /usr/local/etc/syncthing/config.xml | grep jmw5s-hotah
    <folder id="jmw5s-hotah" label="syncthing-test" path="/nas/syncthing-test" type="sendreceive" rescanIntervalS="3600" fsWatcherEnabled="true" fsWatcherDelayS="10" fsWatcherTimeoutS="0" ignorePerms="false" autoNormalize="true">

Тепер додаємо першого “клієнта” – хоча Syncthing все ж peer-to-peer архітектура, але конкретно в моєму випадку є окремий сервер чи хаб, а інші хости – це клієнти.

Установка Syncthing на Arch Linux

Теж є в репозиторії, встановлюємо:

[setevoy@setevoy-work ~] $ sudo pacman -S syncthing

Можна поки запустити руками – подивитись на його output:

[setevoy@setevoy-work ~]  $ syncthing
2026-02-13 19:14:21 INF syncthing v2.0.14 "Hafnium Hornet" (go1.25.6 X:nodwarf5 linux-amd64) syncthing@archlinux 2026-02-03 09:05:00 UTC [noupgrade] (log.pkg=main)
2026-02-13 19:14:21 INF Generating key and certificate (cn=syncthing log.pkg=syncthing)
2026-02-13 19:14:21 INF Default config saved; edit to taste (with Syncthing stopped) or use the GUI (path=/home/setevoy/.local/state/syncthing/config.xml log.pkg=syncthing)
2026-02-13 19:14:21 INF Archiving a copy of old config file format (path=/home/setevoy/.local/state/syncthing/config.xml.v0 log.pkg=syncthing)
2026-02-13 19:14:21 INF Calculated our device ID (device=2W2JHRW-T***-2TRDAAF log.pkg=syncthing)
2026-02-13 19:14:21 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-lookup.syncthing.net/v2/?noannounce" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-announce-v4.syncthing.net/v2/?nolookup" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="global discovery server https://discovery-announce-v6.syncthing.net/v2/?nolookup" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
2026-02-13 19:14:21 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
2026-02-13 19:14:21 INF Relay listener starting (id=dynamic+https://relays.syncthing.net/endpoint log.pkg=connections)
2026-02-13 19:14:21 INF QUIC listener starting (address="[::]:22000" log.pkg=connections)
2026-02-13 19:14:21 INF Creating new HTTPS certificate (log.pkg=api)
2026-02-13 19:14:21 INF TCP listener starting (address="[::]:22000" log.pkg=connections)
2026-02-13 19:14:21 INF GUI and API listening (address=127.0.0.1:8384 log.pkg=api)
2026-02-13 19:14:21 INF Access the GUI via the following URL: http://127.0.0.1:8384/ (log.pkg=api)
2026-02-13 19:14:21 INF Loaded configuration (name=setevoy-work log.pkg=syncthing)
2026-02-13 19:14:21 INF Measured hashing performance (perf="1978.89 MB/s" log.pkg=syncthing)

Додавання Remote Devices

Тепер треба Syncthing на Linux додати в пул до Syncthing на FreeBSD.

На Linux йдемо в Actions > Show ID:

(QR дуже ручний для підключення мобільних клієнтів – теж вже робив, працює чудово)

Далі на FreeBSD клікаємо Add Remote Device:

Він відразу в мережі побачив клієнта на Linux-хості (див. Syncthing Discovery Server та Security Principles):

Клікаємо Save, але Linux-клієнт поки що в статусі Disconnected:

Повертаємось до Syncthing на Linux – туди приходить запит на підключення:

І тепер маємо два девайси, об’єднані в мережу.

На FreeBSD:

І на ноутбуці з Linux:

Налаштування Folder Sharing

Тепер подивимось, як працює синхронізація.

На FreeBSD у створеному раніше Folder клікаємо Edit:

Переходимо на вкладку Sharing, вибираємо девайси, і якими хочемо зашарити папку:

Аналогічно до процесу додавання Devices – спочатку нам на Linux-клієнт прийде запит на підтвердження:

Клікаємо Add, задаємо локальний шлях на ноутбуці з Linux:

Перевіряємо, як це все діло працює.

Створимо файл на FreeBSD:

root@setevoy-nas:~ # echo "hello from nas" > /nas/syncthing-test/test1.txt

Дивимось Syncthing output на ноуті – пише що і коли змінилось:

...
2026-02-13 19:23:54 INF Synced file (folder.label=syncthing-test folder.id=jmw5s-hotah folder.type=sendreceive file.name=test1.txt file.modified="2026-02-13 19:23:43.432316 +0200 EET" file.permissions=0644 file.size=15 file.blocksize=131072 blocks.local=0 blocks.download=1 log.pkg=model)
...

І файл тепер є на Linux-клієнті:

[setevoy@setevoy-work ~]  $ ll nas/syncthing-test/
total 4
-rw-r--r-- 1 setevoy setevoy 15 Feb 13 19:23 test1.txt

Перевіримо зворотню синхронізацію – додамо файл на Linux:

[setevoy@setevoy-work ~]  $ echo "hello from laptop" > /home/setevoy/nas/syncthing-test/test2.txt

І через декілька секунд – він є і на FreeBSD:

root@setevoy-nas:~ # cat /nas/syncthing-test/test2.txt 
hello from laptop

Тестуємо видалення:

[setevoy@setevoy-work ~]  $ rm /home/setevoy/nas/syncthing-test/test2.txt

І на FreeBSD він теж зникає:

root@setevoy-nas:~ # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:25 test2.txt
root@setevoy-nas:~ # ll /nas/syncthing-test/
total 2
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Налаштування Versioning для бекапів

Тепер про те, як можна бекапити дані – захист від випадкового видалення.

Документація – File Versioning.

Переходимо в Folder > Edit, вкладка File Versioning:

Тут опції:

  • Trash Can: при видаленні файл переноситься в .stversions
  • Simple: зберігає N останніх версій
  • Staggered: зберігає версії з часом (1h, 1d, 1w і т.д.)
  • External: викликати зовнішній скрипт

Спробуємо з Trash Versioning:

На Linux-клієнті створимо новий файл:

[setevoy@setevoy-work ~]  $ echo "hello from laptop" > /home/setevoy/nas/syncthing-test/test-trash.txt

Чекаємо на його появу на FreeBSD-хості:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:33 test-trash.txt
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Видаляємо на ноутбуці:

[setevoy@setevoy-work ~]  $ rm /home/setevoy/nas/syncthing-test/test-trash.txt

І через кілька секунд він зникає на FreeBSD:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:33 test-trash.txt
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt
root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/
total 3
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:09 .stfolder
drwxr-xr-x  2 setevoy setevoy    3B Feb 13 19:34 .stversions
-rw-r--r--  1 root    setevoy   15B Feb 13 19:23 test1.txt

Але збережений в .stversions/:

root@setevoy-nas:/home/setevoy # ll /nas/syncthing-test/.stversions/
total 1
-rw-r--r--  1 setevoy setevoy   18B Feb 13 19:34 test-trash.txt

Крім того, у Web тепер є кнопочка Versions:

Де показані видалені файли, і які звідси можна відновити:

Для NAS, скоріш за все, зроблю Trash на 30 днів, а довготривалі бекапи будуть через ZFS snaphosts + копіювання на Google/Roton Drive та AWS S3.

Наступні кроки

Ну і тепер можна на Linux-клієнт додати Syncthing в автостарт:

[setevoy@setevoy-work ~]  $ systemctl --user enable syncthing.service
Created symlink '/home/setevoy/.config/systemd/user/default.target.wants/syncthing.service' → '/usr/lib/systemd/user/syncthing.service'.
[setevoy@setevoy-work ~]  $ systemctl --user start syncthing.service
[setevoy@setevoy-work ~]  $ systemctl --user status syncthing.service
● syncthing.service - Syncthing - Open Source Continuous File Synchronization
     Loaded: loaded (/usr/lib/systemd/user/syncthing.service; enabled; preset: enabled)
     Active: active (running) since Fri 2026-02-13 19:41:17 EET; 3s ago
...

Можна додати запуск сервісу без user login – корисно для ребутів, див. loginctl:

[setevoy@setevoy-work ~]  $ loginctl enable-linger setevoy

І додати базову перевірку в 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
...

Далі почитати і поробити Configuration Tuning, налаштувати Firewall Setup, і уважно перечитати Security Principles.

Наостанок – як Syncthing виглядає на телефоні з Syncthing-Fork:

І клієнт телефона в дашборді на FreeBSD:

Готово.

Loading