Новости:

SMF - Just Installed!

Главное меню

Последние сообщения

#1
Автообновление Docker-ноды с Galera (без потери кворума)

Описание
Скрипт выполняет:
  • безопасный вывод ноды из Galera
  • drain Docker Swarm
  • apt upgrade
  • reboot при необходимости
  • возврат в Galera и Docker

---

⚠️ ВАЖНО
  • минимум 3 ноды в Galera
  • wsrep_cluster_size > 1 перед началом

---

1. Скрипт

Код: bash
nano /usr/local/sbin/docker-galera-upgrade.sh

Код: bash
#!/bin/bash

set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

LOG="/var/log/docker-galera-upgrade.log"
STATE_FILE="/var/run/docker-galera-upgrade.state"

NODE=$(hostname)

log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}

log "===== START NODE UPGRADE (DOCKER + GALERA) ====="

# --- функции Galera ---

galera_size() {
mysql -Nse "SHOW STATUS LIKE 'wsrep_cluster_size'" | awk '{print $2}'
}

galera_ready() {
mysql -Nse "SHOW STATUS LIKE 'wsrep_ready'" | grep -q ON
}

# --- swarm check ---

if docker info 2>/dev/null | grep -q "Swarm: active"; then
SWARM=1
else
SWARM=0
fi

# --- POST REBOOT ---

if [ -f "$STATE_FILE" ]; then
log "Post-reboot stage"

```
# ждём Galera
until galera_ready; do
    log "Waiting Galera..."
    sleep 5
done

# возвращаем Docker
if [ "$SWARM" -eq 1 ]; then
    docker node update --availability active "$NODE" || true
fi

rm -f "$STATE_FILE"
log "Node restored"
exit 0
```

fi

# --- PRECHECK Galera ---

SIZE=$(galera_size)

if [ "$SIZE" -le 1 ]; then
log "Cluster size too small → abort"
exit 1
fi

log "Galera size OK: $SIZE"

# --- drain docker ---

if [ "$SWARM" -eq 1 ]; then
docker node update --availability drain "$NODE"
fi

# --- graceful Galera leave ---

log "Stopping MariaDB (leave cluster)"
systemctl stop mariadb

touch "$STATE_FILE"

# --- upgrade ---

apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1

# --- reboot ---

if [ -f /var/run/reboot-required ]; then
reboot
fi

# --- start MariaDB ---

systemctl start mariadb

# ждём Galera

until galera_ready; do
sleep 5
done

# --- restore docker ---

if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE"
fi

rm -f "$STATE_FILE"

log "===== DONE ====="

---

2. systemd service

Код: bash
nano /etc/systemd/system/docker-galera-upgrade.service

Код: bash
[Unit]
Description=Docker + Galera node upgrade
After=network-online.target docker.service mariadb.service
Requires=docker.service mariadb.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-galera-upgrade.sh

[Install]
WantedBy=multi-user.target

---

3. systemd timer

Код: bash
nano /etc/systemd/system/docker-galera-upgrade.timer

Код: bash
[Unit]
Description=Node upgrade timer

[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target

---

4. Активация

Код: bash
systemctl daemon-reload
systemctl enable --now docker-galera-upgrade.timer
systemctl enable docker-galera-upgrade.service

---

Как это работает
  • проверяется размер Galera
  • нода выводится из Docker
  • MariaDB корректно останавливается
  • после reboot ждём wsrep_ready
  • нода возвращается в кластер

---

⚠️ Рекомендации
  • использовать IST (одинаковая версия MariaDB)
  • проверить wsrep_sst_method (xtrabackup-v2)
  • не обновлять все ноды одновременно

---

Проверка

Код: bash
mysql -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
docker node ls

---
#2
Мониторинг (минимальный, но рабочий)

1. Лог-файл

Docker:
Код: bash
tail -f /var/log/docker-apt-upgrade.log

HAProxy:
Код: bash
tail -f /var/log/haproxy-upgrade.log

---

2. Проверка через systemd

Код: bash
systemctl status docker-apt-upgrade.service
systemctl status haproxy-upgrade.service

---

3. Уведомление в Telegram (простой вариант)

Добавить в конец скрипта:

Код: bash
curl -s -X POST https://api.telegram.org/botTOKEN/sendMessage 
-d chat_id=CHAT_ID 
-d text="Upgrade done on $(hostname)"

---

4. Проверка кластера после обновления

Docker:
Код: bash
docker node ls
docker service ls

---

5. Health endpoint (если нет)

Код: bash
echo OK | nc -l -p 8080

или через nginx.

---

Результат
  • видно статус обновлений
  • есть лог
  • есть уведомления
#3
Rolling обновление HAProxy (keepalived, 2 ноды)

Описание
Обновляет обе ноды по очереди:
  • проверяет health второй ноды
  • обновляет текущую
  • гарантирует, что одна нода всегда жива

---

1. Скрипт

Код: bash
nano /usr/local/sbin/haproxy-ha-upgrade.sh

Код: bash
#!/bin/bash

set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

LOG="/var/log/haproxy-upgrade.log"
LOCK="/var/run/haproxy-upgrade.lock"

VIP="10.10.1.100"
PEER="haproxy-2"   # поменять на вторую ноду

log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}

if [ -f "$LOCK" ]; then
exit 0
fi

touch "$LOCK"
trap "rm -f $LOCK" EXIT

# self check

curl -fs http://127.0.0.1/health || exit 1

# peer check

ssh "$PEER" "curl -fs http://127.0.0.1/health" || exit 1

# upgrade

apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1

if [ -f /var/run/reboot-required ]; then
reboot
fi

sleep 30

curl -fs http://127.0.0.1/health || exit 1

---

2. systemd service

Код: bash
nano /etc/systemd/system/haproxy-upgrade.service

Код: bash
[Unit]
Description=HAProxy upgrade

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/haproxy-ha-upgrade.sh

---

3. systemd timer

Код: bash
nano /etc/systemd/system/haproxy-upgrade.timer

Код: bash
[Unit]
Description=HAProxy upgrade timer

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

---

4. Активация

Код: bash
systemctl daemon-reload
systemctl enable --now haproxy-upgrade.timer

---

Важно
  • на второй ноде указать обратный PEER
  • сделать разное время запуска (например 03:00 и 03:10)
#4
Автоматическое обновление Docker-ноды (APT + reboot + возврат из maintenance)

Описание
Скрипт выполняет:
  • apt update && apt upgrade -y
  • перевод ноды в drain (если Docker Swarm)
  • перезагрузку при необходимости
  • автоматический возврат ноды в active после reboot

---

1. Создание скрипта

Код: bash
nano /usr/local/sbin/docker-apt-upgrade.sh

Вставить:

Код: bash
#!/bin/bash

set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

LOG="/var/log/docker-apt-upgrade.log"
STATE_FILE="/var/run/docker-upgrade.state"

NODE=$(hostname)

log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}

log "===== START DOCKER NODE UPGRADE ====="

if docker info 2>/dev/null | grep -q "Swarm: active"; then
SWARM=1
else
SWARM=0
fi

if [ -f "$STATE_FILE" ]; then
log "Post-reboot detected → restoring node"

```
if [ "$SWARM" -eq 1 ]; then
    docker node update --availability active "$NODE" || true
fi

rm -f "$STATE_FILE"
exit 0
```

fi

if [ "$SWARM" -eq 1 ]; then
docker node update --availability drain "$NODE"
fi

touch "$STATE_FILE"

apt update >> "$LOG" 2>&1
apt upgrade -y >> "$LOG" 2>&1

if [ -f /var/run/reboot-required ]; then
reboot
fi

if [ "$SWARM" -eq 1 ]; then
docker node update --availability active "$NODE"
fi

rm -f "$STATE_FILE"

Код: bash
chmod +x /usr/local/sbin/docker-apt-upgrade.sh

---

2. systemd service

Код: bash
nano /etc/systemd/system/docker-apt-upgrade.service

Код: bash
[Unit]
Description=Docker node upgrade
After=network-online.target docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-apt-upgrade.sh

[Install]
WantedBy=multi-user.target

---

3. systemd timer

Код: bash
nano /etc/systemd/system/docker-apt-upgrade.timer

Код: bash
[Unit]
Description=Docker node upgrade timer

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

---

4. Активация

Код: bash
systemctl daemon-reload
systemctl enable --now docker-apt-upgrade.timer
systemctl enable docker-apt-upgrade.service

---

Результат
  • нода уходит в drain
  • обновляется
  • перезагружается
  • сама возвращается в кластер
#5
📌 Core Scripts (HAProxy Dynamic System)

1. MAIN CLI:
Код: bash
/usr/local/bin/haproxy-map-update.sh

---

2. CERT ENGINE:
Код: bash
/usr/local/bin/haproxy-cert.sh

---

⚙️ 1. haproxy-cert.sh

Код: bash
#!/bin/bash
set -euo pipefail

DOMAIN=${1:-}

if [ -z "$DOMAIN" ]; then
    echo "[CERT] no domain!"
    exit 1
fi

echo "[CERT] Issuing certificate for $DOMAIN"

certbot certonly --standalone \
  --http-01-port=8888 \
  -d "$DOMAIN" \
  --non-interactive \
  --agree-tos \
  --email george@cloud-life.site \
  --expand

echo "[CERT] Building PEM"

cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
    /etc/letsencrypt/live/$DOMAIN/privkey.pem \
    > /etc/ssl/haproxy/$DOMAIN.pem

echo "[CERT] Done"

---

⚙️ 2. haproxy-map-update.sh

Код: bash
#!/bin/bash

GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"

CFG="/etc/haproxy/haproxy.cfg"

HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"

usage() {
    echo -e "${GREEN}"
    echo "Usage:"
    echo "  $0 set DOMAIN BACKEND MODE IP_LIST PORT [-y]"
    echo "  $0 del DOMAIN"
    echo -e "${NC}"
    exit 1
}

if [ $# -lt 2 ]; then
    usage
fi

ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}
IP_LIST=${5:-}
PORT=${6:-}
AUTO_YES=${7:-}

set -euo pipefail

backend_exists() {
    grep -q "^backend $BACKEND" "$CFG"
}

build_servers() {
    IFS=',' read -ra IPS <<< "$IP_LIST"
    i=1
    SERVERS=""

    for ip in "${IPS[@]}"; do
        name="${BACKEND}${i}"

        if [ "$PORT" == "443" ]; then
            SERVERS+="    server ${name} ${ip}:${PORT} ssl verify none check\n"
        else
            SERVERS+="    server ${name} ${ip}:${PORT} check\n"
        fi

        i=$((i+1))
    done
}

create_backend() {

    echo "[BACKEND] Creating: $BACKEND"

    if [ -z "${IP_LIST:-}" ]; then
        read -p "IP(s): " IP_LIST
    fi

    if [ -z "${PORT:-}" ]; then
        read -p "PORT: " PORT
    fi

    build_servers

    TMP=$(mktemp)

    awk -v block="$(cat <<EOF
backend $BACKEND
    mode http
    balance roundrobin
    option forwardfor header X-Real-IP
    http-request set-header X-Forwarded-For %[src]
    http-request set-header X-Forwarded-Port %[dst_port]

$SERVERS
EOF
)" '
    /# --- AUTO_BACKENDS_END ---/ {
        print block
    }
    { print }
    ' "$CFG" > "$TMP"

    mv "$TMP" "$CFG"
}

issue_cert() {
    /usr/local/bin/haproxy-cert.sh "$DOMAIN"
}

update_map() {
    local MAP=$1
    local KEY=$2
    local VALUE=$3

    TMP=$(mktemp)
    [ -f "$MAP" ] && cp "$MAP" "$TMP"

    grep -v "^$KEY " "$TMP" > "$TMP.new" || true
    echo "$KEY $VALUE" >> "$TMP.new"

    mv "$TMP.new" "$MAP"
    rm -f "$TMP"
}

delete_maps() {
    for m in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
        [ -f "$m" ] && grep -v "^$DOMAIN " "$m" > "$m.tmp" && mv "$m.tmp" "$m"
    done
}

engine() {
    for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
        [ -f "$MAP" ] || continue
        awk '{ print length($1), $0 }' "$MAP" | sort -nr | cut -d" " -f2- > "$MAP.tmp"
        mv "$MAP.tmp" "$MAP"
    done
}

echo "[MAP] $DOMAIN"

if [ "$ACTION" == "set" ]; then

    if ! backend_exists; then
        if [[ "$AUTO_YES" == "-y" ]]; then
            create_backend
        else
            read -p "Create backend $BACKEND? [Y/n]: " c
            [[ "$c" =~ ^[Nn]$ ]] && exit 1
            create_backend
        fi
    fi

    if [ "$MODE" == "http" ]; then
        issue_cert
        update_map "$HOSTS_MAP" "$DOMAIN" "$BACKEND"

    elif [ "$MODE" == "passth" ]; then
        update_map "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
        update_map "$ACME_MAP" "$DOMAIN" "acme_${BACKEND}"
    fi

    delete_maps
    engine
    systemctl reload haproxy

elif [ "$ACTION" == "del" ]; then
    delete_maps
    engine
    systemctl reload haproxy
fi

echo -e "${GREEN}[DONE]${NC}"

---

📌 Требуемая структура HAProxy:

Код: bash
# --- AUTO_BACKENDS_END ---

---

🏁 Результат

  • Полностью автоматическое создание backend'ов
  • Multi-node поддержка
  • ACME + TLS split
  • Maps-driven routing
  • UI-ready CLI backend
#6
HAProxy Dynamic Provisioning System

Назначение:
Система автоматического управления HAProxy:
- создание backend'ов
- работа с maps (hosts / passth / acme)
- выпуск SSL сертификатов (Let's Encrypt)
- поддержка multi-node backend
- zero-downtime reload

---

📦 Архитектура

Код: bash
Client
  ↓
HAProxy (:80 / :443)
  ↓
Maps routing:
   ├── hosts.map (HTTP backend)
   ├── passth.map (TLS SNI backend)
   └── acme_backends.map (ACME HTTP backend)
  ↓
Backends (auto-generated)

---

⚙️ CLI инструмент

Основной скрипт:

Код: bash
/usr/local/bin/haproxy-map-update.sh

---

📌 Общий синтаксис

Код: bash
haproxy-map-update.sh set DOMAIN BACKEND MODE IP_LIST PORT [-y]
haproxy-map-update.sh del DOMAIN

---

🧪 Примеры использования

---

1. HTTP сайт (один сервер)

Код: bash
haproxy-map-update.sh set ftp.cloud-life.site ftp http 10.10.1.37 80 -y

Результат:
- backend ftp:80
- certbot через HAProxy (8888)
- hosts.map обновлён

---

2. HTTPS backend (multi-node passthrough)

Код: bash
haproxy-map-update.sh set games.cloud-life.site games passth "10.10.1.46,10.10.1.47,10.10.1.48" 443 -y

Результат:
- acme_games (HTTP/80)
- games backend (HTTPS/443)
- roundrobin между нодами
- SSL verify none

---

3. Удаление домена

Код: bash
haproxy-map-update.sh del games.cloud-life.site

---

🔐 SSL схема (Let's Encrypt)

HTTP challenge flow:

Код: bash
Internet → HAProxy :80
         → /.well-known/acme-challenge/
         → 127.0.0.1:8888 (certbot standalone)

---

⚙️ Backend types

HTTP backend:
- один сервер или несколько
- порт 80
- certbot active

PASSTH backend:
- TLS SNI routing
- порт 443
- multi-node support
- acme backend auto-created

---

📂 Maps

Код: bash
/etc/haproxy/maps/hosts.map
/etc/haproxy/maps/passth.map
/etc/haproxy/maps/acme_backends.map

---

🧠 Особенности системы

  • Backend создаётся автоматически при отсутствии
  • Поддержка multi-server (comma-separated IPs)
  • HTTPS backend автоматически получает ssl verify none
  • ACME backend создаётся отдельно для passth
  • Maps сортируются по длине ключа (важно для subdomains)
  • HAProxy reload только после всех операций

---

🚀 Multi-server пример

Код: bash
IP_LIST="10.10.1.46,10.10.1.47,10.10.1.48"
PORT=443

Результат:
Код: bash
server games1 10.10.1.46:443 ssl verify none check
server games2 10.10.1.47:443 ssl verify none check
server games3 10.10.1.48:443 ssl verify none check

---

💡 Режим -y

Автоматическое создание backend без вопросов:

Код: bash
-y

---

🏁 Итог

Система заменяет:
- ручное редактирование haproxy.cfg
- ручное управление backend'ами
- ручное создание SSL
- ручной reload

И превращает HAProxy в:
динамический ingress controller уровня mini-Kubernetes
#7
Задача:
Организовать автоматическое:
- получение SSL (Let's Encrypt)
- обновление HAProxy maps
- без остановки HAProxy
- без webroot
- без ломания routing

---

📌 Архитектура:

Код: bash
Internet
   ↓
:80 (HAProxy)
   ↓
ACME path → 127.0.0.1:8888 (certbot standalone)
   ↓
остальной трафик → maps → backend

---

🔧 1. HAProxy (ACME routing)

В frontend на 80:

Код: bash
frontend http_80
    bind *:80
    mode http

    acl letsencrypt-acl path_beg /.well-known/acme-challenge/
    use_backend letsencrypt-backend if letsencrypt-acl

    http-request redirect scheme https code 301 if !letsencrypt-acl

Backend:

Код: bash
backend letsencrypt-backend
    mode http
    server letsencrypt 127.0.0.1:8888

---

⚙️ 2. Скрипт получения сертификата

/usr/local/bin/haproxy-cert.sh

Код: bash
#!/bin/bash
set -euo pipefail

DOMAIN=${1:-}

if [ -z "$DOMAIN" ]; then
    echo "[CERT] no domain!"
    exit 1
fi

echo "[CERT] Issuing certificate for $DOMAIN"

certbot certonly --standalone \
  --http-01-port=8888 \
  -d "$DOMAIN" \
  --non-interactive \
  --agree-tos \
  --email george@cloud-life.site \
  --expand

echo "[CERT] Building PEM"

cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
    /etc/letsencrypt/live/$DOMAIN/privkey.pem \
    > /etc/ssl/haproxy/$DOMAIN.pem

echo "[CERT] Done"

Важно:
- HAProxy НЕ останавливается
- порт 80 остаётся занят HAProxy
- certbot работает через 8888

---

⚙️ 3. Основной скрипт управления maps

/usr/local/bin/haproxy-map-update.sh

Код: bash
#!/bin/bash

GREEN="\033[0;32m"
NC="\033[0m"
RED="\033[0;31m"

usage() {
    echo -e "${GREEN}"
    echo "Usage:"
    echo "  $0 set DOMAIN BACKEND MODE"
    echo "  $0 del DOMAIN"
    echo ""
    echo "Modes:"
    echo "  http    - обычный HTTP (hosts.map)"
    echo "  passth  - TLS passthrough (passth.map + acme_backends.map)"
    echo -e "${NC}"
    exit 1
}

if [ $# -lt 2 ]; then
    echo -e "${RED}[ERROR] Invalid arguments${NC}"
    usage
fi

ACTION=$1
DOMAIN=$2
BACKEND=${3:-}
MODE=${4:-}

if [ "$ACTION" == "set" ]; then
    if [ -z "$BACKEND" ] || [ -z "$MODE" ]; then
        echo -e "${RED}[ERROR] Invalid arguments${NC}"
        usage
    fi
fi

set -euo pipefail

HOSTS_MAP="/etc/haproxy/maps/hosts.map"
PASSTH_MAP="/etc/haproxy/maps/passth.map"
ACME_MAP="/etc/haproxy/maps/acme_backends.map"

ACME_BACKEND="acme_${BACKEND}"

issue_cert() {
    echo "[CERT] Ensuring certificate for $DOMAIN"

    if /usr/local/bin/haproxy-cert.sh "$DOMAIN"; then
        echo -e "${GREEN}[CERT] OK${NC}"
    else
        echo -e "${RED}[CERT] FAILED → aborting${NC}"
        exit 1
    fi
}

update_file() {
    local MAP=$1
    local KEY=$2
    local VALUE=$3

    TMP=$(mktemp)
    [ -f "$MAP" ] && cp "$MAP" "$TMP"

    grep -v "^$KEY " "$TMP" > "$TMP.new" || true

    if [ "$ACTION" == "set" ]; then
        echo "$KEY $VALUE" >> "$TMP.new"
    fi

    mv "$TMP.new" "$MAP"
    rm -f "$TMP"
}

delete_from_all() {
    update_file "$HOSTS_MAP" "$DOMAIN" ""
    update_file "$PASSTH_MAP" "$DOMAIN" ""
    update_file "$ACME_MAP" "$DOMAIN" ""
}

engine() {
    for MAP in "$HOSTS_MAP" "$PASSTH_MAP" "$ACME_MAP"; do
        if [ -f "$MAP" ]; then
            awk '{ print length($1), $0 }' "$MAP" \
                | sort -nr \
                | cut -d" " -f2- \
                > "$MAP.tmp"

            mv "$MAP.tmp" "$MAP"
        fi
    done
}

echo "[MAP] Processing: $DOMAIN"

# 🔥 1. СНАЧАЛА сертификат
if [ "$ACTION" == "set" ]; then
    issue_cert
fi

# 🔥 2. потом routing
delete_from_all

if [ "$ACTION" == "set" ]; then

    if [ "$MODE" == "http" ]; then
        update_file "$HOSTS_MAP" "$DOMAIN" "$BACKEND"

    elif [ "$MODE" == "passth" ]; then
        update_file "$PASSTH_MAP" "$DOMAIN" "$BACKEND"
        update_file "$ACME_MAP" "$DOMAIN" "$ACME_BACKEND"

    else
        echo "Mode must be: http | passth"
        exit 1
    fi
fi

echo "[MAP] Sorting maps"
engine

echo "[MAP] Reloading HAProxy"
systemctl reload haproxy

echo -e "${GREEN}[MAP] DONE${NC}"

---

🚀 Использование:

Код: bash
# HTTP сайт
haproxy-map-update.sh set site.com backend_name http

# TLS passthrough
haproxy-map-update.sh set site.com backend_name passth

# удалить
haproxy-map-update.sh del site.com

---

💡 Важные моменты:

  • Certbot вызывается ДО изменения maps
  • Это критично (иначе будет 503)
  • ACME идёт через HAProxy → 8888
  • HAProxy не перезапускается дважды
  • maps сортируются по длине (чтобы поддомены не ломались)

---

🏁 Результат:

  • Никаких 503 при выпуске сертификатов
  • Нет остановки HAProxy
  • Полностью автоматизированный onboarding доменов
  • Production-ready схема
#8
HAProxy Edge (VIP + Auto Reload + Let's Encrypt) — пошаговая инструкция



📌 Описание

Схема:

* 2 VPS с HAProxy
* keepalived (VIP, active/passive)
* GlusterFS:

  * /etc/haproxy
  * /etc/letsencrypt
* Certbot (Let's Encrypt)

Цель:

* reload HAProxy при изменении конфигов/maps
* автоматическое обновление сертификатов
* исключение гонок и двойных reload



1. Скрипт безопасного reload HAProxy

Создаём файл:

Код: bash
nano /usr/local/bin/haproxy-safe-reload.sh

Содержимое:

Код: bash
#!/bin/bash

set -euo pipefail

VIP="10.10.10.10"

# Проверяем, что нода активная (есть VIP)

ip addr | grep -q "$VIP"
if [ $? -ne 0 ]; then
echo "[HAProxy] Not active node → skip reload"
exit 0
fi

LOCK=/var/lock/haproxy-reload.lock

flock -n "$LOCK" bash -c '

echo "[HAProxy] validating config..."

haproxy -c -f /etc/haproxy/haproxy.cfg
if [ $? -ne 0 ]; then
echo "[HAProxy] CONFIG INVALID → abort"
exit 1
fi

echo "[HAProxy] reloading..."

systemctl reload haproxy

echo "[HAProxy] reload OK"
'

Делаем исполняемым:

Код: bash
chmod +x /usr/local/bin/haproxy-safe-reload.sh



2. Watcher изменений конфигурации

Устанавливаем:

Код: bash
apt install inotify-tools -y

Создаём watcher:

Код: bash
nano /usr/local/bin/haproxy-watch.sh

Содержимое:

Код: bash
#!/bin/bash

WATCH="/etc/haproxy"

LOCK=/var/lock/haproxy-watch.lock
LAST_RUN=0

inotifywait -m -r -e close_write --format '%f' $WATCH | while read file; do

```
# игнор временных файлов
case "$file" in
    *.swp|*.swx|*.tmp|4913)
        continue
        ;;
esac

# реагируем только на нужные файлы
if [[ "$file" != "haproxy.cfg" && "$file" != *.map ]]; then
    continue
fi

NOW=$(date +%s)

# debounce (защита от повторов)
if (( NOW - LAST_RUN < 3 )); then
    continue
fi

LAST_RUN=$NOW

echo "[WATCH] Change detected: $file"

flock -n $LOCK /usr/local/bin/haproxy-safe-reload.sh
```

done

Делаем исполняемым:

Код: bash
chmod +x /usr/local/bin/haproxy-watch.sh



3. systemd сервис для watcher

Создаём:

Код: bash
nano /etc/systemd/system/haproxy-watch.service

Содержимое:

Код: bash
[Unit]
Description=HAProxy config watcher
After=network.target

[Service]
ExecStart=/usr/local/bin/haproxy-watch.sh
Restart=always

[Install]
WantedBy=multi-user.target

Включаем:

Код: bash
systemctl daemon-reload
systemctl enable --now haproxy-watch

Проверка:

Код: bash
systemctl status haproxy-watch



4. Автоматическая сборка сертификатов для HAProxy

Создаём deploy-hook:

Код: bash
nano /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh

Содержимое:

Код: bash
#!/bin/bash

set -euo pipefail

CERT_DIR="/etc/haproxy/certs"

if [ -n "${RENEWED_LINEAGE:-}" ]; then
DOMAIN=$(basename "$RENEWED_LINEAGE")

```
cat "$RENEWED_LINEAGE/fullchain.pem" \
    "$RENEWED_LINEAGE/privkey.pem" \
    > "$CERT_DIR/$DOMAIN.pem"
```

else
for d in /etc/letsencrypt/live/*/; do
DOMAIN=$(basename "$d")
cat "$d/fullchain.pem" "$d/privkey.pem" > "$CERT_DIR/$DOMAIN.pem"
done
fi

/usr/local/bin/haproxy-safe-reload.sh

Права:

Код: bash
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-deploy-cert.sh



5. Важные принципы работы

  • Reload выполняется только на ACTIVE node (через VIP check)
  • flock защищает от параллельных reload
  • haproxy -c проверяет конфиг перед применением
  • Watcher реагирует только на реальные изменения (cfg + maps)
  • Игнорируются временные файлы редакторов



6. Тестирование

Редактируем конфиг:

Код: bash
nano /etc/haproxy/haproxy.cfg

Ожидаем:

Код: bash
[WATCH] Change detected: haproxy.cfg
[HAProxy] reload OK



🎯 Итог

Получаем:

* автоматический reload HAProxy
* безопасный деплой конфигурации
* корректную работу в HA (VIP)
* автоматическую интеграцию с Let's Encrypt

[/hr]
#9
Статус 502 от Caddy означает, что сам веб-сервер Caddy работает отлично и защищает соединение по HTTPS, но он не может достучаться до контейнеров OnlyOffice внутри сети Docker Swarm.Плюс, обратите внимание на домен: в запросе вы проверяете docu.cloud-life.site, а в файле Caddyfile мы прописали docs.cloud-life.site [1].Давайте проведем быструю диагностику по шагам, чтобы найти, на каком этапе обрывается связь.Шаг 1. Проверяем доменное имя в CaddyfileЕсли вы планируете использовать именно docu.cloud-life.site, то Caddy сейчас просто не применяет правила проксирования к этому поддомену.Откройте Caddyfile:bashnano /mnt/gluster-storage/Caddyfile
Используйте код с осторожностью.Убедитесь, что имя домена совпадает с тем, что вы вводите в браузере (поменяйте docs на docu [1], если необходимо):caddydocu.cloud-life.site {
    reverse_proxy hub_onlyoffice:80 {
        transport http {
            dial_timeout 10s
        }
    }
    encode gzip zstd
}
Используйте код с осторожностью.Перечитайте конфиг Caddy [2]:bashdocker exec -it $(docker ps -q -f name=hub_caddy | head -n 1) caddy reload --config /etc/caddy/Caddyfile
Используйте код с осторожностью.Шаг 2. Проверяем статус контейнеров OnlyOfficeНам нужно убедиться, что контейнеры OnlyOffice вообще запустились и не падают по кругу (например, из-за неверного пароля к базе данных).Выполните команду на менеджер-ноде:bashdocker stack ps hub
Используйте код с осторожностью.Что искать: В колонке CURRENT STATE у сервиса hub_onlyoffice должно быть написано Running ... X minutes ago.Если там написано Shutdown или Failed: Контейнер падает при старте. Переходите к Шагу 3.Шаг 3. Смотрим логи OnlyOffice (Ищем причину падения)Если контейнеры не могут запуститься, они пишут ошибку в логи. Давайте посмотрим последние 50 строк:bashdocker service logs hub_onlyoffice --tail 50
Используйте код с осторожностью.Частая проблема 1 (Connection refused к БД): OnlyOffice не может достучаться до вашей внешней Galera. Проверьте, разрешен ли доступ пользователю onlyoffice с IP-адресов подсети Docker (мы настраивали это через ufw allow в Шаге 3).Частая проблема 2 (Access denied для пользователя): Неверно указан пароль DB_PASSWORD или имя базы в файле prod-stack.yml.Шаг 4. Проверка связи изнутри CaddyЕсли OnlyOffice запущен (Running), но Caddy всё равно выдает 502, значит, они не видят друг друга внутри overlay-сети Docker.Зайдите внутрь контейнера Caddy и попробуйте сделать запрос к балансировщику OnlyOffice:bashdocker exec -it $(docker ps -q -f name=hub_caddy | head -n 1) wget -qO- http://hub_onlyoffice:80
Используйте код с осторожностью.Если вернет HTML-код (или ошибку 404/отказ, но связь построится) — сеть Docker исправна, дело было в домене из Шага 1.Если напишет bad address или зависнет по таймауту — контейнеры Caddy и OnlyOffice почему-то оказались в разных сетях.Выполните команду docker stack ps hub и проверьте домен. Что показывают логи OnlyOffice из Шага 3?нет, там docu. docs старый пока живетПонял вас, docs не трогаем, работаем со свежим docu.Раз домен в порядке, а Caddy выдает стабильный 502, значит, контейнеры OnlyOffice либо физически не запустились, либо не могут пройти внутренний тест самопроверки (Healthcheck) из-за проблем с подключением к нашей новой внешней Galera Cluster.Давайте вытащим логи OnlyOffice и посмотрим, на каком этапе спотыкается инициализация. Выполните команду на мастер-ноде:bashsudo docker service logs hub_onlyoffice --tail 100
Используйте код с осторожностью.На какие строки в логах нужно обратить внимание:Если видите Error: Connect ECONNREFUSED 10.10.1.36:3306OnlyOffice пытается подключиться к Galera на хосте, но операционная система Ubuntu сбрасывает соединение.Решение: Проверьте, что в /etc/mysql/mariadb.conf.d/60-galera.cnf на хостах действительно прописано bind-address = 0.0.0.0, а не 127.0.0.1.Если видите Access denied for user 'onlyoffice'@'10.0.X.X'Galera сбрасывает подключение, так как IP-адрес, с которого пришел контейнер (внутренний IP сети Docker Swarm, например 10.0.0.5), не совпадает с правилами прав в MySQL.Решение: Зайдите в консоль MariaDB на хосте и выполните проверку: SELECT user, host FROM mysql.user WHERE user='onlyoffice';. Убедитесь, что там стоит хост % (разрешено подключение с любых IP).Если видите Waiting for connection to rabbitmq... или redis...OnlyOffice не может связаться с соседними контейнерами очередей или кэша внутри Swarm. Это означает, что они упали или зависли.Выполните команду sudo docker service logs hub_onlyoffice --tail 100 и пришлите последние строки, где видна ошибка. Что именно мешает запуску?.0.ko0df57lfm5y@onlyoffice-stack-2    | Waiting for connection to the 10.10.1.36,10.10.1.37,10.10.1.38 host on port 3306
hub_onlyoffice.0.ko0df57lfm5y@onlyoffice-stack-2    | nc: getaddrinfo for host "10.10.1.36,10.10.1.37,10.10.1.38" port 3306: Name or service not known
hub_onlyoffice.0.ko0df57lfm5y@onlyoffice-stack-2    | Waiting for connection to the 10.10.1.36,10.10.1.37,10.10.1.38 host on port 3306
hub_onlyoffice.0.ko0df57lfm5y@onlyoffice-stack-2    | nc: getaddrinfo for host "10.10.1.36,10.10.1.37,10.10.1.38" port 3306: Name or service not known
hub_onlyoffice.0.ko0df57lfm5y@onlyoffice-stack-2    | Waiting for connection to the 10.10.1.36,10.10.1.37,10.10.1.38 host on port 3306
hub_onlyoffice.0.ko0df57lfm5y@onlyoffice-stack-2    | nc: getaddrinfo for host "10.10.1.36,10.10.1.37,10.10.1.38" port 3306: Name or service not known он эти перечисленные хосты считает одним, и не можнт достучаться. Мы можем добавить реверс-прокси (балансировщик)? я использую haproxy. но в данном случан не хочу обращаться к внешним серверам, чтоб все жило на этих 3-хОтличная диагностика! Вы абсолютно правы: встроенный скрипт инициализации OnlyOffice внутри контейнера банально не умеет парсить строку с запятыми через nc (netcat). Он воспринимает всю строку "10.10.1.36,10.10.1.37,10.10.1.38" как один монолитный некорректный адрес и уходит в бесконечный цикл ожидания.Разворачивать внешние серверы ради этого точно не нужно. Так как вы уже знакомы с HAProxy, мы поднимем его внутри самого Docker Swarm в виде легковесного отказоустойчивого балансировщика прямо на этих же трех нодах.HAProxy будет слушать внутри сети Docker и перенаправлять трафик на ваши внешние хосты Galera. Если одна из нод базы данных упадет, HAProxy сам переключит поток на живую.Шаг 1. Переделываем YAML-файл стека (prod-stack.yml)Мы добавляем в стек официальный сервис haproxy и прописываем для onlyoffice один единственный внутренний адрес балансировщика.Откройте ваш prod-stack.yml и внесите изменения:В секции onlyoffice замените параметры БД на одиночный хост:yaml      # Теперь указываем локальный балансировщик HAProxy внутри Swarm
      - DB_TYPE=mysql
      - DB_HOST=onlyoffice-db-balancer
      - DB_PORT=3306
      - DB_NAME=onlyoffice
      - DB_USER=onlyoffice
      - DB_PASSWORD=SuperSecretDBPass
Используйте код с осторожностью.В самый конец секции services: добавьте новый сервис HAProxy:yaml  # --- ВНУТРЕННИЙ БАЛАНСИРОВЩИК ДЛЯ GALERA CLUSTER ---
  onlyoffice-db-balancer:
    image: haproxy:2.8-alpine
    volumes:
      - /mnt/gluster-storage/haproxy:/usr/local/etc/haproxy:ro
    networks:
      - cloudlife_net
    deploy:
      mode: replicated
      replicas: 2 # Запускаем 2 копии для отказоустойчивости самого прокси
      restart_policy:
        condition: on-failure
Используйте код с осторожностью.Шаг 2. Создание конфигурации HAProxy на GlusterFSПоскольку папка /mnt/gluster-storage синхронизируется между всеми серверами, конфигурация балансировщика будет доступна всем его копиям.Создайте папку для конфига:bashsudo mkdir -p /mnt/gluster-storage/haproxy
Используйте код с осторожностью.Создайте файл конфигурации:bashsudo nano /mnt/gluster-storage/haproxy/haproxy.cfg
Используйте код с осторожностью.Вставьте следующий продакшен-конфиг для балансировки MySQL/Galera:haproxyglobal
    log stdout format raw local0

defaults
    log     global
    mode    tcp            # Для баз данных используем чистый TCP
    timeout connect 5s
    timeout client  30m
    timeout server  30m

# Входная точка внутри Docker Swarm
frontend galera_in
    bind *:3306
    default_backend galera_nodes

# Распределение трафика на физические хосты Ubuntu
backend galera_nodes
    mode tcp
    option tcp-check       # Проверка доступности порта
    balance round-robin    # Равномерное распределение нагрузки

    # Прописываем ваши реальные ноды Galera Cluster
    server galera1 10.10.1.36:3306 check inter 2s rise 2 fall 3
    server galera2 10.10.1.37:3306 check inter 2s rise 2 fall 3
    server galera3 10.10.1.38:3306 check inter 2s rise 2 fall 3
Используйте код с осторожностью.Шаг 3. Запуск обновленияПримените измененную конфигурацию стека:bashsudo docker stack deploy -c prod-stack.yml hub
Используйте код с осторожностью.Как это будет работать теперь:Docker Swarm поднимет контейнеры onlyoffice-db-balancer.OnlyOffice при старте увидит корректный одиночный адрес DB_HOST=onlyoffice-db-balancer, встроенный nc успешно пройдет проверку за долю секунды.Весь трафик к базе данных пойдет через HAProxy, который будет балансировать запросы по кругу на .36, .37 и .38 хосты операционной системы. При падении любой ноды базы HAProxy за 4 секунды (fall 3) исключит её из пула.Запускайте деплой. Давайте проверим логи docker service logs hub_onlyoffice --tail 50 — проскочит ли контейнер этап проверки базы данных?hub_onlyoffice.0.xcov6n551jiy@onlyoffice-stack-3    | Waiting for connection to the onlyoffice-db-balancer host on port 3306
hub_onlyoffice.0.sxx5feu6lj69@onlyoffice-stack-2    | nc: getaddrinfo for host "onlyoffice-db-balancer" port 3306: Name or service not known
hub_onlyoffice.0.sxx5feu6lj69@onlyoffice-stack-2    | Waiting for connection to the onlyoffice-db-balancer host on port 3306
hub_onlyoffice.0.o5fmg14mhwww@onlyoffice-stack-1    | nc: getaddrinfo for host "onlyoffice-db-balancer" port 3306: Name or service not known
hub_onlyoffice.0.o5fmg14mhwww@onlyoffice-stack-1    | Waiting for connection to the onlyoffice-db-balancer host on port 3306
hub_onlyoffice.0.xcov6n551jiy@onlyoffice-stack-3    | nc: getaddrinfo for host "onlyoffice-db-balancer" port 3306: Name or service not known
hub_onlyoffice.0.xcov6n551jiy@onlyoffice-stack-3    | Waiting for connection to the onlyoffice-db-balancer host on port 3306Ошибка nc: getaddrinfo ... Name or service not known означает, что контейнеры OnlyOffice вообще не могут разрешить имя onlyoffice-db-balancer через внутренний DNS Docker.В Docker Swarm есть особенность: внутренний DNS-резолвер не создаст запись для сервиса, пока хотя бы один контейнер этого сервиса-балансировщика (haproxy) физически не перейдет в статус Running [1]. Если HAProxy долго запускается или падает, для OnlyOffice этого имени просто не существует в сети.Давайте найдем причину блокировки HAProxy.Шаг 1. Проверяем состояние балансировщикаВыполните команду на менеджер-ноде, чтобы увидеть текущий статус HAProxy:bashsudo docker stack ps hub | grep balancer
Используйте код с осторожностью.Сценарий А: Если вы видите статус Rejected или Failed с ошибкой вроде «invalid volume» или «permission denied», значит HAProxy не может прочитать файл конфигурации из GlusterFS.Сценарий Б: Если он висит в Preparing или Starting, Docker еще скачивает образ haproxy:2.8-alpine на ноды stack-2 и stack-3.Шаг 2. Самая частая причина (Ошибка чтения haproxy.cfg)Контейнер HAProxy в образе Alpine по умолчанию работает под встроенным пользователем haproxy (UID 99). Если папка /mnt/gluster-storage/haproxy, которую мы создали, принадлежит пользователю root, контейнер не может заглянуть внутрь и прочитать файл конфигурации.Давайте исправим права доступа на GlusterFS (выполните на любой одной ноде хоста):bashsudo chmod -R 755 /mnt/gluster-storage/haproxy
sudo chown -R 99:99 /mnt/gluster-storage/haproxy
Используйте код с осторожностью.Шаг 3. Использование DNS-имени с префиксом стекаЕсли команда из Шага 1 показывает, что HAProxy запущен и работает (Running), то OnlyOffice не видит его из-за того, что имя сервиса внутри overlay-сети должно включать имя стека.Поскольку наш стек называется hub, попробуйте изменить переменную в prod-stack.yml внутри секции onlyoffice на полное имя:yaml      - DB_HOST=hub_onlyoffice-db-balancer
Используйте код с осторожностью.После изменения примените стек: sudo docker stack deploy -c prod-stack.yml hub.Выполните команду sudo docker stack ps hub | grep balancer, чтобы увидеть точный статус прокси-сервиса. Что именно мешает HAProxy запуститься?| [ALERT]    (1) : config : Error(s) found in configuration file : /usr/local/etc/haproxy/haproxy.cfg
hub_onlyoffice-db-balancer.1.ovxjb3egevlo@onlyoffice-stack-1    | [ALERT]    (1) : config : Fatal errors found in configuration.
hub_onlyoffice-db-balancer.2.vivbaqvokrto@onlyoffice-stack-1    | [NOTICE]   (1) : haproxy version is 2.8.24-eea79933В HAProxy версии 2.8+ директива option tcp-check устарела и была удалена из синтаксиса. Современная версия считает её критической ошибкой, из-за чего контейнер onlyoffice-db-balancer падает на этапе парсинга конфигурации.В режиме mode tcp для проверки баз данных достаточно оставить ключевое слово check в строке каждого сервера. Балансировщик будет самостоятельно пинговать порт 3306.Давайте исправим конфигурационный файл и вернем чистоту в логи.Шаг 1. Исправление файла haproxy.cfgОткройте конфигурационный файл на хосте:bashsudo nano /mnt/gluster-storage/haproxy/haproxy.cfg
Используйте код с осторожностью.Замените весь блок backend galera_nodes на этот исправленный вариант (строка option tcp-check удалена, а метод балансировки заменен на leastconn — это более эффективная практика для СУБД, направляющая запрос на менее загруженную ноду базы):haproxy# Распределение трафика на физические хосты Ubuntu
backend galera_nodes
    mode tcp
    balance leastconn      # Направляем новые сессии на наименее загруженную ноду
   
    # Прописываем ваши реальные ноды Galera Cluster
    server galera1 10.10.1.36:3306 check inter 2s rise 2 fall 3
    server galera2 10.10.1.37:3306 check inter 2s rise 2 fall 3
    server galera3 10.10.1.38:3306 check inter 2s rise 2 fall 3
Используйте код с осторожностью.Шаг 2. Принудительный перезапуск балансировщикаПоскольку HAProxy упал, Docker Swarm пытается перезапустить его. Поможем ему применить новый файл конфигурации мгновенно:bashsudo docker service update --force hub_onlyoffice-db-balancer
Используйте код с осторожностью.Шаг 3. Финальная проверка цепочкиУбедитесь, что балансировщик перешел в статус Running:bashsudo docker stack ps hub | grep balancer
Используйте код с осторожностью.Посмотрите свежие логи onlyoffice. Ошибка разрешения имени или таймаута netcat должна исчезнуть, уступив место строкам инициализации приложения:bashsudo docker service logs hub_onlyoffice --tail 50
Используйте код с осторожностью.Как только балансировщик поднимется, DNS-имя заработает. Проверяем логи OnlyOffice
#10
1. Распределенное хранилище: GlusterFSГде живет: Напрямую на операционной системе всех трех нод.Зачем нужно: Это наше «общее облачное подземелье». Оно объединяет локальные диски трех серверов в один виртуальный жесткий диск, примонтированный в /mnt/gluster-storage.Как работает: Когда Caddy сохраняет SSL-сертификат или пользователь открывает документ в OnlyOffice, файл физически записывается на GlusterFS. В ту же миллисекунду по сети этот файл дублируется на остальные два сервера. Если сервер №1 сгорит, сервер №2 и №3 имеют точную копию всех файлов и продолжают работу без потери данных.2. Сердце данных: Galera Cluster (MariaDB)Где живет: Напрямую на хостах Ubuntu (вне Docker, для максимальной стабильности и скорости).Зачем нужно: Хранит информацию о пользователях, активных сессиях редактирования, правах доступа и истории изменений документов.Как работает: В отличие от классической схемы «Мастер-Слейв» (где один сервер главный, а остальные только копируют), Galera — это кластер Multi-Master. Все три ноды базы данных абсолютно равноправны и активны. Мы прописали в OnlyOffice все три IP-адреса через запятую. Если «падает» база на первой ноде, OnlyOffice за доли секунды переключает свои SQL-запросы на вторую ноду. Никаких сбоев при открытии документов.3. Диспетчер задач: RabbitMQГде живет: Внутри Docker Swarm.Зачем нужно: Это брокер очередей сообщений. Когда 1000 человек одновременно печатают текст, отправляют комментарии или сохраняют файлы, OnlyOffice не может обрабатывать всё это мгновенно в один поток — сервер бы завис.Как работает: RabbitMQ принимает все действия пользователей (например: «Иван вставил картинку», «Петр нажал Сохранить») и выстраивает их в аккуратную очередь. Ноды OnlyOffice подходят к этой очереди и разбирают задачи по мере своих сил. Это защищает систему от перегрузки: даже если начнется дикий наплыв пользователей, RabbitMQ удержит удары, и серверы не упадут, а просто будут обрабатывать задачи последовательно.4. Оперативная память кластера: RedisГде живет: Внутри Docker Swarm.Зачем нужно: Самый критичный компонент для коллективной работы. Отвечает за кэширование и синхронизацию сессий «на лету».Как работает: Когда 5 человек одновременно редактируют один и тот же отчет, их изменения не пишутся на жесткий диск каждую секунду (это бы убило GlusterFS). Все правки, движения курсоров и выделения текста мгновенно улетают в Redis — сверхбыструю базу данных, работающую прямо в оперативной памяти. Из Redis эти правки мгновенно транслируются всем остальным редакторам. Именно благодаря Redis пользователи видят изменения друг друга в реальном времени без задержек.Как всё это работает вместе (Путь одного запроса):Пользователь заходит в Nextcloud и открывает документ report.docx.Запрос прилетает на Caddy (который запущен на всех серверах). Caddy понимает, что это запрос к OnlyOffice, и перенаправляет его внутрь Docker Swarm на сервис OnlyOffice.Встроенный балансировщик Docker Swarm выбирает наименее загруженную ноду OnlyOffice (например, на третьем сервере) и отдает запрос ей.Нода OnlyOffice идет в Galera Cluster, проверяет сессию, затем заглядывает в GlusterFS, считывает сам файл документа, кэширует его в Redis и открывает пользователю в браузере.Когда пользователь начинает печатать, RabbitMQ и Redis подхватывают его действия, координируя их с другими участниками, а после закрытия вкладки OnlyOffice собирает все правки из оперативной памяти и сохраняет финальный файл обратно в GlusterFS.Каждый элемент дублирует друг друга. Это классическая, взрослая Production-архитектура, которую используют крупные корпорации.
🡱 🡳

Отметьте интересные вам фрагменты текста и они станут доступны по уникальной ссылке в адресной строке браузера.