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
@@ -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 }} запущен
|
||||
Reference in new issue
Block a user