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
This commit is contained in:
Sergey Antropoff
2026-04-25 06:34:48 +03:00
parent 8aa55a694c
commit a94039e0f1
30 changed files with 1301 additions and 169 deletions

214
playbooks/add-etcd-node.yml Normal file
View File

@@ -0,0 +1,214 @@
---
# ─────────────────────────────────────────────────────────────────────────────
# Добавить ноду в внешний etcd кластер
#
# Использование:
# make add-etcd-node NODE=etcd04 — добавить ноду etcd04
# make add-etcd-node NODE=master04 — добавить мастер-ноду как etcd-участника
#
# Перед запуском:
# 1. Добавь ноду в [etcd_nodes] в inventory/hosts.ini
# 2. Убедись что etcd PKI существует (etcd-pki/ca.key, etcd-pki/ca.crt)
# 3. k3s_etcd_type: external в group_vars/all/main.yml
#
# PKI:
# Серверные/peer сертификаты для новой ноды генерируются на Ansible-контроллере
# и подписываются существующим CA из etcd-pki/. CA ключ должен быть доступен.
# ─────────────────────────────────────────────────────────────────────────────
# ── Валидация ─────────────────────────────────────────────────────────────────
- name: Validate prerequisites
hosts: localhost
gather_facts: false
become: false
tags: [always]
tasks:
- name: Check node_to_add is specified
ansible.builtin.assert:
that: node_to_add is defined and node_to_add | length > 0
fail_msg: "Укажи ноду: make add-etcd-node NODE=<nodename>"
- name: Check node is in etcd_nodes group
ansible.builtin.assert:
that: node_to_add in groups['etcd_nodes']
fail_msg: >
Нода '{{ node_to_add }}' не найдена в группе [etcd_nodes].
Добавь её в inventory/hosts.ini и повтори запуск.
- name: Check etcd PKI CA exists
ansible.builtin.stat:
path: "{{ etcd_pki_local_dir }}/ca.key"
register: _ca_key_stat
- name: Fail if CA key missing
ansible.builtin.assert:
that: _ca_key_stat.stat.exists
fail_msg: >
CA ключ не найден: {{ etcd_pki_local_dir }}/ca.key
Запусти первоначальную установку etcd: make install-etcd
- name: Check node is not already in cluster
ansible.builtin.shell: |
EXISTING_NODES=$(ls "{{ etcd_pki_local_dir }}"/server-*.crt 2>/dev/null \
| xargs -I{} basename {} .crt \
| sed 's/server-//')
echo "${EXISTING_NODES}" | grep -qx "{{ node_to_add }}" && echo "exists" || echo "new"
register: _node_cert_check
changed_when: false
- name: Warn if certs already exist (will be regenerated)
ansible.builtin.debug:
msg: >
Сертификаты для ноды {{ node_to_add }} уже существуют в PKI.
Они будут пересозданы.
when: _node_cert_check.stdout == "exists"
# ── Генерируем сертификаты для новой ноды ────────────────────────────────────
- name: Generate PKI certificates for new etcd node
hosts: localhost
gather_facts: false
become: false
tags: [pki]
vars:
_new_node_ip: "{{ hostvars[node_to_add]['ansible_host'] }}"
tasks:
- name: Generate server/peer certs for new node
ansible.builtin.shell: |
set -e
NODE="{{ node_to_add }}"
NODE_IP="{{ _new_node_ip }}"
PKI="{{ etcd_pki_local_dir }}"
SAN="subjectAltName=IP:${NODE_IP},IP:127.0.0.1,DNS:${NODE},DNS:localhost"
# Принудительно пересоздаём (нода могла быть заменена с тем же именем)
rm -f "${PKI}/server-${NODE}."{crt,key,csr} "${PKI}/peer-${NODE}."{crt,key,csr}
openssl genrsa -out "${PKI}/server-${NODE}.key" 2048
openssl req -new -key "${PKI}/server-${NODE}.key" -out "${PKI}/server-${NODE}.csr" \
-subj "/CN=${NODE}/O=etcd"
openssl x509 -req -days 3650 \
-in "${PKI}/server-${NODE}.csr" -CA "${PKI}/ca.crt" -CAkey "${PKI}/ca.key" \
-CAcreateserial -out "${PKI}/server-${NODE}.crt" \
-extfile <(printf "${SAN}")
openssl genrsa -out "${PKI}/peer-${NODE}.key" 2048
openssl req -new -key "${PKI}/peer-${NODE}.key" -out "${PKI}/peer-${NODE}.csr" \
-subj "/CN=${NODE}/O=etcd-peer"
openssl x509 -req -days 3650 \
-in "${PKI}/peer-${NODE}.csr" -CA "${PKI}/ca.crt" -CAkey "${PKI}/ca.key" \
-CAcreateserial -out "${PKI}/peer-${NODE}.crt" \
-extfile <(printf "${SAN}")
args:
executable: /bin/bash
changed_when: true
# ── Устанавливаем etcd на новую ноду ─────────────────────────────────────────
- name: Install etcd binary on new node
hosts: "{{ node_to_add }}"
gather_facts: true
become: true
tags: [install]
tasks:
- name: Install etcd binary
ansible.builtin.include_role:
name: etcd
tasks_from: install
- name: Copy CA cert
ansible.builtin.copy:
src: "{{ etcd_pki_local_dir }}/ca.crt"
dest: "{{ etcd_pki_dir }}/ca.crt"
owner: etcd
group: etcd
mode: '0644'
- name: Copy server/peer certs for new node
ansible.builtin.copy:
src: "{{ etcd_pki_local_dir }}/{{ item.src }}"
dest: "{{ etcd_pki_dir }}/{{ item.dest }}"
owner: etcd
group: etcd
mode: "{{ item.mode }}"
loop:
- { src: "server-{{ inventory_hostname }}.crt", dest: "server.crt", mode: "0644" }
- { src: "server-{{ inventory_hostname }}.key", dest: "server.key", mode: "0600" }
- { src: "peer-{{ inventory_hostname }}.crt", dest: "peer.crt", mode: "0644" }
- { src: "peer-{{ inventory_hostname }}.key", dest: "peer.key", mode: "0600" }
# ── Регистрируем новый member в кластере ─────────────────────────────────────
- name: Register new etcd member
hosts: "{{ (groups['etcd_nodes'] | reject('equalto', node_to_add) | list)[0] }}"
gather_facts: false
become: true
tags: [register]
vars:
_new_node_ip: "{{ hostvars[node_to_add]['ansible_host'] }}"
tasks:
- name: Add new member via etcdctl
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 add "{{ node_to_add }}" \
--peer-urls="https://{{ _new_node_ip }}:{{ etcd_peer_port }}"
register: _member_add
changed_when: true
- name: Show member add output
ansible.builtin.debug:
msg: "{{ _member_add.stdout_lines }}"
# ── Запускаем etcd на новой ноде ─────────────────────────────────────────────
- name: Start etcd on new node
hosts: "{{ node_to_add }}"
gather_facts: true
become: true
tags: [start]
vars:
# Для новой ноды которая присоединяется к существующему кластеру
etcd_initial_cluster_state: "existing"
tasks:
- name: Build initial cluster members string
ansible.builtin.set_fact:
etcd_initial_cluster_members: >-
{%- set members = [] -%}
{%- for h in groups['etcd_nodes'] -%}
{%- set _ = members.append(h ~ '=https://' ~ hostvars[h]['ansible_host'] ~ ':' ~ etcd_peer_port) -%}
{%- endfor -%}
{{ members | join(',') }}
- name: Configure and start etcd service
ansible.builtin.include_role:
name: etcd
tasks_from: service
# ── Верификация кластера ──────────────────────────────────────────────────────
- name: Verify etcd cluster
hosts: "{{ node_to_add }}"
gather_facts: false
become: true
tags: [verify]
tasks:
- name: Show etcd cluster members
ansible.builtin.shell: |
ETCDCTL_API=3 etcdctl \
--endpoints="{% for h in groups['etcd_nodes'] %}https://{{ hostvars[h]['ansible_host'] }}:{{ etcd_client_port }}{% if not loop.last %},{% endif %}{% endfor %}" \
--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 cluster members
ansible.builtin.debug:
msg: "{{ _members.stdout_lines }}"
- name: Summary
ansible.builtin.debug:
msg: >
Нода {{ node_to_add }} ({{ hostvars[node_to_add]['ansible_host'] }})
успешно добавлена в etcd кластер.
Не забудь обновить k3s конфиг если нода новая: make install-k3s.

View File

@@ -85,15 +85,132 @@
ansible.builtin.debug:
msg: "{{ new_node_status.stdout_lines }}"
- name: Install NFS client on new node (if CSI NFS enabled)
- name: Install NFS client on new node
ansible.builtin.apt:
name: nfs-common
state: present
update_cache: true
when: csi_nfs_enabled | default(true) | bool
- name: Summary
ansible.builtin.debug:
msg: >
Нода {{ inventory_hostname }} успешно добавлена в кластер.
Тип: {{ 'master (etcd participant)' if inventory_hostname in groups['k3s_master'] else 'worker (agent)' }}
Нода {{ inventory_hostname }} успешно добавлена в k3s кластер.
Тип: {{ 'master' if inventory_hostname in groups['k3s_master'] else 'worker' }}.
# ── Генерируем PKI и регистрируем в etcd (если нода в [etcd_nodes]) ──────────
- name: Generate PKI certificates for new etcd member
hosts: localhost
gather_facts: false
become: false
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_add in groups['etcd_nodes']
tags: [etcd]
tasks:
- name: Generate server/peer certs for new etcd node
ansible.builtin.shell: |
set -e
NODE="{{ node_to_add }}"
NODE_IP="{{ hostvars[node_to_add]['ansible_host'] }}"
PKI="{{ etcd_pki_local_dir }}"
SAN="subjectAltName=IP:${NODE_IP},IP:127.0.0.1,DNS:${NODE},DNS:localhost"
[ -f "${PKI}/ca.key" ] || { echo "CA key not found: ${PKI}/ca.key"; exit 1; }
rm -f "${PKI}/server-${NODE}."{crt,key,csr} "${PKI}/peer-${NODE}."{crt,key,csr}
openssl genrsa -out "${PKI}/server-${NODE}.key" 2048
openssl req -new -key "${PKI}/server-${NODE}.key" -out "${PKI}/server-${NODE}.csr" \
-subj "/CN=${NODE}/O=etcd"
openssl x509 -req -days 3650 \
-in "${PKI}/server-${NODE}.csr" -CA "${PKI}/ca.crt" -CAkey "${PKI}/ca.key" \
-CAcreateserial -out "${PKI}/server-${NODE}.crt" \
-extfile <(printf "${SAN}")
openssl genrsa -out "${PKI}/peer-${NODE}.key" 2048
openssl req -new -key "${PKI}/peer-${NODE}.key" -out "${PKI}/peer-${NODE}.csr" \
-subj "/CN=${NODE}/O=etcd-peer"
openssl x509 -req -days 3650 \
-in "${PKI}/peer-${NODE}.csr" -CA "${PKI}/ca.crt" -CAkey "${PKI}/ca.key" \
-CAcreateserial -out "${PKI}/peer-${NODE}.crt" \
-extfile <(printf "${SAN}")
args:
executable: /bin/bash
changed_when: true
- name: Install etcd and distribute certs to new etcd member
hosts: "{{ node_to_add }}"
gather_facts: true
become: true
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_add in groups['etcd_nodes']
tags: [etcd]
tasks:
- name: Install etcd binary
ansible.builtin.include_role:
name: etcd
tasks_from: install
- name: Copy CA and server/peer certs
ansible.builtin.copy:
src: "{{ etcd_pki_local_dir }}/{{ item.src }}"
dest: "{{ etcd_pki_dir }}/{{ item.dest }}"
owner: etcd
group: etcd
mode: "{{ item.mode }}"
loop:
- { src: "ca.crt", dest: "ca.crt", mode: "0644" }
- { src: "server-{{ inventory_hostname }}.crt", dest: "server.crt", mode: "0644" }
- { src: "server-{{ inventory_hostname }}.key", dest: "server.key", mode: "0600" }
- { src: "peer-{{ inventory_hostname }}.crt", dest: "peer.crt", mode: "0644" }
- { src: "peer-{{ inventory_hostname }}.key", dest: "peer.key", mode: "0600" }
- name: Register new etcd member in cluster
hosts: "{{ (groups['etcd_nodes'] | default([]) | reject('equalto', node_to_add | default('')) | list)[0] | default('') }}"
gather_facts: false
become: true
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_add in groups['etcd_nodes']
- (groups['etcd_nodes'] | reject('equalto', node_to_add) | list) | length > 0
tags: [etcd]
tasks:
- name: Add member via etcdctl
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 add "{{ node_to_add }}" \
--peer-urls="https://{{ hostvars[node_to_add]['ansible_host'] }}:{{ etcd_peer_port }}"
changed_when: true
- name: Start etcd on new etcd member
hosts: "{{ node_to_add }}"
gather_facts: true
become: true
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_add in groups['etcd_nodes']
tags: [etcd]
vars:
etcd_initial_cluster_state: "existing"
tasks:
- name: Build cluster members string
ansible.builtin.set_fact:
etcd_initial_cluster_members: >-
{%- set members = [] -%}
{%- for h in groups['etcd_nodes'] -%}
{%- set _ = members.append(h ~ '=https://' ~ hostvars[h]['ansible_host'] ~ ':' ~ etcd_peer_port) -%}
{%- endfor -%}
{{ members | join(',') }}
- name: Deploy etcd environment and start service
ansible.builtin.include_role:
name: etcd
tasks_from: service

89
playbooks/addons.yml Normal file
View File

@@ -0,0 +1,89 @@
---
# ─────────────────────────────────────────────────────────────────────────────
# Установка аддонов по флагам из group_vars/all/addons.yml
#
# Порядок важен: NFS → CSI → Ingress → cert-manager → остальные
#
# Запуск всех включённых аддонов: make install-full
# Конкретный аддон напрямую: make addon-<name>
# ─────────────────────────────────────────────────────────────────────────────
- name: Install NFS Server
hosts: nfs_server
gather_facts: true
become: true
when: addon_nfs_server | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/nfs-server/role"
- name: Install CSI NFS Driver
hosts: k3s_cluster
gather_facts: true
become: true
when: addon_csi_nfs | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/csi-nfs/role"
- name: Install ingress-nginx
hosts: k3s_cluster
gather_facts: true
become: true
when: addon_ingress_nginx | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/ingress-nginx/role"
- name: Install cert-manager
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_cert_manager | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/cert-manager/role"
- name: Install metrics-server
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_metrics_server | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/metrics-server/role"
- name: Install kube-prometheus-stack
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_prometheus_stack | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/prometheus-stack/role"
- name: Install Istio + Kiali
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_istio | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/istio/role"
- name: Install ArgoCD
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_argocd | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/argocd/role"
- name: Install Longhorn
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_longhorn | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/longhorn/role"
- name: Install Kubernetes Dashboard
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_kubernetes_dashboard | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/kubernetes-dashboard/role"

View File

@@ -0,0 +1,206 @@
---
# ─────────────────────────────────────────────────────────────────────────────
# Безопасно удалить ноду из внешнего 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.

View File

@@ -15,6 +15,56 @@
# После удаления мастера рекомендуется добавить новый: make add-node NODE=...
# ─────────────────────────────────────────────────────────────────────────────
# ── Удаляем из etcd кластера ПЕРЕД удалением из k3s ─────────────────────────
- name: Remove node from external etcd cluster (if applicable)
hosts: "{{ (groups['etcd_nodes'] | default([]) | reject('equalto', node_to_remove | default('')) | list)[0] | default('') }}"
gather_facts: false
become: true
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_remove is defined
- node_to_remove in groups['etcd_nodes']
- (groups['etcd_nodes'] | reject('equalto', node_to_remove) | list) | length > 0
tags: [etcd]
tasks:
- name: Warn about etcd membership removal
ansible.builtin.debug:
msg: >
Нода {{ node_to_remove }} является участником внешнего etcd кластера.
Удаляю из etcd перед удалением из k3s.
- name: Get member ID
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: _etcd_member_id
changed_when: false
failed_when: _etcd_member_id.rc != 0
- name: Remove member from etcd
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 {{ _etcd_member_id.stdout }}
changed_when: true
- name: Validate and prepare
hosts: "{{ groups['k3s_master'][0] }}"
gather_facts: false
@@ -91,6 +141,30 @@
- groups['k3s_workers'] is defined
- node_to_remove in groups['k3s_workers']
- name: Stop and disable etcd service (if running)
ansible.builtin.systemd:
name: etcd
state: stopped
enabled: false
failed_when: false
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_remove in groups['etcd_nodes']
- name: Remove etcd data and config (if applicable)
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ etcd_data_dir }}"
- "{{ etcd_config_dir }}"
failed_when: false
when:
- k3s_etcd_type | default('embedded') == 'external'
- groups['etcd_nodes'] is defined
- node_to_remove in groups['etcd_nodes']
- name: Clean up k3s data directory
ansible.builtin.file:
path: "{{ item }}"

View File

@@ -14,6 +14,16 @@
# Полный стек с аддонами: make install-full
# ─────────────────────────────────────────────────────────────────────────────
# ── 0. External etcd (если k3s_etcd_type: external) ──────────────────────────
- name: Deploy external etcd cluster
hosts: etcd_nodes
gather_facts: true
become: true
tags: [etcd]
when: k3s_etcd_type | default('embedded') == 'external'
roles:
- role: etcd
# ── 0. Chrony — синхронизация времени ────────────────────────────────────────
- name: Configure time synchronization (chrony)
hosts: k3s_cluster