Publish cloudflare-balancer Docker image / build-and-push (push) Successful in 34s
310 lines
17 KiB
Markdown
310 lines
17 KiB
Markdown
# Cloudflare DNS pool balancer
|
||
|
||
Лёгкий контейнер на **Alpine**: периодически проверяет доступность бэкендов (**ping** или **HTTP**), синхронизирует записи **A / AAAA** для одного DNS-имени (**пул**) в Cloudflare через **API Token**, пишет цветные логи и опционально отдаёт **HTML-страницу статуса** с ограничением по IP.
|
||
|
||
---
|
||
|
||
## Оглавление
|
||
|
||
- [Как это работает](#как-это-работает)
|
||
- [Требования](#требования)
|
||
- [Сборка образа (Linux / bash)](#сборка-образа-linux--bash)
|
||
- [Быстрый старт](#быстрый-старт)
|
||
- [Переменные окружения](#переменные-окружения)
|
||
- [Cloudflare: API Token](#cloudflare-api-token)
|
||
- [Примеры запуска](#примеры-запуска)
|
||
- [Docker Compose](#docker-compose)
|
||
- [Веб-интерфейс статуса и whitelist](#веб-интерфейс-статуса-и-whitelist)
|
||
- [Логи](#логи)
|
||
- [Ограничения](#ограничения)
|
||
- [Устранение неполадок](#устранение-неполадок)
|
||
- [Сборка в CI (Gitea Actions)](#сборка-в-ci-gitea-actions)
|
||
- [Безопасность](#безопасность)
|
||
- [Windows и WSL2](#windows-и-wsl2)
|
||
|
||
---
|
||
|
||
## Как это работает
|
||
|
||
1. Для каждой цели из `CHECK_TARGETS` определяется IP (прямой адрес, резолв имени или хоста из URL).
|
||
2. Выполняется проверка: **ICMP ping** к IP или **curl** по URL (см. `CHECK_MODE`).
|
||
3. IP считается **желательным в пуле**, только если **все** цели, которые резолвятся в этот IP, прошли проверку.
|
||
4. Читаются текущие записи **A/AAAA** с именем `POOL_DOMAIN` в Cloudflare.
|
||
5. Лишние записи удаляются, недостающие создаются (TTL и proxied из переменных окружения).
|
||
|
||
Два разных смысла **curl**:
|
||
|
||
- **Проверка бэкендов** (`CHECK_MODE=curl`) — запросы к URL из целей / шаблону `CURL_URL_TEMPLATE`.
|
||
- **Порт `STATUS_HTTP_PORT`** — только встроенная страница статуса внутри контейнера, не health-check.
|
||
|
||
### CHECK_TARGETS: не только «локальные» адреса
|
||
|
||
В инструкции в примерах часто встречаются частные IP — это лишь **пример**. Формат **тот же** для любых целей, которые контейнер реально может проверить:
|
||
|
||
| Тип | Примеры |
|
||
|-----|---------|
|
||
| Частные IP | `10.0.0.1`, `192.168.1.10`, `172.16.0.5` |
|
||
| Публичные (глобальные) IP | `203.0.113.50`, адреса VPS/анонсеров |
|
||
| Внутренние FQDN | `backend.prod.local`, `node1.dc.company.internal` |
|
||
| Публичные домены | `api.example.com`, `origin.example.org` |
|
||
| URL (режим `curl`) | `https://api.example.com/health`, `http://203.0.113.1:8080/ping` |
|
||
|
||
Имя резолвится через **DNS из контейнера** (`dig`); для сопоставления с пулом Cloudflare используется полученный **A/AAAA**. Проверка — обычный **ping** к IP или **curl** к URL.
|
||
|
||
Единственное условие: **из сети контейнера** до цели должен доходить трафик (маршрутизация, файрвол, `docker network`, VPN и т.д.). Публичные хосты в интернете обычно доступны из стандартного bridge; частные адреса за NAT без проброса/маршрута — нет, пока не настроите сеть.
|
||
|
||
---
|
||
|
||
## Требования
|
||
|
||
- **Docker Engine** на Linux (или эквивалент с Linux-контейнерами).
|
||
- У контейнера должен быть доступ в интернет (Cloudflare API, ping/curl к бэкендам).
|
||
- Для **ping** из пользователя `nobody` в образе выставлен `cap_net_raw` на `/usr/bin/ping`. Если ping не работает, запускайте с **`--cap-add=NET_RAW`** (в `docker-compose.yml` это уже указано).
|
||
|
||
---
|
||
|
||
## Сборка образа (Linux / bash)
|
||
|
||
Из корня репозитория:
|
||
|
||
```bash
|
||
cd /path/to/cloudflare_balancer
|
||
docker build -t cloudflare-balancer:latest .
|
||
```
|
||
|
||
Проверка локально (без реальных секретов контейнер сразу завершится с ошибкой — это нормально):
|
||
|
||
```bash
|
||
docker build -t cloudflare-balancer:latest . && echo "сборка OK"
|
||
```
|
||
|
||
---
|
||
|
||
## Быстрый старт
|
||
|
||
Минимально нужны: `POOL_DOMAIN`, `CHECK_TARGETS`, `CLOUDFLARE_API_TOKEN`.
|
||
|
||
Рекомендуется передавать секреты через **`--env-file`**:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
# отредактируйте .env
|
||
|
||
docker run --rm \
|
||
--cap-add=NET_RAW \
|
||
--env-file .env \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
---
|
||
|
||
## Переменные окружения
|
||
|
||
| Переменная | Обязательно | По умолчанию | Описание |
|
||
|------------|-------------|--------------|----------|
|
||
| `POOL_DOMAIN` | да | — | DNS-имя пула (все **A/AAAA** с этим именем управляются скриптом). |
|
||
| `CHECK_TARGETS` | да | — | Список через **запятую**: любые **достижимые** цели — частные/публичные **IP**, внутренние или **глобальные** имена, либо `http(s)://...` (подробнее — подраздел **«CHECK_TARGETS: не только „локальные“ адреса»** выше). |
|
||
| `CLOUDFLARE_API_TOKEN` | да* | — | Bearer-токен Cloudflare. |
|
||
| `CLOUDFLARE_API_KEY` | нет | — | Алиас для токена (если не задан `CLOUDFLARE_API_TOKEN`). |
|
||
| `CLOUDFLARE_ZONE_ID` | нет | — | ID зоны; если пусто — поиск по имени зоны. |
|
||
| `CLOUDFLARE_ZONE_NAME` | нет | — | Имя зоны (apex), например `example.com`; если пусто — эвристика: **две последние метки** `POOL_DOMAIN`. |
|
||
| `CHECK_INTERVAL_SEC` | нет | `30` | Пауза между циклами, сек. |
|
||
| `CHECK_MODE` | нет | `ping` | `ping` или `curl`. |
|
||
| `PING_COUNT` | нет | `2` | Число ICMP-запросов. |
|
||
| `PING_TIMEOUT_SEC` | нет | `2` | Таймаут ping (см. `iputils-ping`). |
|
||
| `CURL_URL_TEMPLATE` | нет | `http://%s/` | Для не-URL целей: один плейсхолдер `%s` заменяется на цель. |
|
||
| `CURL_MAX_TIME_SEC` | нет | `5` | `--max-time` для curl. |
|
||
| `CURL_CONNECT_TIMEOUT_SEC` | нет | `3` | `--connect-timeout`. |
|
||
| `CURL_OK_MIN` | нет | `200` | Минимальный допустимый HTTP-код. |
|
||
| `CURL_OK_MAX` | нет | `399` | Максимальный допустимый HTTP-код. |
|
||
| `DEFAULT_TTL` | нет | `300` | TTL при создании записи (для **прокси**-записей Cloudflare сам использует авто-TTL). |
|
||
| `DEFAULT_PROXIED` | нет | `false` | `true` / `false` — оранжевое облако. |
|
||
| `ENABLE_STATUS_HTTP` | нет | `0` | `1` — включить HTTP-страницу статуса. |
|
||
| `STATUS_HTTP_PORT` | нет | `8080` | Порт **внутри контейнера** для страницы статуса. |
|
||
| `STATUS_HTTP_ALLOW_IPS` | нет | *(пусто)* | Whitelist: IP и IPv4 **CIDR** через запятую. Пусто = слушать только **127.0.0.1**. |
|
||
|
||
\* Обязателен токен или алиас.
|
||
|
||
> **Важно:** эвристика зоны по двум последним меткам не подходит для всех публичных суффиксов (например некоторые зоны второго уровня). В сомнениях задайте `CLOUDFLARE_ZONE_ID` или `CLOUDFLARE_ZONE_NAME`.
|
||
|
||
---
|
||
|
||
## Cloudflare: API Token
|
||
|
||
1. Cloudflare Dashboard → **My Profile** → **API Tokens** → **Create Token**.
|
||
2. Шаблон **Edit zone DNS** или кастомный минимум:
|
||
- **Zone** — **DNS** — **Edit**
|
||
- **Zone** — **Zone** — **Read**
|
||
- Ограничить **конкретной зоной** (рекомендуется).
|
||
3. Скопируйте токен в `.env` как `CLOUDFLARE_API_TOKEN`.
|
||
|
||
Официальная документация API: [Cloudflare API](https://developers.cloudflare.com/api/).
|
||
|
||
---
|
||
|
||
## Примеры запуска
|
||
|
||
### Только ping, без UI
|
||
|
||
```bash
|
||
docker run --rm --cap-add=NET_RAW \
|
||
-e POOL_DOMAIN=app.example.com \
|
||
-e CHECK_TARGETS='10.0.0.1,10.0.0.2' \
|
||
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
|
||
-e CHECK_INTERVAL_SEC=20 \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
### Режим curl и шаблон URL
|
||
|
||
```bash
|
||
docker run --rm --cap-add=NET_RAW \
|
||
-e POOL_DOMAIN=app.example.com \
|
||
-e CHECK_TARGETS='backend1.internal,backend2.internal' \
|
||
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
|
||
-e CHECK_MODE=curl \
|
||
-e CURL_URL_TEMPLATE='http://%s:8080/health' \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
### Полный URL как цель
|
||
|
||
```bash
|
||
docker run --rm --cap-add=NET_RAW \
|
||
-e POOL_DOMAIN=app.example.com \
|
||
-e CHECK_TARGETS='https://node1.example.com/health' \
|
||
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
|
||
-e CHECK_MODE=curl \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
### Смешанные цели: частные IP, публичный домен, глобальный IP
|
||
|
||
```bash
|
||
docker run --rm --cap-add=NET_RAW \
|
||
-e POOL_DOMAIN=app.example.com \
|
||
-e CHECK_TARGETS='10.0.0.1,origin.example.com,203.0.113.10' \
|
||
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
*(Замените `203.0.113.10` на реальный публичный IP бэкенда; `origin.example.com` — на любой FQDN, который резолвится и отвечает на ping из контейнера.)*
|
||
|
||
### Веб-статус + проброс порта + whitelist
|
||
|
||
При доступе **с хоста Docker** клиентский IP в контейнере часто совпадает с адресом **шлюза bridge** (часто `172.17.0.1` или подсеть `172.17.0.0/16`). Добавьте её или конкретный IP в whitelist.
|
||
|
||
```bash
|
||
docker run --rm --cap-add=NET_RAW \
|
||
-p 8080:8080 \
|
||
-e POOL_DOMAIN=app.example.com \
|
||
-e CHECK_TARGETS='10.0.0.1' \
|
||
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
|
||
-e ENABLE_STATUS_HTTP=1 \
|
||
-e STATUS_HTTP_PORT=8080 \
|
||
-e STATUS_HTTP_ALLOW_IPS='127.0.0.1,172.17.0.0/16,10.0.0.0/8' \
|
||
cloudflare-balancer:latest
|
||
```
|
||
|
||
Откройте в браузере: `http://127.0.0.1:8080/` (если whitelist это разрешает).
|
||
|
||
---
|
||
|
||
## Docker Compose
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
# заполните CLOUDFLARE_API_TOKEN и остальное
|
||
|
||
docker compose build
|
||
docker compose up -d
|
||
docker compose logs -f
|
||
```
|
||
|
||
Файл [docker-compose.yml](docker-compose.yml) подключает `.env` и добавляет **`NET_RAW`** для ping.
|
||
|
||
---
|
||
|
||
## Веб-интерфейс статуса и whitelist
|
||
|
||
- **`STATUS_HTTP_PORT`** — порт HTTP-сервера **статуса**, не имеет отношения к `CHECK_MODE=curl`.
|
||
- Если **`STATUS_HTTP_ALLOW_IPS` пустой** — `socat` слушает **127.0.0.1** (доступ с хоста через `-p` без NAT к loopback контейнера обычно **не** попадёт на 127.0.0.1 внутри контейнера). Для просмотра со стороны хоста задайте whitelist и `0.0.0.0`-bind (это делается автоматически при непустом whitelist).
|
||
- В whitelist поддерживаются **точные IPv4** и **IPv4 CIDR**; для IPv6 — в основном точное совпадение (CIDR для v6 в обработчике минимальный).
|
||
|
||
---
|
||
|
||
## Логи
|
||
|
||
```bash
|
||
docker logs -f <container_id>
|
||
```
|
||
|
||
В логах используются **ANSI-цвета** (удобно в терминале). Блоки: заголовок цикла, строки **OK/FAIL** по целям, действия **DELETE/POST** в API.
|
||
|
||
---
|
||
|
||
## Ограничения
|
||
|
||
- Одна строка в `CHECK_TARGETS` с запятой внутри URL может сломать разбор — избегайте запятых в целях или используйте только один хост на цель.
|
||
- Несколько имён, резолвящихся в **один IP**, считаются одним бэкендом: IP остаётся в пуле только если **все** такие проверки успешны.
|
||
- Записи **A/AAAA** для `POOL_DOMAIN`, которые **не соответствуют** IP из текущего резолва целей, **не удаляются** (в лог и HTML выводится предупреждение «вне списка»).
|
||
|
||
---
|
||
|
||
## Устранение неполадок
|
||
|
||
| Симптом | Что проверить |
|
||
|--------|----------------|
|
||
| Ошибка авторизации Cloudflare | Токен, срок, зона в области действия токена. |
|
||
| «Зона не найдена» | Задайте `CLOUDFLARE_ZONE_ID` или корректный `CLOUDFLARE_ZONE_NAME`. |
|
||
| Все ping FAIL | `NET_RAW` / `cap_add`; маршрутизация сети контейнера до бэкендов; ICMP может быть запрещён файрволом. |
|
||
| curl всегда FAIL | URL, TLS, коды ответа (`CURL_OK_MIN` / `CURL_OK_MAX`), таймауты. |
|
||
| UI отдаёт 403 | Whitelist: добавьте IP шлюза Docker или вашу подсеть. |
|
||
| После старта пул «пустой» | При всех FAIL записи удаляются — проверьте доступность целей и корректность IP. |
|
||
|
||
---
|
||
|
||
## Сборка в CI (Gitea Actions)
|
||
|
||
1. На сервере Gitea включите **Actions** и подключите **runner** ([документация](https://docs.gitea.com/usage/actions/overview)).
|
||
2. Workflow: [.gitea/workflows/docker.yml](.gitea/workflows/docker.yml) — **Buildx**, `docker/login-action`, `docker/build-push-action`; пути и метаданные из контекста **`gitea.*`** (`repository`, `actor`, `sha`, `ref_name`, `server_url`).
|
||
|
||
**Авторизация в registry**
|
||
|
||
- Отдельный **`ACTIONS_PAT` не нужен**: в `docker login` используется встроенный **`gitea.token`** (токен текущего запуска workflow) и **`gitea.actor`**. У job задано **`permissions: packages: write`** — этого достаточно для push в Container Registry на том же Gitea (при несовместимости вашей версии см. [документацию Gitea Actions](https://docs.gitea.com/usage/actions/overview)).
|
||
|
||
**Секрет для обновления контейнера (опционально)**
|
||
|
||
- **`CONTAINER_UPDATE_URL`** — полный URL webhook после успешного push (например ссылка **Redeploy** в Portainer, другой оркестратор). Workflow делает **`curl` (GET)** по этому URL; если секрет пустой, шаг только пишет в лог и выходит. При необходимости другого метода (POST) измените шаг в `docker.yml`.
|
||
|
||
**Переменные в workflow**
|
||
|
||
- **`env.REGISTRY`** в начале `docker.yml` — хост registry **без** схемы (`https://`), как для `docker pull` (в файле задан пример `git.shts.su`; замените при переносе на другой инстанс).
|
||
- **`IMAGE_REPO`:** `${{ gitea.repository }}` — полный путь образа: `${REGISTRY}/${{ gitea.repository }}`.
|
||
|
||
**Триггеры:** push по всем веткам и тегам, **`workflow_dispatch`**.
|
||
|
||
**Теги:** `latest`, «безопасное» имя ветки/тега (`/` → `-`), `sha-<12 hex>`.
|
||
|
||
**Job:** `permissions: contents: read`, `packages: write`.
|
||
|
||
---
|
||
|
||
## Безопасность
|
||
|
||
- Не коммитьте `.env` и реальные токены.
|
||
- Минимизируйте права API Token одной зоной.
|
||
- Не публикуйте порт статуса в интернет без **whitelist** и без понимания сетевой модели Docker.
|
||
|
||
---
|
||
|
||
## Windows и WSL2
|
||
|
||
Основные команды из этой инструкции рассчитаны на **bash под Linux**. На Windows удобнее запускать Docker **внутри WSL2** и выполнять те же `docker build` / `docker run` из Ubuntu (или другого дистрибутива в WSL).
|
||
|
||
---
|
||
|
||
## Лицензия
|
||
|
||
Проект в репозитории пользователя — при необходимости добавьте файл `LICENSE` отдельно.
|