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:
ayurishchevandClaude Sonnet 5.5 committed 2026-10-02 09:23:39 +03:00
1 parent abbee9a08a
commit 49890ff5de
16 files changed
+684 -1

No files matched your search

+2
View File
@@ -5,6 +5,8 @@
!/deploy/docker/.env.example
!/deploy/docker/.env.prod.example
/deploy/docker/control-api/control-api.docker.yaml
# Ansible: рабочий env-файл запуска validator-agent (шаблон .example остаётся в git).
/deploy/ansible/env/*.env
/rxprod-compose/.env
# Live runtime database for the rxprod-compose control-api container.
+1 -1
View File
@@ -149,7 +149,7 @@ docs/ документация и планы доработок
## Публикация и эксплуатация
- **Бинарники.** Готовые linux/amd64 лежат в `bin/` и **не обновляются автоматически**: после правок кода пересоберите их и обновите `SHA256SUMS` (команды — [docs/SETUP.md](docs/SETUP.md#вариант-b-сборка-из-исходников)); Dockerfile копируют именно `bin/*`.
- **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` с учётными данными в репозиторий не входит.
- **Миграции** применяются при старте `control-api`; версия схемы — `PRAGMA user_version`. Начальная загрузка (`validators`, `sites`, `targets`, `check_types`, `inbound_checks`) выполняется только в пустые таблицы.
- Остановка (`SIGTERM`) корректно завершает HTTP-сервер и фоновые циклы. Состояние автоцикла и очереди сохраняется в БД.
+107
View File
@@ -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`).
+15
View File
@@ -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
View File
@@ -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"
+69
View File
@@ -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') }}"
+4
View File
@@ -739,6 +739,10 @@ docker compose up -d --build
сборке) и `docker rm -f <имя> && docker run ... ` (или `docker restart`,
если менялись только переменные окружения, а не сам бинарник/образ).
Для массового обновления валидаторов (git-клон, сборка образа на хосте,
замена контейнера, проверка регистрации) есть Ansible-сценарий:
[`deploy/ansible/`](../deploy/ansible/README.md).
### Диагностика Docker-развёртывания
- **Контейнер сразу падает, в логах `exec format error`** — образ собран