Add Ansible playbook to deliver validator-agent to the validators
Run from the jump host: on each validator it updates the git clone in /opt/cloud-ip-validator, builds the image there, stops and removes the current container and starts a new one from the new image. Run parameters live in an env file (deploy/ansible/env/validator-agent.env, git-ignored, template committed). The image is built before the running container is touched, so a failed build leaves the old container running. Hosts are updated in waves (1, 4, rest) and any failure stops the run. validator_id comes from the inventory and is checked against the running container before it is replaced. Only ansible.builtin modules are used, so the validators need no extra packages. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abbee9a08a
commit
49890ff5de
16 files changed
+684
-1
No files matched your search
@@ -5,6 +5,8 @@
|
|||||||
!/deploy/docker/.env.example
|
!/deploy/docker/.env.example
|
||||||
!/deploy/docker/.env.prod.example
|
!/deploy/docker/.env.prod.example
|
||||||
/deploy/docker/control-api/control-api.docker.yaml
|
/deploy/docker/control-api/control-api.docker.yaml
|
||||||
|
# Ansible: рабочий env-файл запуска validator-agent (шаблон .example остаётся в git).
|
||||||
|
/deploy/ansible/env/*.env
|
||||||
/rxprod-compose/.env
|
/rxprod-compose/.env
|
||||||
|
|
||||||
# Live runtime database for the rxprod-compose control-api container.
|
# Live runtime database for the rxprod-compose control-api container.
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ docs/ документация и планы доработок
|
|||||||
## Публикация и эксплуатация
|
## Публикация и эксплуатация
|
||||||
- **Бинарники.** Готовые linux/amd64 лежат в `bin/` и **не обновляются автоматически**: после правок кода пересоберите их и обновите `SHA256SUMS` (команды — [docs/SETUP.md](docs/SETUP.md#вариант-b-сборка-из-исходников)); Dockerfile копируют именно `bin/*`.
|
- **Бинарники.** Готовые linux/amd64 лежат в `bin/` и **не обновляются автоматически**: после правок кода пересоберите их и обновите `SHA256SUMS` (команды — [docs/SETUP.md](docs/SETUP.md#вариант-b-сборка-из-исходников)); Dockerfile копируют именно `bin/*`.
|
||||||
- **systemd.** Юниты в `deploy/systemd/`; у `control-api` — `EnvironmentFile` с учётными данными OpenStack.
|
- **systemd.** Юниты в `deploy/systemd/`; у `control-api` — `EnvironmentFile` с учётными данными OpenStack.
|
||||||
- **Docker.** `deploy/docker/docker-compose.yml` + `docker-compose.override.yml` (dev, mock) или `docker-compose.prod.yml` (без публикации портов, `restart: unless-stopped`). Какие сервисы поднимаются на хосте, задаёт `COMPOSE_PROFILES`: `control-plane`, `dashboard`, `prober`, `validator`. БД — volume `cloud-ip-validator-db`.
|
- **Docker.** `deploy/docker/docker-compose.yml` + `docker-compose.override.yml` (dev, mock) или `docker-compose.prod.yml` (без публикации портов, `restart: unless-stopped`). Какие сервисы поднимаются на хосте, задаёт `COMPOSE_PROFILES`: `control-plane`, `dashboard`, `prober`, `validator`. БД — volume `cloud-ip-validator-db`. Массовая доставка `validator-agent` на валидаторы (сборка образа на каждом хосте, замена контейнера) — Ansible-сценарий [`deploy/ansible/`](deploy/ansible/README.md).
|
||||||
- **Реальный стенд.** `rxprod-compose/` — compose с готовыми образами, собственным `control-api.yaml` и каталогом БД `capi-db/`; `.env` с учётными данными в репозиторий не входит.
|
- **Реальный стенд.** `rxprod-compose/` — compose с готовыми образами, собственным `control-api.yaml` и каталогом БД `capi-db/`; `.env` с учётными данными в репозиторий не входит.
|
||||||
- **Миграции** применяются при старте `control-api`; версия схемы — `PRAGMA user_version`. Начальная загрузка (`validators`, `sites`, `targets`, `check_types`, `inbound_checks`) выполняется только в пустые таблицы.
|
- **Миграции** применяются при старте `control-api`; версия схемы — `PRAGMA user_version`. Начальная загрузка (`validators`, `sites`, `targets`, `check_types`, `inbound_checks`) выполняется только в пустые таблицы.
|
||||||
- Остановка (`SIGTERM`) корректно завершает HTTP-сервер и фоновые циклы. Состояние автоцикла и очереди сохраняется в БД.
|
- Остановка (`SIGTERM`) корректно завершает HTTP-сервер и фоновые циклы. Состояние автоцикла и очереди сохраняется в БД.
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Доставка validator-agent на валидаторы (Ansible)
|
||||||
|
|
||||||
|
Сценарий запускается с jump-хоста и на каждой ВМ-валидаторе: обновляет git-клон в `/opt/cloud-ip-validator`,
|
||||||
|
**собирает образ на самом хосте**, останавливает и удаляет старый контейнер `cloud-ip-validator-validator-agent`
|
||||||
|
и поднимает на его месте новый. Параметры запуска агента вынесены в env-файл.
|
||||||
|
|
||||||
|
Порядок безопасен: сначала проверки, обновление кода и сборка образа, и только потом замена контейнера. Если что-то
|
||||||
|
упало до замены, старый контейнер продолжает работать. Простой валидатора — секунды (`stop` + `rm` + `run`).
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
| Где | Что |
|
||||||
|
|---|---|
|
||||||
|
| jump-хост (Debian 13) | `apt install ansible-core`; SSH-доступ по ключу ко всем валидаторам |
|
||||||
|
| валидаторы | Docker, git, клон репозитория в `/opt/cloud-ip-validator`, `sudo` без пароля для SSH-пользователя, доступ к Gitea и Docker Hub (`alpine:3.20`), архитектура x86_64 |
|
||||||
|
|
||||||
|
Только модули `ansible.builtin`: Python Docker SDK и дополнительные коллекции на валидаторах не нужны.
|
||||||
|
Go на валидаторах не нужен: образ копирует закоммиченный `bin/validator-agent` (его сумма проверяется по `bin/SHA256SUMS`).
|
||||||
|
|
||||||
|
## Подготовка (один раз)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deploy/ansible
|
||||||
|
cp env/validator-agent.env.example env/validator-agent.env
|
||||||
|
chmod 600 env/validator-agent.env
|
||||||
|
$EDITOR env/validator-agent.env # адрес control-api, токен, способы самопроверки, таймауты
|
||||||
|
ansible validators -m ping # проверка связи (20 хостов: validator-1 ... validator-20)
|
||||||
|
```
|
||||||
|
|
||||||
|
- В `inventory/hosts.yml` 20 хостов `validator-1 … validator-20` с адресами (`ansible_host`) и переменной `validator_id`
|
||||||
|
(`vkiplab-v1 … vkiplab-v20`, как в control-api). Соответствие `validator-N` → `vkiplab-vN` задано по порядку номеров;
|
||||||
|
**сценарий сверяет `validator_id` с тем, что записано в работающем контейнере на хосте, и при расхождении останавливается до замены
|
||||||
|
контейнера**. `VALIDATOR_AGENT_VALIDATOR_ID` в env-файл писать не нужно: сценарий подставляет его из inventory.
|
||||||
|
- Отпечатки хостов запоминаются при первом подключении (`StrictHostKeyChecking=accept-new` в `ansible.cfg`); сменившийся отпечаток известного хоста — ошибка.
|
||||||
|
- SSH: пользователь `debian` и ключ `~/.ssh/vk_cloud_priv.key` на jump-хосте (права 0600) — одинаковые на всех хостах; меняются в
|
||||||
|
`inventory/group_vars/validators.yml` (`ansible_user`, `ansible_ssh_private_key_file`). Для docker и записи env-файла сценарий
|
||||||
|
повышает права через `sudo`.
|
||||||
|
- Токен в env-файле можно зашифровать: `ansible-vault encrypt env/validator-agent.env`, запускать с `--ask-vault-pass`.
|
||||||
|
Рабочий `env/validator-agent.env` в git не попадает (`.gitignore`).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deploy/ansible
|
||||||
|
ansible-playbook playbooks/deploy-validator-agent.yml --limit vkiplab-v1 # канарейка: один валидатор
|
||||||
|
ansible-playbook playbooks/deploy-validator-agent.yml # все валидаторы волнами
|
||||||
|
ansible-playbook playbooks/deploy-validator-agent.yml --check # только проверки (preflight), без изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
Волны по умолчанию: 1 хост, затем 4, затем все остальные (`deploy_serial: [1, 4, "100%"]`). Любой сбой в волне останавливает
|
||||||
|
прогон: следующая волна не начнётся. Все сразу: `-e '{"deploy_serial": ["100%"]}'`.
|
||||||
|
|
||||||
|
Что выкатывается — `deploy_ref` (по умолчанию `main`): ветка, тег или коммит. Выкатить нужно **запушенный** коммит:
|
||||||
|
валидаторы берут код из репозитория, а не с jump-хоста. После правок кода сначала пересоберите `bin/validator-agent`
|
||||||
|
(см. [SETUP.md](../../docs/SETUP.md#обновление-образов-после-изменения-кода)) и закоммитьте его вместе с `bin/SHA256SUMS`.
|
||||||
|
|
||||||
|
### Откат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-playbook playbooks/deploy-validator-agent.yml -e deploy_ref=<предыдущий коммит или тег>
|
||||||
|
```
|
||||||
|
|
||||||
|
Для тега или коммита клон переходит в detached HEAD; следующий запуск с `deploy_ref=main` возвращает его на ветку.
|
||||||
|
|
||||||
|
## Что делает сценарий на каждом хосте
|
||||||
|
|
||||||
|
1. **preflight** — env-файл на jump-хосте существует и в нём задан `VALIDATOR_AGENT_CONTROL_API_URL` (адрес-пример
|
||||||
|
`example.com` не принимается); на валидаторе отвечает Docker, есть git и клон, архитектура x86_64; `validator_id` из inventory совпадает
|
||||||
|
с `validator_id` работающего контейнера. Показывает текущий контейнер.
|
||||||
|
2. **git** — `fetch`, затем клон сбрасывается на `deploy_ref` (`checkout --force`). Через `git`, а не модуль `git`:
|
||||||
|
учётные данные, уже настроенные в клоне, не трогаются. Локальные правки отслеживаемых файлов в клоне будут сброшены;
|
||||||
|
неотслеживаемые и игнорируемые (`.env.*`) — нет.
|
||||||
|
3. **build** — проверка `bin/validator-agent` по `bin/SHA256SUMS`; `docker build --platform linux/amd64` с контекстом в корне репозитория,
|
||||||
|
образ получает метку ревизии (`cloud-ip-validator-validator-agent:<хеш>`) и `latest`.
|
||||||
|
4. **replace** — env-файл копируется в `/opt/cloud-ip-validator/deploy/docker/.env.validator` (0600; путь закрыт `.gitignore`),
|
||||||
|
затем `docker stop` → `docker rm -f` → `docker run -d --restart unless-stopped --cap-add NET_RAW --env-file …`.
|
||||||
|
5. **verify** — ждёт строку `registered` в логе агента (регистрация в control-api), проверяет, что контейнер запущен, без перезапусков
|
||||||
|
и на только что собранном образе. При неудаче хост падает с последними строками лога, следующие волны не стартуют.
|
||||||
|
Затем остаются 3 последних образа с метками ревизий (`keep_images`), остальные и «висячие» удаляются.
|
||||||
|
|
||||||
|
Все шаги можно запускать по тегам: `--tags preflight|git|build|replace|verify`.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
**`env/validator-agent.env`** — запуск агента (читает `deploy/docker/validator-agent/docker-entrypoint.sh`):
|
||||||
|
`VALIDATOR_AGENT_CONTROL_API_URL`, `CONTROL_API_AGENT_TOKEN`, `VALIDATOR_AGENT_SELF_CHECK_METHODS`,
|
||||||
|
`VALIDATOR_AGENT_SELF_CHECK_TIMEOUT_SECONDS`, `VALIDATOR_AGENT_POLL_INTERVAL_SECONDS`, `VALIDATOR_AGENT_HTTPS_TIMEOUT_SECONDS`,
|
||||||
|
`VALIDATOR_AGENT_ICMP_TIMEOUT_SECONDS`, `VALIDATOR_AGENT_ICMP_COUNT`, `VALIDATOR_AGENT_SSH_ENABLED`, `VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS`.
|
||||||
|
Смена только env-файла не требует изменения кода: достаточно запустить сценарий (контейнер пересоздаётся с новыми значениями).
|
||||||
|
|
||||||
|
**`inventory/group_vars/validators.yml`** — доставка: `repo_dir`, `repo_remote`, `deploy_ref`, `git_user` (пользователь, у которого в клоне
|
||||||
|
настроен доступ к репозиторию; пусто — SSH-пользователь), `image_name`, `container_name`, `platform`, `keep_images`, `restart_policy`,
|
||||||
|
`capabilities`, `log_max_size`, `log_max_file`, `stop_timeout`, `verify_retries`, `verify_delay`, `local_env_file`. Любой параметр
|
||||||
|
переопределяется ключом `-e`.
|
||||||
|
|
||||||
|
## Разбор сбоев
|
||||||
|
|
||||||
|
- **«Нет env-файла» / «не задан VALIDATOR_AGENT_CONTROL_API_URL»** — см. «Подготовка».
|
||||||
|
- **«в запущенном контейнере validator_id=…, а в inventory …»** — соответствие `validator-N` и `validator_id` в `inventory/hosts.yml`
|
||||||
|
неверно для этого хоста: исправьте inventory (контейнер при этом не тронут).
|
||||||
|
- **Контейнер не прошёл проверку** — в сообщении последние строки лога. `registration failed` — control-api недоступен с валидатора
|
||||||
|
или `validator_id` не заведён в control-api. Контейнер остаётся на хосте для разбора (`docker logs`).
|
||||||
|
- **`dubious ownership`, `could not read Username`** — клон принадлежит другому пользователю или учётные данные git есть у другого:
|
||||||
|
задайте `git_user`.
|
||||||
|
- **`bin/validator-agent` не совпадает с суммой** — в репозитории устарел `bin/SHA256SUMS`: пересоберите бинарник и обновите сумму.
|
||||||
|
- **Сборка падает на `FROM alpine:3.20` / `apk add`** — с валидатора нет доступа к Docker Hub / репозиториям Alpine.
|
||||||
|
- **Старый образ другого имени остаётся** — сценарий чистит только образы `image_name`; образ с прежним именем удалите вручную (`docker rmi`).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Запускать из каталога deploy/ansible (там же лежит этот файл).
|
||||||
|
[defaults]
|
||||||
|
inventory = inventory/hosts.yml
|
||||||
|
roles_path = roles
|
||||||
|
forks = 20
|
||||||
|
retry_files_enabled = False
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
callback_result_format = yaml
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
|
# accept-new: отпечаток нового хоста запоминается при первом подключении (без
|
||||||
|
# интерактивного вопроса); изменившийся отпечаток известного хоста по-прежнему
|
||||||
|
# приводит к ошибке.
|
||||||
|
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=accept-new
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# Параметры запуска validator-agent. Скопируйте в validator-agent.env и заполните:
|
||||||
|
# cp validator-agent.env.example validator-agent.env && chmod 600 validator-agent.env
|
||||||
|
# Файл передаётся контейнеру как `docker run --env-file` (формат KEY=VALUE, без
|
||||||
|
# кавычек и пробелов вокруг "="). Читает их deploy/docker/validator-agent/docker-entrypoint.sh.
|
||||||
|
# VALIDATOR_AGENT_VALIDATOR_ID сюда НЕ пишется: сценарий подставляет имя хоста.
|
||||||
|
|
||||||
|
# Адрес control-api (обязательно). Для внешнего размещения — адрес, доступный
|
||||||
|
# с валидаторов напрямую (через него же работает способ самопроверки control_api).
|
||||||
|
VALIDATOR_AGENT_CONTROL_API_URL=https://control-api.example.com
|
||||||
|
|
||||||
|
# Токен агентов (CONTROL_API_AGENT_TOKEN на стороне control-api). Пусто — если
|
||||||
|
# токен на control-api ещё не включён.
|
||||||
|
CONTROL_API_AGENT_TOKEN=
|
||||||
|
|
||||||
|
# Способы самопроверки в порядке приоритета: ip_echo, control_api.
|
||||||
|
# Самопроверка проходит, если адрес подтвердил любой способ. Без пробелов.
|
||||||
|
VALIDATOR_AGENT_SELF_CHECK_METHODS=[control_api,ip_echo]
|
||||||
|
# Таймаут одного способа, секунд.
|
||||||
|
VALIDATOR_AGENT_SELF_CHECK_TIMEOUT_SECONDS=10
|
||||||
|
|
||||||
|
# Период опроса control-api, секунд.
|
||||||
|
VALIDATOR_AGENT_POLL_INTERVAL_SECONDS=5
|
||||||
|
|
||||||
|
# Исходящие проверки.
|
||||||
|
VALIDATOR_AGENT_HTTPS_TIMEOUT_SECONDS=10
|
||||||
|
VALIDATOR_AGENT_ICMP_TIMEOUT_SECONDS=5
|
||||||
|
VALIDATOR_AGENT_ICMP_COUNT=3
|
||||||
|
VALIDATOR_AGENT_SSH_ENABLED=false
|
||||||
|
VALIDATOR_AGENT_SSH_TIMEOUT_SECONDS=5
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
# Параметры доставки validator-agent (не секреты). Любой из них можно
|
||||||
|
# переопределить в командной строке: -e deploy_ref=<коммит|тег>.
|
||||||
|
# Параметры запуска самого агента (адрес control-api, токен, способы
|
||||||
|
# самопроверки, таймауты) лежат в env-файле, см. local_env_file.
|
||||||
|
|
||||||
|
# SSH: пользователь и ключ одинаковы на jump-хосте и на валидаторах. Путь к
|
||||||
|
# ключу — на jump-хосте (ключ с правами 0600). Для docker и записи env-файла
|
||||||
|
# сценарий повышает права через sudo (become).
|
||||||
|
ansible_user: debian
|
||||||
|
ansible_ssh_private_key_file: ~/.ssh/vk_cloud_priv.key
|
||||||
|
|
||||||
|
# --- git-клон на валидаторе ---------------------------------------------
|
||||||
|
repo_dir: /opt/cloud-ip-validator
|
||||||
|
repo_remote: origin
|
||||||
|
# Ветка, тег или коммит, который нужно выкатить (откат: -e deploy_ref=<коммит>).
|
||||||
|
deploy_ref: main
|
||||||
|
# Пользователь, у которого в клоне настроен доступ к репозиторию (git fetch).
|
||||||
|
# Пусто — git работает от SSH-пользователя без sudo.
|
||||||
|
git_user: ""
|
||||||
|
|
||||||
|
# --- образ и контейнер --------------------------------------------------
|
||||||
|
image_name: cloud-ip-validator-validator-agent
|
||||||
|
container_name: cloud-ip-validator-validator-agent
|
||||||
|
platform: linux/amd64
|
||||||
|
dockerfile: deploy/docker/validator-agent/Dockerfile
|
||||||
|
# Сколько образов с метками ревизий хранить (кроме latest); старые удаляются.
|
||||||
|
keep_images: 3
|
||||||
|
|
||||||
|
# --- запуск контейнера (как в deploy/docker/RUN.txt) --------------------
|
||||||
|
restart_policy: unless-stopped
|
||||||
|
capabilities: [NET_RAW]
|
||||||
|
log_max_size: 10m
|
||||||
|
log_max_file: "3"
|
||||||
|
stop_timeout: 10
|
||||||
|
|
||||||
|
# --- проверка после запуска ---------------------------------------------
|
||||||
|
verify_retries: 10
|
||||||
|
verify_delay: 3
|
||||||
|
|
||||||
|
# --- env-файл на jump-хосте ---------------------------------------------
|
||||||
|
# Параметры запуска агента. Рабочий файл создаётся из validator-agent.env.example
|
||||||
|
# и в git не попадает. Файл можно зашифровать: ansible-vault encrypt <файл>
|
||||||
|
# (тогда запускайте с --ask-vault-pass или --vault-password-file).
|
||||||
|
local_env_file: "{{ playbook_dir }}/../env/validator-agent.env"
|
||||||
|
# Куда файл копируется на валидатор (путь закрыт .gitignore репозитория,
|
||||||
|
# переживает git reset).
|
||||||
|
remote_env_file: "{{ repo_dir }}/deploy/docker/.env.validator"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Валидаторы. Имя хоста (validator-N) — это имя ВМ; validator_id в control-api
|
||||||
|
# другой (vkiplab-vN) и задаётся переменной validator_id. Соответствие
|
||||||
|
# validator-N -> vkiplab-vN предполагается по порядку номеров; сценарий
|
||||||
|
# сверяет validator_id с тем, что записано в работающем контейнере на хосте,
|
||||||
|
# и при расхождении останавливается ДО замены контейнера.
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
validators:
|
||||||
|
hosts:
|
||||||
|
validator-1:
|
||||||
|
ansible_host: 10.11.12.161
|
||||||
|
validator_id: vkiplab-v1
|
||||||
|
validator-2:
|
||||||
|
ansible_host: 10.11.12.177
|
||||||
|
validator_id: vkiplab-v2
|
||||||
|
validator-3:
|
||||||
|
ansible_host: 10.11.12.33
|
||||||
|
validator_id: vkiplab-v3
|
||||||
|
validator-4:
|
||||||
|
ansible_host: 10.11.12.41
|
||||||
|
validator_id: vkiplab-v4
|
||||||
|
validator-5:
|
||||||
|
ansible_host: 10.11.12.193
|
||||||
|
validator_id: vkiplab-v5
|
||||||
|
validator-6:
|
||||||
|
ansible_host: 10.11.12.197
|
||||||
|
validator_id: vkiplab-v6
|
||||||
|
validator-7:
|
||||||
|
ansible_host: 10.11.12.198
|
||||||
|
validator_id: vkiplab-v7
|
||||||
|
validator-8:
|
||||||
|
ansible_host: 10.11.12.169
|
||||||
|
validator_id: vkiplab-v8
|
||||||
|
validator-9:
|
||||||
|
ansible_host: 10.11.12.185
|
||||||
|
validator_id: vkiplab-v9
|
||||||
|
validator-10:
|
||||||
|
ansible_host: 10.11.12.186
|
||||||
|
validator_id: vkiplab-v10
|
||||||
|
validator-11:
|
||||||
|
ansible_host: 10.11.12.199
|
||||||
|
validator_id: vkiplab-v11
|
||||||
|
validator-12:
|
||||||
|
ansible_host: 10.11.12.196
|
||||||
|
validator_id: vkiplab-v12
|
||||||
|
validator-13:
|
||||||
|
ansible_host: 10.11.12.194
|
||||||
|
validator_id: vkiplab-v13
|
||||||
|
validator-14:
|
||||||
|
ansible_host: 10.11.12.195
|
||||||
|
validator_id: vkiplab-v14
|
||||||
|
validator-15:
|
||||||
|
ansible_host: 10.11.12.65
|
||||||
|
validator_id: vkiplab-v15
|
||||||
|
validator-16:
|
||||||
|
ansible_host: 10.11.12.189
|
||||||
|
validator_id: vkiplab-v16
|
||||||
|
validator-17:
|
||||||
|
ansible_host: 10.11.12.190
|
||||||
|
validator_id: vkiplab-v17
|
||||||
|
validator-18:
|
||||||
|
ansible_host: 10.11.12.168
|
||||||
|
validator_id: vkiplab-v18
|
||||||
|
validator-19:
|
||||||
|
ansible_host: 10.11.12.191
|
||||||
|
validator_id: vkiplab-v19
|
||||||
|
validator-20:
|
||||||
|
ansible_host: 10.11.12.73
|
||||||
|
validator_id: vkiplab-v20
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
# Доставка validator-agent на все валидаторы: обновить git-клон, собрать образ
|
||||||
|
# на каждом хосте, остановить и удалить старый контейнер, поднять новый.
|
||||||
|
# Запуск (из deploy/ansible): ansible-playbook playbooks/deploy-validator-agent.yml
|
||||||
|
- name: Deliver validator-agent to the validators
|
||||||
|
hosts: validators
|
||||||
|
become: true
|
||||||
|
gather_facts: false
|
||||||
|
# Волны: один хост (канарейка), затем четыре, затем все остальные. Любой
|
||||||
|
# сбой останавливает прогон — следующая волна не начнётся.
|
||||||
|
# Все сразу: -e '{"deploy_serial": ["100%"]}'.
|
||||||
|
serial: "{{ deploy_serial }}"
|
||||||
|
max_fail_percentage: 0
|
||||||
|
vars:
|
||||||
|
deploy_serial: [1, 4, "100%"]
|
||||||
|
roles:
|
||||||
|
- validator_agent
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
# Dockerfile ничего не компилирует: в образ копируется закоммиченный
|
||||||
|
# bin/validator-agent. Не даём выкатить бинарник, не совпадающий с суммой.
|
||||||
|
- name: Check bin/validator-agent against SHA256SUMS
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
grep -E '[[:space:]]validator-agent$' SHA256SUMS | sha256sum -c -
|
||||||
|
args:
|
||||||
|
chdir: "{{ repo_dir }}/bin"
|
||||||
|
executable: /bin/bash
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
# Контекст сборки — корень репозитория (так и в SETUP.md). Слои кэшируются,
|
||||||
|
# при смене bin/ образ пересобирается сам.
|
||||||
|
- name: Build the image
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- docker
|
||||||
|
- build
|
||||||
|
- --platform
|
||||||
|
- "{{ platform }}"
|
||||||
|
- --label
|
||||||
|
- "git.rev={{ rev_after.stdout }}"
|
||||||
|
- --label
|
||||||
|
- deployed.by=ansible
|
||||||
|
- -t
|
||||||
|
- "{{ image_ref }}"
|
||||||
|
- -f
|
||||||
|
- "{{ dockerfile }}"
|
||||||
|
- .
|
||||||
|
chdir: "{{ repo_dir }}"
|
||||||
|
|
||||||
|
- name: Tag the image as latest
|
||||||
|
ansible.builtin.command: "docker tag {{ image_ref }} {{ image_name }}:latest"
|
||||||
|
|
||||||
|
- name: Read the image id
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv: [docker, image, inspect, --format, "{% raw %}{{.Id}}{% endraw %}", "{{ image_ref }}"]
|
||||||
|
changed_when: false
|
||||||
|
register: built_image
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
# Команды git вместо модуля git: модуль переписывает URL remote и может
|
||||||
|
# затереть учётные данные, уже настроенные в клоне. Работаем от git_user
|
||||||
|
# (или от SSH-пользователя, если он не задан).
|
||||||
|
- name: Remember the current revision
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} rev-parse HEAD"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
changed_when: false
|
||||||
|
register: rev_before
|
||||||
|
|
||||||
|
- name: Fetch the remote
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} fetch --prune --tags {{ repo_remote }}"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
# deploy_ref — ветка, тег или коммит. Ветка берётся из remote (свежая),
|
||||||
|
# тег и коммит — как есть.
|
||||||
|
- name: Resolve deploy_ref as a remote branch
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
{{ git_cmd }} rev-parse --verify --quiet
|
||||||
|
refs/remotes/{{ repo_remote }}/{{ deploy_ref }}^{commit}
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
register: ref_branch
|
||||||
|
|
||||||
|
- name: Resolve deploy_ref as a tag or commit
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} rev-parse --verify --quiet {{ deploy_ref }}^{commit}"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
register: ref_other
|
||||||
|
when: ref_branch.rc != 0
|
||||||
|
|
||||||
|
- name: Fail if deploy_ref does not exist
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: ref_branch.rc == 0 or (ref_other.rc | default(1)) == 0
|
||||||
|
fail_msg: "deploy_ref={{ deploy_ref }} не найден в {{ repo_dir }} ({{ repo_remote }})."
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Fix the target revision
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
target_rev: "{{ ref_branch.stdout if ref_branch.rc == 0 else ref_other.stdout }}"
|
||||||
|
|
||||||
|
# Ветка: остаёмся на локальной ветке (клон не уходит в detached HEAD),
|
||||||
|
# сброс на remote. Тег или коммит: detached HEAD.
|
||||||
|
- name: Check out the branch
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} checkout --force -B {{ deploy_ref }} {{ target_rev }}"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
when: ref_branch.rc == 0
|
||||||
|
changed_when: rev_before.stdout != target_rev
|
||||||
|
|
||||||
|
- name: Check out the tag or commit
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} checkout --force --detach {{ target_rev }}"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
when: ref_branch.rc != 0
|
||||||
|
changed_when: rev_before.stdout != target_rev
|
||||||
|
|
||||||
|
- name: Read the deployed revision
|
||||||
|
ansible.builtin.command: "{{ git_cmd }} rev-parse HEAD"
|
||||||
|
become: "{{ git_user | length > 0 }}"
|
||||||
|
become_user: "{{ git_user }}"
|
||||||
|
changed_when: false
|
||||||
|
register: rev_after
|
||||||
|
|
||||||
|
- name: Check that the clone is at the target revision
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: rev_after.stdout == target_rev
|
||||||
|
fail_msg: "Клон на {{ rev_after.stdout }}, ожидалось {{ target_rev }}."
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Remember the short revision
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
deploy_rev: "{{ rev_after.stdout[:12] }}"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
# Порядок важен: сначала всё, что не трогает работающий контейнер (проверки,
|
||||||
|
# обновление кода, сборка образа), и только потом замена контейнера. Если
|
||||||
|
# что-то упало до replace, старый контейнер продолжает работать.
|
||||||
|
- name: Preflight checks
|
||||||
|
ansible.builtin.import_tasks: preflight.yml
|
||||||
|
tags: [preflight]
|
||||||
|
|
||||||
|
- name: Dry run stops after preflight
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "check mode: git, build, replace and verify are skipped"
|
||||||
|
when: ansible_check_mode
|
||||||
|
tags: [always]
|
||||||
|
|
||||||
|
- name: Update the git clone
|
||||||
|
ansible.builtin.import_tasks: git.yml
|
||||||
|
when: not ansible_check_mode
|
||||||
|
tags: [git]
|
||||||
|
|
||||||
|
- name: Build the image
|
||||||
|
ansible.builtin.import_tasks: build.yml
|
||||||
|
when: not ansible_check_mode
|
||||||
|
tags: [build]
|
||||||
|
|
||||||
|
- name: Replace the container
|
||||||
|
ansible.builtin.import_tasks: replace.yml
|
||||||
|
when: not ansible_check_mode
|
||||||
|
tags: [replace]
|
||||||
|
|
||||||
|
- name: Verify the new container
|
||||||
|
ansible.builtin.import_tasks: verify.yml
|
||||||
|
when: not ansible_check_mode
|
||||||
|
tags: [verify]
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
# --- на jump-хосте (один раз) -------------------------------------------
|
||||||
|
- name: Check that the env file exists on the jump host
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ local_env_file }}"
|
||||||
|
delegate_to: localhost
|
||||||
|
become: false
|
||||||
|
run_once: true
|
||||||
|
check_mode: false
|
||||||
|
register: env_file_stat
|
||||||
|
|
||||||
|
- name: Fail early without an env file
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: env_file_stat.stat.exists
|
||||||
|
fail_msg: >-
|
||||||
|
Нет env-файла {{ local_env_file }}. Создайте его:
|
||||||
|
cp env/validator-agent.env.example env/validator-agent.env и заполните.
|
||||||
|
quiet: true
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# Содержимое файла (в нём токен) не выводится: разбор идёт в задаче с no_log,
|
||||||
|
# а проверка и её сообщение — по готовым булевым значениям.
|
||||||
|
- name: Inspect the env file without printing it
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
env_url_set: "{{ env_file_text is regex('(?m)^VALIDATOR_AGENT_CONTROL_API_URL=\\S+') }}"
|
||||||
|
env_url_is_example: "{{ env_file_text is regex('(?m)^VALIDATOR_AGENT_CONTROL_API_URL=\\S*example\\.com') }}"
|
||||||
|
vars:
|
||||||
|
env_file_text: "{{ lookup('ansible.builtin.file', local_env_file) }}"
|
||||||
|
run_once: true
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Check that the env file sets the control-api address
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- env_url_set | bool
|
||||||
|
- not (env_url_is_example | bool)
|
||||||
|
fail_msg: >-
|
||||||
|
В {{ local_env_file }} не задан VALIDATOR_AGENT_CONTROL_API_URL
|
||||||
|
(или остался адрес-пример example.com).
|
||||||
|
quiet: true
|
||||||
|
run_once: true
|
||||||
|
|
||||||
|
# --- на каждом валидаторе -----------------------------------------------
|
||||||
|
- name: Check that Docker answers
|
||||||
|
ansible.builtin.command: docker version --format {% raw %}'{{.Server.Version}}'{% endraw %}
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Check that git is installed
|
||||||
|
ansible.builtin.command: git --version
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Check that the git clone exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ repo_dir }}/.git"
|
||||||
|
check_mode: false
|
||||||
|
register: clone_stat
|
||||||
|
|
||||||
|
- name: Fail without a clone
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: clone_stat.stat.exists
|
||||||
|
fail_msg: "Нет git-клона {{ repo_dir }} на {{ inventory_hostname }}."
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Read the CPU architecture
|
||||||
|
ansible.builtin.command: uname -m
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
register: arch
|
||||||
|
|
||||||
|
- name: The image is linux/amd64 only
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: arch.stdout in ['x86_64', 'amd64']
|
||||||
|
fail_msg: "Архитектура {{ arch.stdout }}: образ {{ platform }} здесь не запустится (exec format error)."
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Look at the current container
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
docker container inspect --format
|
||||||
|
{% raw %}'{{.Config.Image}} {{.State.Status}}'{% endraw %}
|
||||||
|
{{ container_name }}
|
||||||
|
register: current_container
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
# validator_id работающего контейнера — эталон: если он отличается от
|
||||||
|
# inventory, заменять контейнер нельзя (агент зарегистрировался бы под чужим
|
||||||
|
# именем, адреса привязывались бы к порту другой ВМ). Выводится только он,
|
||||||
|
# а не все переменные окружения (там токен).
|
||||||
|
- name: Read validator_id of the running container
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
docker container inspect --format '{% raw %}{{range .Config.Env}}{{println .}}{{end}}{% endraw %}' {{ container_name }} \
|
||||||
|
| sed -n 's/^VALIDATOR_AGENT_VALIDATOR_ID=//p'
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: running_validator_id
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
when: current_container.rc == 0
|
||||||
|
|
||||||
|
- name: Check validator_id against the running container
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: >-
|
||||||
|
current_container.rc != 0
|
||||||
|
or (running_validator_id.stdout | trim) == ''
|
||||||
|
or (running_validator_id.stdout | trim) == effective_validator_id
|
||||||
|
fail_msg: >-
|
||||||
|
{{ inventory_hostname }}: в запущенном контейнере validator_id={{ running_validator_id.stdout | default('') | trim }},
|
||||||
|
а в inventory {{ effective_validator_id }}. Проверьте соответствие имени ВМ и validator_id
|
||||||
|
в inventory/hosts.yml; контейнер не тронут.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Report the current container
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ container_name }}:
|
||||||
|
{{ current_container.stdout if current_container.rc == 0 else 'контейнера нет (будет создан)' }};
|
||||||
|
validator_id для запуска: {{ effective_validator_id }}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
# Образ уже собран: простой валидатора — только stop + rm + run.
|
||||||
|
- name: Copy the env file to the validator
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ local_env_file }}"
|
||||||
|
dest: "{{ remote_env_file }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Check whether the container exists
|
||||||
|
ansible.builtin.command: "docker container inspect {{ container_name }}"
|
||||||
|
register: container_exists
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Stop the current container
|
||||||
|
ansible.builtin.command: "docker stop -t {{ stop_timeout }} {{ container_name }}"
|
||||||
|
when: container_exists.rc == 0
|
||||||
|
|
||||||
|
- name: Remove the current container
|
||||||
|
ansible.builtin.command: "docker rm -f {{ container_name }}"
|
||||||
|
when: container_exists.rc == 0
|
||||||
|
|
||||||
|
# --restart нужен: агент завершается, если регистрация в control-api не
|
||||||
|
# удалась, и должен подняться снова. validator_id берётся из inventory
|
||||||
|
# (validator_id) и перекрывает env-файл.
|
||||||
|
- name: Start the new container
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv: >-
|
||||||
|
{{ ['docker', 'run', '-d',
|
||||||
|
'--name', container_name,
|
||||||
|
'--restart', restart_policy,
|
||||||
|
'--platform', platform,
|
||||||
|
'--env-file', remote_env_file,
|
||||||
|
'-e', 'VALIDATOR_AGENT_VALIDATOR_ID=' ~ effective_validator_id,
|
||||||
|
'--log-driver', 'json-file',
|
||||||
|
'--log-opt', 'max-size=' ~ log_max_size,
|
||||||
|
'--log-opt', 'max-file=' ~ log_max_file,
|
||||||
|
'--label', 'git.rev=' ~ rev_after.stdout,
|
||||||
|
'--label', 'deployed.by=ansible']
|
||||||
|
+ (capabilities | map('regex_replace', '^(.*)$', '--cap-add=\1') | list)
|
||||||
|
+ [image_ref] }}
|
||||||
|
register: started
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
- name: Verify the new container
|
||||||
|
block:
|
||||||
|
# Агент пишет "registered" после успешной регистрации в control-api.
|
||||||
|
- name: Wait for the agent to register in control-api
|
||||||
|
ansible.builtin.command: "docker logs --tail 200 {{ container_name }}"
|
||||||
|
register: agent_logs
|
||||||
|
changed_when: false
|
||||||
|
until: agent_logs.stdout is search('msg=registered validator_id=' ~ effective_validator_id ~ '(\s|$)') or agent_logs.stderr is search('msg=registered validator_id=' ~ effective_validator_id ~ '(\s|$)')
|
||||||
|
retries: "{{ verify_retries | int }}"
|
||||||
|
delay: "{{ verify_delay | int }}"
|
||||||
|
|
||||||
|
- name: Inspect the container
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv: [docker, inspect, --format, "{% raw %}{{.State.Running}} {{.RestartCount}} {{.Image}}{% endraw %}", "{{ container_name }}"]
|
||||||
|
register: container_state
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Check the container state
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- container_state.stdout.split()[0] == 'true'
|
||||||
|
- container_state.stdout.split()[1] == '0'
|
||||||
|
- container_state.stdout.split()[2] == built_image.stdout
|
||||||
|
fail_msg: >-
|
||||||
|
Контейнер {{ container_name }} в состоянии «{{ container_state.stdout }}»
|
||||||
|
(ожидалось: запущен, 0 перезапусков, образ {{ built_image.stdout }}).
|
||||||
|
quiet: true
|
||||||
|
rescue:
|
||||||
|
- name: Collect the container log
|
||||||
|
ansible.builtin.command: "docker logs --tail 30 {{ container_name }}"
|
||||||
|
register: failed_logs
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Fail the host and stop the next waves
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: |-
|
||||||
|
{{ inventory_hostname }}: контейнер не прошёл проверку после запуска.
|
||||||
|
Последние строки лога:
|
||||||
|
{{ failed_logs.stdout }}{{ failed_logs.stderr }}
|
||||||
|
|
||||||
|
# Старые образы с метками ревизий: оставляем keep_images последних (docker
|
||||||
|
# выводит от новых к старым), образ работающего контейнера docker не удалит.
|
||||||
|
- name: List image tags
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv: [docker, images, "{{ image_name }}", --format, "{% raw %}{{.Tag}}{% endraw %}"]
|
||||||
|
register: image_tags
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Remove old revision images
|
||||||
|
ansible.builtin.command: "docker rmi {{ image_name }}:{{ item }}"
|
||||||
|
loop: "{{ (image_tags.stdout_lines | reject('equalto', 'latest') | list)[keep_images | int:] }}"
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Remove dangling images
|
||||||
|
ansible.builtin.command: docker image prune -f
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
{{ inventory_hostname }} ({{ effective_validator_id }}): {{ deploy_rev }} ({{ deploy_ref }}),
|
||||||
|
образ {{ built_image.stdout[:19] }}, контейнер {{ container_name }} запущен
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
# git с явным safe.directory: клон может принадлежать другому пользователю.
|
||||||
|
git_cmd: "git -c safe.directory={{ repo_dir }} -C {{ repo_dir }}"
|
||||||
|
# validator_id в control-api: из inventory, иначе имя хоста.
|
||||||
|
effective_validator_id: "{{ validator_id | default(inventory_hostname) }}"
|
||||||
|
# Образ с меткой ревизии, собранный в этом прогоне.
|
||||||
|
image_ref: "{{ image_name }}:{{ deploy_rev | default('unknown') }}"
|
||||||
@@ -739,6 +739,10 @@ docker compose up -d --build
|
|||||||
сборке) и `docker rm -f <имя> && docker run ... ` (или `docker restart`,
|
сборке) и `docker rm -f <имя> && docker run ... ` (или `docker restart`,
|
||||||
если менялись только переменные окружения, а не сам бинарник/образ).
|
если менялись только переменные окружения, а не сам бинарник/образ).
|
||||||
|
|
||||||
|
Для массового обновления валидаторов (git-клон, сборка образа на хосте,
|
||||||
|
замена контейнера, проверка регистрации) есть Ansible-сценарий:
|
||||||
|
[`deploy/ansible/`](../deploy/ansible/README.md).
|
||||||
|
|
||||||
### Диагностика Docker-развёртывания
|
### Диагностика Docker-развёртывания
|
||||||
|
|
||||||
- **Контейнер сразу падает, в логах `exec format error`** — образ собран
|
- **Контейнер сразу падает, в логах `exec format error`** — образ собран
|
||||||
|
|||||||
Reference in new issue
Block a user