feat: 纳管共享 etcd 与 k3s 外 PostgreSQL 高可用及备份
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
retry_files_enabled = False
|
||||
host_key_checking = True
|
||||
interpreter_python = auto_silent
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
# 与常规 site 分离,防止重建/恢复时默默创建新的认证域。
|
||||
- name: 初始化共享 etcd 认证
|
||||
hosts: etcd[0]
|
||||
become: true
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: 加载共享默认参数
|
||||
ansible.builtin.import_role:
|
||||
name: shared_etcd
|
||||
tasks_from: context
|
||||
environment:
|
||||
ETCDCTL_ENDPOINTS: "https://{{ etcd_address }}:{{ etcd_client_port }}"
|
||||
ETCDCTL_CACERT: "{{ etcd_config_dir }}/ca.crt"
|
||||
ETCDCTL_CERT: "{{ etcd_config_dir }}/admin.crt"
|
||||
ETCDCTL_KEY: "{{ etcd_config_dir }}/admin.key"
|
||||
tasks:
|
||||
- name: 读取认证状态
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, auth, status]
|
||||
changed_when: false
|
||||
register: etcd_auth_status
|
||||
check_mode: false
|
||||
|
||||
- name: 初始化管理员与认证
|
||||
when: not ((etcd_auth_status.stdout | from_json).enabled | default(false))
|
||||
block:
|
||||
- name: 要求显式初始化参数
|
||||
ansible.builtin.assert:
|
||||
that: etcd_bootstrap_auth | default(false) | bool
|
||||
fail_msg: 首次初始化需要 -e etcd_bootstrap_auth=true;常规运行不能重建认证。
|
||||
|
||||
- name: 读取现有用户
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, user, list]
|
||||
changed_when: false
|
||||
register: etcd_users
|
||||
|
||||
- name: 创建仅证书认证的 root 用户
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", user, add, root, --no-password]
|
||||
when: "'root' not in ((etcd_users.stdout | from_json).users | default([], true))"
|
||||
changed_when: true
|
||||
|
||||
- name: 读取管理员角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, user, get, root]
|
||||
changed_when: false
|
||||
register: etcd_root_roles
|
||||
|
||||
- name: 授予 root 管理角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", user, grant-role, root, root]
|
||||
when: "'root' not in ((etcd_root_roles.stdout | from_json).roles | default([], true))"
|
||||
changed_when: true
|
||||
|
||||
- name: 开启认证
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", auth, enable]
|
||||
changed_when: true
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
- name: 收敛消费者账号及独立 prefix
|
||||
hosts: etcd[0]
|
||||
become: true
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: 加载共享默认参数
|
||||
ansible.builtin.import_role:
|
||||
name: shared_etcd
|
||||
tasks_from: context
|
||||
environment:
|
||||
ETCDCTL_ENDPOINTS: "https://{{ etcd_address }}:{{ etcd_client_port }}"
|
||||
ETCDCTL_CACERT: "{{ etcd_config_dir }}/ca.crt"
|
||||
ETCDCTL_CERT: "{{ etcd_config_dir }}/admin.crt"
|
||||
ETCDCTL_KEY: "{{ etcd_config_dir }}/admin.key"
|
||||
tasks:
|
||||
- name: 检查认证状态
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, auth, status]
|
||||
register: etcd_auth_status
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: 要求认证已启用
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ((etcd_auth_status.stdout | from_json).enabled | default(false))
|
||||
- etcd_consumers | map(attribute='name') | unique | length == etcd_consumers | length
|
||||
- etcd_bao_token | length > 0
|
||||
no_log: true
|
||||
|
||||
- name: 逐个收敛消费者
|
||||
ansible.builtin.include_tasks: tasks/consumer.yml
|
||||
loop: "{{ etcd_consumers }}"
|
||||
loop_control:
|
||||
loop_var: etcd_consumer
|
||||
label: "{{ etcd_consumer.name }}"
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
# 仅在 Terraform 身份配置完成后部署;预检失败不启用定时器。
|
||||
- name: 安装独立于人工会话的续签调度
|
||||
hosts: etcd-laptop
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 创建 root 管理的程序目录
|
||||
ansible.builtin.file:
|
||||
path: /opt/homelab-etcd-controller
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: 安装证书登录与调度程序
|
||||
ansible.builtin.copy:
|
||||
src: "../controller/{{ item }}"
|
||||
dest: "/opt/homelab-etcd-controller/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
loop: [login.py, renew.py]
|
||||
- name: 以机器身份预检(不输出 token)
|
||||
ansible.builtin.command:
|
||||
argv: [/usr/bin/python3, /opt/homelab-etcd-controller/login.py]
|
||||
changed_when: false
|
||||
no_log: true
|
||||
- name: 安装固定的 Ansible 配置副本
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/"
|
||||
dest: /opt/homelab-etcd-controller/ansible/
|
||||
owner: root
|
||||
group: root
|
||||
mode: preserve
|
||||
- name: 创建调度状态目录
|
||||
ansible.builtin.file:
|
||||
path: /var/lib/homelab-etcd-controller
|
||||
state: directory
|
||||
owner: panxiao81
|
||||
group: panxiao81
|
||||
mode: '0700'
|
||||
- name: 安装续签 oneshot unit
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/homelab-etcd-renew.service
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Renew shared etcd certificates through Bao machine identity
|
||||
After=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=panxiao81
|
||||
Environment=HOME=/home/panxiao81
|
||||
Environment=PATH=/home/panxiao81/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/usr/bin/python3 /opt/homelab-etcd-controller/renew.py
|
||||
TimeoutStartSec=30min
|
||||
UMask=0077
|
||||
- name: 安装每日续签 timer
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/homelab-etcd-renew.timer
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Daily shared etcd certificate renewal check
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:10:00 UTC
|
||||
RandomizedDelaySec=15min
|
||||
Persistent=true
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
- name: 启用续签调度
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-etcd-renew.timer
|
||||
daemon_reload: true
|
||||
enabled: true
|
||||
state: started
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
etcd_bao_url: https://bao.ad.ddupan.top:8200
|
||||
etcd_bao_pki_mount: pki
|
||||
etcd_bao_kv_mount: kv
|
||||
etcd_bao_secret_base: infra/etcd/consumers
|
||||
# 只在控制端使用,不下发 Bao token。也不读取或复制历史 .vault_pass。
|
||||
etcd_bao_token: "{{ lookup('env', 'BAO_TOKEN') }}"
|
||||
etcd_consumers:
|
||||
- name: patroni-pg-prod
|
||||
prefix: /homelab/patroni/pg-prod/
|
||||
etcd_lxc_template: laptop:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst
|
||||
etcd_lxc_storage: pve-rg
|
||||
etcd_lxc_bridge: labnet
|
||||
etcd_lxc_gateway: 10.60.0.1
|
||||
etcd_lxc_memory: 512
|
||||
etcd_lxc_disk_gb: 8
|
||||
etcd_lxc_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
|
||||
|
||||
# 与现有 LAN exporter 相同的内网采集边界;禁止映射到公网。
|
||||
etcd_metrics_urls: "http://127.0.0.1:2381,http://{{ etcd_address }}:2381"
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
# 已部署成员;共置 PG standby/备份的承载预算,数据库数据用独立 HDD mp0。
|
||||
all:
|
||||
vars:
|
||||
ansible_user: root
|
||||
children:
|
||||
etcd:
|
||||
hosts:
|
||||
etcd-laptop:
|
||||
ansible_connection: local
|
||||
ansible_host: 192.168.10.127
|
||||
ansible_user: panxiao81
|
||||
etcd_address: 192.168.10.127
|
||||
etcd-pve1:
|
||||
ansible_host: 10.60.0.20
|
||||
etcd_address: 10.60.0.20
|
||||
etcd-pve2:
|
||||
ansible_host: 10.60.0.21
|
||||
etcd_address: 10.60.0.21
|
||||
etcd_pve:
|
||||
hosts:
|
||||
pve1:
|
||||
ansible_host: 192.168.10.4
|
||||
etcd_lxc_vmid: 150
|
||||
etcd_lxc_hostname: etcd-pve1
|
||||
etcd_lxc_address: 10.60.0.20/24
|
||||
etcd_lxc_memory: 1536
|
||||
pve2:
|
||||
ansible_host: 192.168.10.7
|
||||
etcd_lxc_vmid: 151
|
||||
etcd_lxc_hostname: etcd-pve2
|
||||
etcd_lxc_address: 10.60.0.21/24
|
||||
etcd_lxc_memory: 768
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
# 只创建已声明且尚不存在的 LXC;不接管未知 VMID,不重启现有容器。
|
||||
- name: 创建独立 etcd LXC
|
||||
hosts: etcd_pve
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 读取全局资源,避免 VMID 在其他节点已占用
|
||||
ansible.builtin.command:
|
||||
argv: [pvesh, get, /cluster/resources, --type, vm, --output-format, json]
|
||||
register: etcd_pve_resources
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: 保存同号资源
|
||||
ansible.builtin.set_fact:
|
||||
etcd_lxc_existing: >-
|
||||
{{ etcd_pve_resources.stdout
|
||||
| from_json
|
||||
| selectattr('vmid', 'equalto', etcd_lxc_vmid)
|
||||
| list }}
|
||||
|
||||
- name: 拒绝接管未知资源
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
etcd_lxc_existing | length == 0 or
|
||||
(etcd_lxc_existing[0].type == 'lxc' and etcd_lxc_existing[0].node == inventory_hostname
|
||||
and etcd_lxc_existing[0].name == etcd_lxc_hostname
|
||||
and 'shared-etcd' in (etcd_lxc_existing[0].tags | default('')))
|
||||
|
||||
- name: 新建无特权 LXC
|
||||
when: etcd_lxc_existing | length == 0 and not ansible_check_mode
|
||||
block:
|
||||
- name: 暂存 SSH 公钥
|
||||
ansible.builtin.copy:
|
||||
content: "{{ etcd_lxc_pubkey }}\n"
|
||||
dest: /run/shared-etcd-bootstrap.pub
|
||||
mode: '0600'
|
||||
- name: 创建声明的容器
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- create
|
||||
- "{{ etcd_lxc_vmid }}"
|
||||
- "{{ etcd_lxc_template }}"
|
||||
- --hostname
|
||||
- "{{ etcd_lxc_hostname }}"
|
||||
- --unprivileged
|
||||
- '1'
|
||||
- --cores
|
||||
- '1'
|
||||
- --memory
|
||||
- "{{ etcd_lxc_memory }}"
|
||||
- --swap
|
||||
- '0'
|
||||
- --rootfs
|
||||
- "{{ etcd_lxc_storage }}:{{ etcd_lxc_disk_gb }}"
|
||||
- --net0
|
||||
- "name=eth0,bridge={{ etcd_lxc_bridge }},ip={{ etcd_lxc_address }},gw={{ etcd_lxc_gateway }},type=veth"
|
||||
- --nameserver
|
||||
- 192.168.10.5
|
||||
- --searchdomain
|
||||
- ad.ddupan.top
|
||||
- --ssh-public-keys
|
||||
- /run/shared-etcd-bootstrap.pub
|
||||
- --onboot
|
||||
- '1'
|
||||
- --tags
|
||||
- ansible;shared-etcd
|
||||
changed_when: true
|
||||
always:
|
||||
- name: 删除暂存公钥
|
||||
ansible.builtin.file:
|
||||
path: /run/shared-etcd-bootstrap.pub
|
||||
state: absent
|
||||
|
||||
- name: 读取容器配置
|
||||
ansible.builtin.command:
|
||||
argv: [pct, config, "{{ etcd_lxc_vmid }}"]
|
||||
changed_when: false
|
||||
register: etcd_lxc_config
|
||||
when: not ansible_check_mode or etcd_lxc_existing | length > 0
|
||||
|
||||
- name: 配置漂移先报错,不直接改运行中的网络/资源
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
('ip=' ~ etcd_lxc_address ~ ',') in etcd_lxc_config.stdout or
|
||||
('ip=' ~ etcd_lxc_address ~ '\n') in etcd_lxc_config.stdout
|
||||
- "('bridge=' ~ etcd_lxc_bridge ~ ',') in etcd_lxc_config.stdout"
|
||||
- "'unprivileged: 1' in etcd_lxc_config.stdout"
|
||||
- "('memory: ' ~ etcd_lxc_memory) in etcd_lxc_config.stdout"
|
||||
- "('rootfs: ' ~ etcd_lxc_storage ~ ':') in etcd_lxc_config.stdout"
|
||||
when: etcd_lxc_config is not skipped
|
||||
|
||||
- name: 读取容器运行状态
|
||||
ansible.builtin.command:
|
||||
argv: [pct, status, "{{ etcd_lxc_vmid }}"]
|
||||
changed_when: false
|
||||
register: etcd_lxc_status
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 启动容器
|
||||
ansible.builtin.command:
|
||||
argv: [pct, start, "{{ etcd_lxc_vmid }}"]
|
||||
changed_when: true
|
||||
when: not ansible_check_mode and 'running' not in etcd_lxc_status.stdout
|
||||
|
||||
- name: 通过可信宿主机取得容器 SSH 公钥
|
||||
ansible.builtin.command:
|
||||
argv: [pct, exec, "{{ etcd_lxc_vmid }}", --, cat, /etc/ssh/ssh_host_ed25519_key.pub]
|
||||
register: etcd_lxc_hostkey
|
||||
changed_when: false
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: etcd_lxc_hostkey.rc == 0
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 保存经宿主机验证的 SSH host key
|
||||
ansible.builtin.known_hosts:
|
||||
name: "{{ etcd_lxc_address.split('/')[0] }}"
|
||||
key: "{{ etcd_lxc_address.split('/')[0] }} {{ etcd_lxc_hostkey.stdout }}"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
# 首次部署承载调整,逐节点迁移;普通 lxc.yml 不自动移动磁盘。
|
||||
- name: 逐个将新 etcd 容器迁入声明的 SSD 池
|
||||
hosts: etcd_pve
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
etcd_move_health_command:
|
||||
- /opt/homelab-etcd/etcdctl
|
||||
- --endpoints=https://192.168.10.127:2379,https://10.60.0.20:2379,https://10.60.0.21:2379
|
||||
- --cacert=/etc/homelab-etcd/ca.crt
|
||||
- --cert=/etc/homelab-etcd/admin.crt
|
||||
- --key=/etc/homelab-etcd/admin.key
|
||||
- endpoint
|
||||
- health
|
||||
tasks:
|
||||
- name: 核对容器配置
|
||||
ansible.builtin.command:
|
||||
argv: [pct, config, "{{ etcd_lxc_vmid }}"]
|
||||
register: etcd_move_config
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: 限定本项目新建容器与允许的源池
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- etcd_lxc_vmid in [150, 151]
|
||||
- "('hostname: ' ~ etcd_lxc_hostname) in etcd_move_config.stdout"
|
||||
- "'shared-etcd' in etcd_move_config.stdout"
|
||||
- "'rootfs: local-lvm:' in etcd_move_config.stdout or 'rootfs: pve-rg:' in etcd_move_config.stdout"
|
||||
- etcd_lxc_storage == 'pve-rg'
|
||||
|
||||
- name: 验证迁移前全部端点健康
|
||||
ansible.builtin.command:
|
||||
argv: "{{ etcd_move_health_command }}"
|
||||
delegate_to: localhost
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: 迁移当前尚在本地池的根卷
|
||||
when: "'rootfs: local-lvm:' in etcd_move_config.stdout and not ansible_check_mode"
|
||||
block:
|
||||
- name: 创建已验证的集群快照
|
||||
ansible.builtin.command:
|
||||
argv: [systemctl, start, homelab-etcd-snapshot.service]
|
||||
delegate_to: localhost
|
||||
changed_when: true
|
||||
|
||||
- name: 正常关闭一个容器
|
||||
ansible.builtin.command:
|
||||
argv: [pct, shutdown, "{{ etcd_lxc_vmid }}", --timeout, '60']
|
||||
changed_when: true
|
||||
|
||||
- name: 复制成功后移除该新建容器的原卷
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- move-volume
|
||||
- "{{ etcd_lxc_vmid }}"
|
||||
- rootfs
|
||||
- "{{ etcd_lxc_storage }}"
|
||||
- --delete
|
||||
- '1'
|
||||
- --bwlimit
|
||||
- '32768'
|
||||
changed_when: true
|
||||
always:
|
||||
- name: 核对容器运行状态
|
||||
ansible.builtin.command:
|
||||
argv: [pct, status, "{{ etcd_lxc_vmid }}"]
|
||||
register: etcd_move_status
|
||||
changed_when: false
|
||||
- name: 重新启动容器
|
||||
ansible.builtin.command:
|
||||
argv: [pct, start, "{{ etcd_lxc_vmid }}"]
|
||||
when: "'running' not in etcd_move_status.stdout"
|
||||
changed_when: true
|
||||
|
||||
- name: 等待当前成员回归
|
||||
ansible.builtin.command:
|
||||
argv: "{{ etcd_move_health_command }}"
|
||||
delegate_to: localhost
|
||||
changed_when: false
|
||||
register: etcd_move_health
|
||||
retries: 18
|
||||
delay: 5
|
||||
until: etcd_move_health.rc == 0
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.crypto
|
||||
version: 3.2.1
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
etcd_version: 3.7.2
|
||||
etcd_archive_checksum: sha256:3a3679bc51a4ee9d30bccea1da7cd4fe62c6fc1d2ca1255068d2c53bf3026135
|
||||
etcd_install_dir: /opt/homelab-etcd
|
||||
etcd_config_dir: /etc/homelab-etcd
|
||||
etcd_data_dir: /var/lib/homelab-etcd
|
||||
etcd_cluster_token: homelab-shared-etcd-v1
|
||||
etcd_client_port: 2379
|
||||
etcd_peer_port: 2380
|
||||
etcd_metrics_port: 2381
|
||||
etcd_quota_bytes: 268435456
|
||||
etcd_memory_high: 256M
|
||||
etcd_memory_max: 384M
|
||||
etcd_certificate_ttl: 1440h
|
||||
etcd_renew_before: +14d
|
||||
etcd_snapshot_dir: /var/backups/homelab-etcd
|
||||
etcd_snapshot_keep: 3
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
- name: 在成员本地生成私钥
|
||||
community.crypto.openssl_privatekey:
|
||||
path: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.key"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
owner: root
|
||||
group: "{{ etcd_cert.group }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: 本地生成 CSR
|
||||
community.crypto.openssl_csr:
|
||||
path: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.csr"
|
||||
privatekey_path: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.key"
|
||||
common_name: "{{ etcd_cert.cn | default(omit, true) }}"
|
||||
subject_alt_name: >-
|
||||
{{ ['IP:' ~ etcd_address, 'DNS:' ~ inventory_hostname] if etcd_cert.name in ['server', 'peer']
|
||||
else (['DNS:' ~ inventory_hostname] if etcd_cert.name == 'gateway' else []) }}
|
||||
use_common_name_for_san: false
|
||||
extended_key_usage: "{{ etcd_cert.eku }}"
|
||||
key_usage: [digitalSignature]
|
||||
mode: '0644'
|
||||
register: etcd_csr_state
|
||||
|
||||
- name: 检查已有证书
|
||||
ansible.builtin.stat:
|
||||
path: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.crt"
|
||||
register: etcd_cert_file
|
||||
|
||||
- name: 检查续签窗口
|
||||
community.crypto.x509_certificate_info:
|
||||
path: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.crt"
|
||||
valid_at:
|
||||
renewal: "{{ etcd_renew_before }}"
|
||||
register: etcd_cert_info
|
||||
when: etcd_cert_file.stat.exists
|
||||
|
||||
- name: 通过中央 CA 签发需更新的证书
|
||||
when: >-
|
||||
not etcd_cert_file.stat.exists or etcd_csr_state is changed or
|
||||
not (etcd_cert_info.valid_at.renewal | default(false))
|
||||
block:
|
||||
- name: 仅在确需签发时要求 Bao 凭据
|
||||
ansible.builtin.assert:
|
||||
that: etcd_bao_token | length > 0
|
||||
no_log: true
|
||||
|
||||
- name: 读取 CSR 公共内容
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.csr"
|
||||
register: etcd_csr
|
||||
|
||||
- name: 控制端提交 Bao 签名请求
|
||||
ansible.builtin.uri:
|
||||
url: "{{ etcd_bao_url }}/v1/{{ etcd_bao_pki_mount }}/sign/{{ etcd_cert.role }}"
|
||||
method: POST
|
||||
headers:
|
||||
X-Vault-Token: "{{ etcd_bao_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
csr: "{{ etcd_csr.content | b64decode }}"
|
||||
ttl: "{{ etcd_certificate_ttl }}"
|
||||
status_code: 200
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: etcd_signed
|
||||
no_log: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 保存签发的证书链
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{{ etcd_signed.json.data.certificate }}
|
||||
{{ etcd_signed.json.data.ca_chain | join('\n') }}
|
||||
dest: "{{ etcd_config_dir }}/{{ etcd_cert.name }}.crt"
|
||||
owner: root
|
||||
group: "{{ etcd_cert.group }}"
|
||||
mode: '0644'
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 保存中央 CA 信任链
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{{ etcd_signed.json.data.ca_chain | join('\n') }}
|
||||
dest: "{{ etcd_config_dir }}/ca.crt"
|
||||
owner: root
|
||||
group: homelab-etcd
|
||||
mode: '0644'
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 标记证书已更新
|
||||
ansible.builtin.set_fact:
|
||||
etcd_certificates_changed: true
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# 只加载 role defaults,供运维入口复用。
|
||||
[]
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
- name: 验证拓扑和签发配置
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- groups['etcd'] | length == 3
|
||||
- groups['etcd'] | map('extract', hostvars, 'etcd_address') | unique | length == 3
|
||||
- ansible_facts['architecture'] == 'x86_64'
|
||||
- etcd_bao_url is match('^https://')
|
||||
- etcd_archive_checksum is match('^sha256:[a-f0-9]{64}$')
|
||||
no_log: true
|
||||
|
||||
- name: 安装证书处理依赖
|
||||
ansible.builtin.package:
|
||||
name: [python3-cryptography, openssl]
|
||||
state: present
|
||||
|
||||
- name: 创建独立 etcd 组
|
||||
ansible.builtin.group:
|
||||
name: homelab-etcd
|
||||
system: true
|
||||
|
||||
- name: 创建独立 etcd 用户
|
||||
ansible.builtin.user:
|
||||
name: homelab-etcd
|
||||
group: homelab-etcd
|
||||
system: true
|
||||
shell: /usr/sbin/nologin
|
||||
create_home: false
|
||||
|
||||
- name: 创建受管目录
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ item.owner }}"
|
||||
group: homelab-etcd
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- {path: "{{ etcd_config_dir }}", owner: root, mode: '0750'}
|
||||
- {path: "{{ etcd_data_dir }}", owner: homelab-etcd, mode: '0700'}
|
||||
- {path: "{{ etcd_install_dir }}", owner: root, mode: '0755'}
|
||||
|
||||
- name: 下载固定版本及校验归档
|
||||
ansible.builtin.get_url:
|
||||
url: >-
|
||||
https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz
|
||||
dest: "{{ etcd_install_dir }}/etcd-v{{ etcd_version }}.tar.gz"
|
||||
checksum: "{{ etcd_archive_checksum }}"
|
||||
mode: '0644'
|
||||
register: etcd_download
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: etcd_download is succeeded
|
||||
|
||||
- name: 展开固定版本
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ etcd_install_dir }}/etcd-v{{ etcd_version }}.tar.gz"
|
||||
dest: "{{ etcd_install_dir }}"
|
||||
remote_src: true
|
||||
creates: "{{ etcd_install_dir }}/etcd-v{{ etcd_version }}-linux-amd64/etcd"
|
||||
|
||||
- name: 安装版本链接
|
||||
ansible.builtin.file:
|
||||
src: "{{ etcd_install_dir }}/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}"
|
||||
dest: "{{ etcd_install_dir }}/{{ item }}"
|
||||
state: link
|
||||
loop: [etcd, etcdctl, etcdutl]
|
||||
register: etcd_binary_links
|
||||
|
||||
- name: 签发成员与管理员证书
|
||||
ansible.builtin.include_tasks: certificate.yml
|
||||
loop:
|
||||
- {name: server, role: homelab-etcd-server, cn: "{{ inventory_hostname }}", eku: [serverAuth], group: homelab-etcd}
|
||||
- {name: peer, role: homelab-etcd-peer, cn: homelab-etcd-peer, eku: [serverAuth, clientAuth], group: homelab-etcd}
|
||||
- {name: gateway, role: homelab-etcd-gateway, cn: "", eku: [clientAuth], group: homelab-etcd}
|
||||
- {name: admin, role: homelab-etcd-admin, cn: root, eku: [clientAuth], group: root}
|
||||
loop_control:
|
||||
loop_var: etcd_cert
|
||||
|
||||
- name: 写入独立 etcd 配置
|
||||
ansible.builtin.template:
|
||||
src: etcd.yml.j2
|
||||
dest: "{{ etcd_config_dir }}/etcd.yml"
|
||||
owner: root
|
||||
group: homelab-etcd
|
||||
mode: '0640'
|
||||
register: etcd_config_file
|
||||
|
||||
- name: 写入独立 systemd unit
|
||||
ansible.builtin.template:
|
||||
src: homelab-etcd.service.j2
|
||||
dest: /etc/systemd/system/homelab-etcd.service
|
||||
mode: '0644'
|
||||
register: etcd_unit
|
||||
|
||||
# 跨失败重跑记录激活状态,防止上一轮写文件后中断导致漏掉必要重启。
|
||||
- name: 计算受管文件校验和
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
checksum_algorithm: sha256
|
||||
loop:
|
||||
- "{{ etcd_config_dir }}/etcd.yml"
|
||||
- "{{ etcd_config_dir }}/server.crt"
|
||||
- "{{ etcd_config_dir }}/peer.crt"
|
||||
- "{{ etcd_config_dir }}/gateway.crt"
|
||||
- "{{ etcd_config_dir }}/ca.crt"
|
||||
- /etc/systemd/system/homelab-etcd.service
|
||||
register: etcd_managed_files
|
||||
|
||||
- name: 计算期望激活指纹
|
||||
ansible.builtin.set_fact:
|
||||
etcd_config_fingerprint: >-
|
||||
{{ ((etcd_managed_files.results | map(attribute='stat.checksum') | list | join(':'))
|
||||
~ ':' ~ etcd_version) | hash('sha256') }}
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 检查已激活指纹
|
||||
ansible.builtin.stat:
|
||||
path: "{{ etcd_config_dir }}/activated.sha256"
|
||||
register: etcd_activated_file
|
||||
|
||||
- name: 读取已激活指纹
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ etcd_config_dir }}/activated.sha256"
|
||||
register: etcd_activated
|
||||
when: etcd_activated_file.stat.exists
|
||||
|
||||
- name: 判断是否需要滚动激活
|
||||
ansible.builtin.set_fact:
|
||||
etcd_config_changed: >-
|
||||
{{ not etcd_activated_file.stat.exists or
|
||||
(etcd_activated.content | default('') | b64decode | trim) != etcd_config_fingerprint | default('check-mode') }}
|
||||
|
||||
- name: 管理本地快照任务
|
||||
ansible.builtin.import_tasks: snapshot.yml
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
- name: 创建仅 root 可访问的快照目录
|
||||
ansible.builtin.file:
|
||||
path: "{{ etcd_snapshot_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0700'
|
||||
|
||||
- name: 写入快照脚本和 systemd 任务
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- {src: snapshot.sh.j2, dest: "{{ etcd_install_dir }}/snapshot", mode: '0700'}
|
||||
- {src: snapshot.service.j2, dest: /etc/systemd/system/homelab-etcd-snapshot.service, mode: '0644'}
|
||||
- {src: snapshot.timer.j2, dest: /etc/systemd/system/homelab-etcd-snapshot.timer, mode: '0644'}
|
||||
register: etcd_snapshot_units
|
||||
|
||||
- name: 启用本地快照计划
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-etcd-snapshot.timer
|
||||
daemon_reload: "{{ etcd_snapshot_units is changed }}"
|
||||
enabled: true
|
||||
state: started
|
||||
@@ -0,0 +1,33 @@
|
||||
# Ansible 管理;与 k3s、数据库生命周期独立。
|
||||
name: {{ inventory_hostname | to_json }}
|
||||
data-dir: {{ etcd_data_dir | to_json }}
|
||||
listen-client-urls: https://{{ etcd_address }}:{{ etcd_client_port }}
|
||||
advertise-client-urls: https://{{ etcd_address }}:{{ etcd_client_port }}
|
||||
listen-peer-urls: https://{{ etcd_address }}:{{ etcd_peer_port }}
|
||||
initial-advertise-peer-urls: https://{{ etcd_address }}:{{ etcd_peer_port }}
|
||||
initial-cluster: "{% for member in groups['etcd'] %}{{ member }}=https://{{ hostvars[member].etcd_address }}:{{ etcd_peer_port }}{{ ',' if not loop.last else '' }}{% endfor %}"
|
||||
initial-cluster-token: {{ etcd_cluster_token | to_json }}
|
||||
initial-cluster-state: new
|
||||
# 已存在的数据目录优先;成员替换必须走单独 runbook,不删除数据重建。
|
||||
client-transport-security:
|
||||
cert-file: {{ etcd_config_dir }}/server.crt
|
||||
key-file: {{ etcd_config_dir }}/server.key
|
||||
client-cert-file: {{ etcd_config_dir }}/gateway.crt
|
||||
client-key-file: {{ etcd_config_dir }}/gateway.key
|
||||
trusted-ca-file: {{ etcd_config_dir }}/ca.crt
|
||||
client-cert-auth: true
|
||||
peer-transport-security:
|
||||
cert-file: {{ etcd_config_dir }}/peer.crt
|
||||
key-file: {{ etcd_config_dir }}/peer.key
|
||||
trusted-ca-file: {{ etcd_config_dir }}/ca.crt
|
||||
client-cert-auth: true
|
||||
allowed-cn: [homelab-etcd-peer]
|
||||
# 独立 metrics listener 仅提供指标/健康,不开放 KV API;只绑定受管内网地址。
|
||||
listen-metrics-urls: {{ etcd_metrics_urls | default("http://127.0.0.1:" ~ etcd_metrics_port) | to_json }}
|
||||
quota-backend-bytes: {{ etcd_quota_bytes }}
|
||||
auto-compaction-mode: periodic
|
||||
auto-compaction-retention: '1h'
|
||||
heartbeat-interval: 100
|
||||
election-timeout: 1000
|
||||
logger: zap
|
||||
log-level: info
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Homelab shared etcd
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
User=homelab-etcd
|
||||
Group=homelab-etcd
|
||||
ExecStart={{ etcd_install_dir }}/etcd --config-file={{ etcd_config_dir }}/etcd.yml
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=60
|
||||
MemoryHigh={{ etcd_memory_high }}
|
||||
MemoryMax={{ etcd_memory_max }}
|
||||
UMask=0077
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ReadWritePaths={{ etcd_data_dir }}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Snapshot homelab shared etcd
|
||||
After=homelab-etcd.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart={{ etcd_install_dir }}/snapshot
|
||||
User=root
|
||||
UMask=0077
|
||||
TimeoutStartSec=300
|
||||
Nice=10
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
export ETCDCTL_ENDPOINTS="https://{{ etcd_address }}:{{ etcd_client_port }}"
|
||||
export ETCDCTL_CACERT="{{ etcd_config_dir }}/ca.crt"
|
||||
export ETCDCTL_CERT="{{ etcd_config_dir }}/admin.crt"
|
||||
export ETCDCTL_KEY="{{ etcd_config_dir }}/admin.key"
|
||||
repo="{{ etcd_snapshot_dir }}"
|
||||
exec 9>"$repo/.lock"
|
||||
flock -n 9 || exit 0
|
||||
output="$repo/$(date -u +%Y%m%dT%H%M%SZ).db"
|
||||
trap 'rm -f "$output.partial" "$output.partial.part"' EXIT
|
||||
{{ etcd_install_dir }}/etcdctl snapshot save "$output.partial"
|
||||
{{ etcd_install_dir }}/etcdutl snapshot status "$output.partial" >/dev/null
|
||||
mv "$output.partial" "$output"
|
||||
# 只有新快照成功且验证可读才清理旧备份。目录仅由此任务管理。
|
||||
mapfile -t snapshots < <(find "$repo" -maxdepth 1 -type f -name '????????T??????Z.db' -printf '%f\n' | sort -r)
|
||||
for old in "${snapshots[@]:{{ etcd_snapshot_keep }}}"; do
|
||||
rm -- "$repo/$old"
|
||||
done
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Daily homelab etcd snapshot
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:20:00 UTC
|
||||
RandomizedDelaySec=300
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
# 先配置全体,再滚动启动:首次集群形成不能在第一个节点等待 quorum。
|
||||
- name: 配置 shared etcd 成员
|
||||
hosts: etcd
|
||||
become: true
|
||||
roles:
|
||||
- shared_etcd
|
||||
|
||||
- name: 滚动启动并验证 shared etcd
|
||||
hosts: etcd
|
||||
become: true
|
||||
serial: 1
|
||||
pre_tasks:
|
||||
- name: 加载共享默认参数
|
||||
ansible.builtin.import_role:
|
||||
name: shared_etcd
|
||||
tasks_from: context
|
||||
tasks:
|
||||
- name: 启动已配置的成员
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-etcd
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
state: "{{ 'restarted' if etcd_config_changed | bool else 'started' }}"
|
||||
- name: 等待本机客户端端口
|
||||
ansible.builtin.wait_for:
|
||||
host: "{{ etcd_address }}"
|
||||
port: "{{ etcd_client_port }}"
|
||||
timeout: 60
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: 已有集群每次激活后等待本成员恢复 quorum 通信
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ etcd_install_dir }}/etcdctl"
|
||||
- --endpoints=https://{{ etcd_address }}:{{ etcd_client_port }}
|
||||
- --cacert={{ etcd_config_dir }}/ca.crt
|
||||
- --cert={{ etcd_config_dir }}/admin.crt
|
||||
- --key={{ etcd_config_dir }}/admin.key
|
||||
- endpoint
|
||||
- health
|
||||
changed_when: false
|
||||
register: etcd_member_health
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: etcd_member_health.rc == 0
|
||||
when: etcd_activated_file.stat.exists and not ansible_check_mode
|
||||
|
||||
- name: 核对全部成员
|
||||
ansible.builtin.import_playbook: verify.yml
|
||||
|
||||
- name: 记录已成功激活的配置
|
||||
hosts: etcd
|
||||
become: true
|
||||
pre_tasks:
|
||||
- name: 加载共享默认参数
|
||||
ansible.builtin.import_role:
|
||||
name: shared_etcd
|
||||
tasks_from: context
|
||||
tasks:
|
||||
- name: 写入激活指纹(全体健康检查通过后)
|
||||
ansible.builtin.copy:
|
||||
content: "{{ etcd_config_fingerprint }}\n"
|
||||
dest: "{{ etcd_config_dir }}/activated.sha256"
|
||||
mode: '0644'
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
- name: 验证消费者范围
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- etcd_consumer.name is match('^[a-z][a-z0-9-]+$')
|
||||
- etcd_consumer.name != 'root'
|
||||
- etcd_consumer.prefix is match('^/homelab/[a-zA-Z0-9/_-]+/$')
|
||||
- etcd_consumer.prefix | length > 10
|
||||
|
||||
- name: 读取已有用户和角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, "{{ item }}", list]
|
||||
loop: [user, role]
|
||||
register: etcd_identities
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
# 404 data 也可能代表被删除/销毁的旧秘密;只有 metadata 不存在才允许生成。
|
||||
- name: 控制端读取 Bao 秘密元数据
|
||||
ansible.builtin.uri:
|
||||
url: "{{ etcd_bao_url }}/v1/{{ etcd_bao_kv_mount }}/metadata/{{ etcd_bao_secret_base }}/{{ etcd_consumer.name }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ etcd_bao_token }}"
|
||||
status_code: [200, 404]
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
no_log: true
|
||||
register: etcd_secret_metadata
|
||||
check_mode: false
|
||||
|
||||
- name: 禁止已有用户丢失秘密后自动换密码
|
||||
ansible.builtin.assert:
|
||||
that: >-
|
||||
etcd_secret_metadata.status == 200 or
|
||||
etcd_consumer.name not in ((etcd_identities.results[0].stdout | from_json).users | default([], true))
|
||||
fail_msg: etcd 用户已存在但 Bao 秘密缺失;需恢复原秘密或执行显式轮换。
|
||||
|
||||
- name: 首次创建随机秘密且禁止覆盖已有版本
|
||||
ansible.builtin.uri:
|
||||
url: "{{ etcd_bao_url }}/v1/{{ etcd_bao_kv_mount }}/data/{{ etcd_bao_secret_base }}/{{ etcd_consumer.name }}"
|
||||
method: POST
|
||||
headers:
|
||||
X-Vault-Token: "{{ etcd_bao_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
options: {cas: 0}
|
||||
data:
|
||||
username: "{{ etcd_consumer.name }}"
|
||||
password: "{{ lookup('ansible.builtin.password', '/dev/null', length=48, chars=['ascii_letters', 'digits']) }}"
|
||||
prefix: "{{ etcd_consumer.prefix }}"
|
||||
status_code: 200
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
no_log: true
|
||||
changed_when: true
|
||||
when:
|
||||
- etcd_secret_metadata.status == 404
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: 控制端读取既有秘密
|
||||
ansible.builtin.uri:
|
||||
url: "{{ etcd_bao_url }}/v1/{{ etcd_bao_kv_mount }}/data/{{ etcd_bao_secret_base }}/{{ etcd_consumer.name }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ etcd_bao_token }}"
|
||||
status_code: 200
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: etcd_consumer_secret
|
||||
no_log: true
|
||||
when: etcd_secret_metadata.status == 200 or not ansible_check_mode
|
||||
check_mode: false
|
||||
|
||||
- name: 核对已保存秘密归属
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- etcd_consumer_secret.json.data.data.username == etcd_consumer.name
|
||||
- etcd_consumer_secret.json.data.data.prefix == etcd_consumer.prefix
|
||||
- etcd_consumer_secret.json.data.data.password | length >= 32
|
||||
no_log: true
|
||||
when: etcd_secret_metadata.status == 200 or not ansible_check_mode
|
||||
|
||||
- name: 新建消费者用户(密码只通过 stdin 传递)
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", user, add, "{{ etcd_consumer.name }}", --interactive=false]
|
||||
stdin: "{{ etcd_consumer_secret.json.data.data.password }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
when:
|
||||
- etcd_consumer.name not in ((etcd_identities.results[0].stdout | from_json).users | default([], true))
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: 新建消费者角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", role, add, "{{ etcd_consumer.name }}"]
|
||||
changed_when: true
|
||||
when: etcd_consumer.name not in ((etcd_identities.results[1].stdout | from_json).roles | default([], true))
|
||||
|
||||
- name: 读取角色权限
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, role, get, "{{ etcd_consumer.name }}"]
|
||||
register: etcd_role_state
|
||||
changed_when: false
|
||||
when: >-
|
||||
not ansible_check_mode or
|
||||
etcd_consumer.name in ((etcd_identities.results[1].stdout | from_json).roles | default([], true))
|
||||
|
||||
- name: 拒绝不符合声明的既有权限(不自动扩大或删除)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
((etcd_role_state.stdout | from_json).perm | default([], true)) in
|
||||
[[], [{'permType': 2, 'key': etcd_consumer.prefix | b64encode,
|
||||
'range_end': (etcd_consumer.prefix[:-1] ~ '0') | b64encode}]]
|
||||
fail_msg: 既有角色权限与声明不同,请显式审查权限迁移。
|
||||
when: etcd_role_state is not skipped
|
||||
|
||||
- name: 授予唯一 prefix 读写权限
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ etcd_install_dir }}/etcdctl"
|
||||
- role
|
||||
- grant-permission
|
||||
- "{{ etcd_consumer.name }}"
|
||||
- readwrite
|
||||
- "{{ etcd_consumer.prefix }}"
|
||||
- --prefix=true
|
||||
changed_when: true
|
||||
when:
|
||||
- etcd_role_state is not skipped
|
||||
- ((etcd_role_state.stdout | from_json).perm | default([], true)) | length == 0
|
||||
|
||||
- name: 读取用户角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", --write-out=json, user, get, "{{ etcd_consumer.name }}"]
|
||||
register: etcd_user_state
|
||||
changed_when: false
|
||||
when: >-
|
||||
not ansible_check_mode or
|
||||
etcd_consumer.name in ((etcd_identities.results[0].stdout | from_json).users | default([], true))
|
||||
|
||||
- name: 拒绝消费者已有额外角色
|
||||
ansible.builtin.assert:
|
||||
that: >-
|
||||
((etcd_user_state.stdout | from_json).roles | default([], true))
|
||||
| difference([etcd_consumer.name]) | length == 0
|
||||
when: etcd_user_state is not skipped
|
||||
|
||||
- name: 绑定消费者角色
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ etcd_install_dir }}/etcdctl", user, grant-role, "{{ etcd_consumer.name }}", "{{ etcd_consumer.name }}"]
|
||||
changed_when: true
|
||||
when:
|
||||
- etcd_user_state is not skipped
|
||||
- etcd_consumer.name not in ((etcd_user_state.stdout | from_json).roles | default([], true))
|
||||
|
||||
# 使用 gateway 的真实密码登录来核对 Bao 与 etcd 一致性,不能只看用户已存在。
|
||||
- name: 验证消费者密码可经 gateway 登录
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ etcd_address }}:{{ etcd_client_port }}/v3/auth/authenticate"
|
||||
method: POST
|
||||
client_cert: "{{ etcd_config_dir }}/gateway.crt"
|
||||
client_key: "{{ etcd_config_dir }}/gateway.key"
|
||||
ca_path: "{{ etcd_config_dir }}/ca.crt"
|
||||
body_format: json
|
||||
body:
|
||||
name: "{{ etcd_consumer.name }}"
|
||||
password: "{{ etcd_consumer_secret.json.data.data.password }}"
|
||||
status_code: 200
|
||||
no_log: true
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: 验证 shared etcd 全部端点
|
||||
hosts: etcd[0]
|
||||
become: true
|
||||
gather_facts: false
|
||||
pre_tasks:
|
||||
- name: 加载共享默认参数
|
||||
ansible.builtin.import_role:
|
||||
name: shared_etcd
|
||||
tasks_from: context
|
||||
tasks:
|
||||
- name: 通过管理员 mTLS 检查所有端点健康
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ etcd_install_dir }}/etcdctl"
|
||||
- >-
|
||||
--endpoints={{ groups['etcd'] | map('extract', hostvars, 'etcd_address')
|
||||
| map('regex_replace', '^(.*)$', 'https://\1:' ~ etcd_client_port) | join(',') }}
|
||||
- --cacert={{ etcd_config_dir }}/ca.crt
|
||||
- --cert={{ etcd_config_dir }}/admin.crt
|
||||
- --key={{ etcd_config_dir }}/admin.key
|
||||
- endpoint
|
||||
- health
|
||||
changed_when: false
|
||||
register: etcd_health
|
||||
retries: 12
|
||||
delay: 5
|
||||
until: etcd_health.rc == 0
|
||||
when: not ansible_check_mode
|
||||
Reference in New Issue
Block a user