Files
K3S/playbooks/remove-etcd-node.yml
Sergey Antropoff a94039e0f1 feat: аддоны через addons.yml, внешний etcd, управление etcd нодами
## Аддоны (group_vars/all/addons.yml)

- Создан group_vars/all/addons.yml — единое место для включения/отключения
  аддонов (addon_ingress_nginx: true/false и т.д.) и их основных настроек
- Из group_vars/all/main.yml убраны все секции аддонов (NFS, CSI, ingress,
  cert-manager, etcd backup, Istio, Prometheus) — остался только core кластер
- Создан playbooks/addons.yml — комбинированный плейбук с 10 плеями,
  каждый с `when: addon_X | default(false) | bool`; запускает только включённые
- make install-full: core (site.yml) + аддоны по addons.yml
- make install-addons: только аддоны без переустановки core
- Убраны все *_enabled флаги из аддонов (cert_manager_enabled, istio_enabled,
  prometheus_stack_enabled и др.) — аддон ставится явным вызовом
- kube-vip: убран skip guard и kube_vip_enabled флаг (core, всегда ставится)
- TLS defaults в argocd/longhorn/kubernetes-dashboard: убрана зависимость
  от cert_manager_enabled, теперь просто false (задаётся явно)
- Kiali: убрана зависимость от prometheus_stack_enabled, добавлены переменные
  kiali_prometheus_enabled/url и kiali_grafana_enabled/url

## Внешний etcd кластер

- Новая переменная k3s_etcd_type: embedded|external в main.yml
- inventory/hosts.ini: добавлена группа [etcd_nodes] — любые серверы,
  не обязательно мастера
- roles/etcd/: полная роль для установки внешнего etcd кластера:
  - install.yml — скачивает бинарник, создаёт пользователя и директории
  - pki.yml — генерирует CA + server/peer/client сертификаты через openssl
    на Ansible-контроллере; раскладывает на etcd ноды и k3s мастера
  - service.yml — разворачивает etcd.env и systemd сервис, проверяет здоровье
  - etcd.env.j2 и etcd.service.j2 — шаблоны конфигурации
  - etcd_pki_local_dir: persistent путь (<project>/etcd-pki/) вместо /tmp,
    etcd-pki/ добавлен в .gitignore
- roles/k3s/templates/k3s-server-config.yaml.j2: при external режиме
  подставляет datastore-endpoint со всеми etcd нодами + пути к клиентским
  сертификатам; при embedded — прежняя логика cluster-init
- playbooks/site.yml: условный плей для etcd перед k3s (тег etcd)
- make install-etcd: отдельная команда для развёртывания etcd кластера

## Управление etcd нодами

- playbooks/add-etcd-node.yml: добавить ноду в работающий etcd кластер
  (PKI генерация → install → etcdctl member add → start с state=existing → verify)
- playbooks/remove-etcd-node.yml: безопасно удалить ноду из etcd кластера
  (проверка кворума → member remove → stop → clean up PKI)
- playbooks/add-node.yml: при k3s_etcd_type=external и наличии ноды в
  [etcd_nodes] автоматически добавляет её в etcd кластер после k3s
- playbooks/remove-node.yml: при k3s_etcd_type=external сначала удаляет
  ноду из etcd (member remove + stop), затем из k3s
- make add-etcd-node NODE=etcd04 / make remove-etcd-node NODE=etcd04
- Команды add-etcd-node / remove-etcd-node в docker/entrypoint.sh
2026-04-25 06:34:48 +03:00

207 lines
8.3 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# ─────────────────────────────────────────────────────────────────────────────
# Безопасно удалить ноду из внешнего etcd кластера
#
# Использование:
# make remove-etcd-node NODE=etcd04 — удалить ноду etcd04
# make remove-etcd-node NODE=master04 — удалить мастер-ноду из etcd участников
#
# ВНИМАНИЕ:
# Нельзя удалить ноду если в кластере меньше 3 участников без потери кворума.
# После удаления обнови inventory/hosts.ini — убери ноду из [etcd_nodes].
#
# Порядок:
# 1. Валидация (кворум, не последняя нода)
# 2. etcdctl member remove <id>
# 3. Остановить etcd на ноде
# 4. Очистить данные и сертификаты
# 5. Верификация оставшегося кластера
# ─────────────────────────────────────────────────────────────────────────────
# ── Валидация ─────────────────────────────────────────────────────────────────
- name: Validate and check quorum
hosts: "{{ (groups['etcd_nodes'] | reject('equalto', node_to_remove | default('')) | list)[0] | default('localhost') }}"
gather_facts: false
become: true
tags: [always]
tasks:
- name: Check node_to_remove is specified
ansible.builtin.assert:
that: node_to_remove is defined and node_to_remove | length > 0
fail_msg: "Укажи ноду: make remove-etcd-node NODE=<nodename>"
delegate_to: localhost
become: false
- name: Check node is in etcd_nodes group
ansible.builtin.assert:
that: node_to_remove in groups['etcd_nodes']
fail_msg: >
Нода '{{ node_to_remove }}' не найдена в группе [etcd_nodes].
Убедись что имя совпадает с inventory.
delegate_to: localhost
become: false
- name: Check minimum cluster size
ansible.builtin.assert:
that: groups['etcd_nodes'] | length > 1
fail_msg: >
В кластере только одна нода etcd — удаление невозможно.
Нельзя оставить кластер без узлов.
delegate_to: localhost
become: false
- name: Warn about quorum loss risk
ansible.builtin.debug:
msg: >
ВНИМАНИЕ: после удаления в кластере останется {{ groups['etcd_nodes'] | length - 1 }} нод(ы).
Кворум (большинство): требуется минимум {{ ((groups['etcd_nodes'] | length - 1) // 2 + 1) }} нод.
{% if (groups['etcd_nodes'] | length - 1) < 2 %}
ПРЕДУПРЕЖДЕНИЕ: менее 2 нод — кластер потеряет HA!
{% endif %}
- name: Get member ID of node to remove
ansible.builtin.shell: |
ETCDCTL_API=3 etcdctl \
--endpoints="https://{{ ansible_host }}:{{ etcd_client_port }}" \
--cacert="{{ etcd_pki_dir }}/ca.crt" \
--cert="{{ etcd_pki_dir }}/server.crt" \
--key="{{ etcd_pki_dir }}/server.key" \
member list -w json \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in data['members']:
if m.get('name') == '{{ node_to_remove }}':
print(hex(m['ID']))
sys.exit(0)
sys.exit(1)
"
register: _member_id
changed_when: false
failed_when: _member_id.rc != 0
- name: Show member ID
ansible.builtin.debug:
msg: "Member ID для {{ node_to_remove }}: {{ _member_id.stdout }}"
- name: Remove member from etcd cluster
ansible.builtin.shell: |
ETCDCTL_API=3 etcdctl \
--endpoints="https://{{ ansible_host }}:{{ etcd_client_port }}" \
--cacert="{{ etcd_pki_dir }}/ca.crt" \
--cert="{{ etcd_pki_dir }}/server.crt" \
--key="{{ etcd_pki_dir }}/server.key" \
member remove {{ _member_id.stdout }}
changed_when: true
# ── Останавливаем etcd на удаляемой ноде ─────────────────────────────────────
- name: Stop etcd on removed node
hosts: "{{ node_to_remove }}"
gather_facts: false
become: true
tags: [stop]
tasks:
- name: Stop and disable etcd service
ansible.builtin.systemd:
name: etcd
state: stopped
enabled: false
failed_when: false
- name: Remove etcd systemd service file
ansible.builtin.file:
path: /etc/systemd/system/etcd.service
state: absent
notify: Reload systemd
- name: Remove etcd data directory
ansible.builtin.file:
path: "{{ etcd_data_dir }}"
state: absent
- name: Remove etcd config and PKI
ansible.builtin.file:
path: "{{ etcd_config_dir }}"
state: absent
- name: Remove etcd binaries
ansible.builtin.file:
path: "{{ etcd_install_dir }}/{{ item }}"
state: absent
loop:
- etcd
- etcdctl
failed_when: false
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
# ── Удаляем локальные сертификаты удалённой ноды ─────────────────────────────
- name: Clean up local PKI artifacts
hosts: localhost
gather_facts: false
become: false
tags: [pki]
tasks:
- name: Remove node certs from local PKI dir
ansible.builtin.file:
path: "{{ etcd_pki_local_dir }}/{{ item }}"
state: absent
loop:
- "server-{{ node_to_remove }}.crt"
- "server-{{ node_to_remove }}.key"
- "server-{{ node_to_remove }}.csr"
- "peer-{{ node_to_remove }}.crt"
- "peer-{{ node_to_remove }}.key"
- "peer-{{ node_to_remove }}.csr"
failed_when: false
# ── Верификация ───────────────────────────────────────────────────────────────
- name: Verify remaining cluster
hosts: "{{ (groups['etcd_nodes'] | reject('equalto', node_to_remove) | list)[0] }}"
gather_facts: false
become: true
tags: [verify]
tasks:
- name: Check cluster health
ansible.builtin.shell: |
ETCDCTL_API=3 etcdctl \
--endpoints="{{ remaining_endpoints }}" \
--cacert="{{ etcd_pki_dir }}/ca.crt" \
--cert="{{ etcd_pki_dir }}/server.crt" \
--key="{{ etcd_pki_dir }}/server.key" \
endpoint health
vars:
remaining_endpoints: >-
{%- set eps = [] -%}
{%- for h in groups['etcd_nodes'] | reject('equalto', node_to_remove) | list -%}
{%- set _ = eps.append('https://' ~ hostvars[h]['ansible_host'] ~ ':' ~ etcd_client_port) -%}
{%- endfor -%}
{{ eps | join(',') }}
register: _health
changed_when: false
- name: Show remaining members
ansible.builtin.shell: |
ETCDCTL_API=3 etcdctl \
--endpoints="https://{{ ansible_host }}:{{ etcd_client_port }}" \
--cacert="{{ etcd_pki_dir }}/ca.crt" \
--cert="{{ etcd_pki_dir }}/server.crt" \
--key="{{ etcd_pki_dir }}/server.key" \
member list -w table
register: _members
changed_when: false
- name: Display remaining members
ansible.builtin.debug:
msg: "{{ _members.stdout_lines }}"
- name: Summary
ansible.builtin.debug:
msg: >
Нода {{ node_to_remove }} успешно удалена из etcd кластера.
Осталось нод: {{ groups['etcd_nodes'] | length - 1 }}.
Удали ноду из [etcd_nodes] в inventory/hosts.ini.