feat: 纳管共享 etcd 与 k3s 外 PostgreSQL 高可用及备份
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = True
|
||||
retry_files_enabled = False
|
||||
interpreter_python = auto_silent
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
- name: 读取现有实例管理凭据
|
||||
ansible.builtin.import_playbook: credentials.yml
|
||||
- name: 将 Ayatori 身份交付到独立路径(不携带 superuser 凭据)
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 逐实例 CAS 创建并核对,只接受一致的已有值
|
||||
ansible.builtin.include_tasks: tasks/ayatori-credential.yml
|
||||
loop: [prod, dev]
|
||||
loop_control:
|
||||
loop_var: pg_ayatori_instance
|
||||
@@ -0,0 +1,160 @@
|
||||
---
|
||||
- name: 配置独立备份仓库账号与工具
|
||||
hosts: pg_repository
|
||||
become: true
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: 必须先挂载专属数据盘
|
||||
ansible.builtin.command:
|
||||
argv: [findmnt, --mountpoint, "{{ pg_repo_path }}", --noheadings]
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 配置与数据库相同的软件源
|
||||
ansible.builtin.import_role:
|
||||
name: pg_packages
|
||||
tasks_from: repository
|
||||
- name: 安装备份工具(不安装数据库)
|
||||
ansible.builtin.apt:
|
||||
name: [pgbackrest, openssh-client]
|
||||
state: present
|
||||
install_recommends: false
|
||||
- name: 创建仓库组
|
||||
ansible.builtin.group:
|
||||
name: pgbackup
|
||||
system: true
|
||||
- name: 创建仓库账号
|
||||
ansible.builtin.user:
|
||||
name: pgbackup
|
||||
group: pgbackup
|
||||
home: "{{ pg_repo_path }}"
|
||||
create_home: false
|
||||
system: true
|
||||
shell: /bin/bash
|
||||
- name: 设置仓库目录权限
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_repo_path }}"
|
||||
state: directory
|
||||
owner: pgbackup
|
||||
group: pgbackup
|
||||
mode: '0700'
|
||||
|
||||
- name: 建立专用 SSH 传输身份
|
||||
hosts: pg_hosts:pg_repository
|
||||
become: true
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
pg_transport_user: "{{ 'pgbackup' if inventory_hostname in groups['pg_repository'] else 'pgprod' }}"
|
||||
pg_transport_home: "{{ pg_repo_path if inventory_hostname in groups['pg_repository'] else pg_data_root ~ '/prod' }}"
|
||||
tasks:
|
||||
- name: 创建 SSH 目录
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_transport_home }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0700'
|
||||
# community.crypto 的跨文件系统 preserved_copy 会把 ext4 属性带到 ZFS,产生 chattr 错误。
|
||||
- name: 仅在缺失时原地生成专属传输私钥
|
||||
ansible.builtin.command:
|
||||
argv: [ssh-keygen, -q, -t, ed25519, -N, '', -f, "{{ pg_transport_home }}/.ssh/id_ed25519"]
|
||||
creates: "{{ pg_transport_home }}/.ssh/id_ed25519"
|
||||
- name: 收敛私钥属主与权限
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_transport_home }}/.ssh/id_ed25519"
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0600'
|
||||
- name: 从既有私钥派生公开部分,不重新生成密钥
|
||||
ansible.builtin.command:
|
||||
argv: [ssh-keygen, -y, -f, "{{ pg_transport_home }}/.ssh/id_ed25519"]
|
||||
register: pg_transport_key
|
||||
changed_when: false
|
||||
- name: 保存对应公钥
|
||||
ansible.builtin.copy:
|
||||
content: "{{ pg_transport_key.stdout }}\n"
|
||||
dest: "{{ pg_transport_home }}/.ssh/id_ed25519.pub"
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0644'
|
||||
- name: 经已信任 Ansible 通道读取 SSH 主机公钥
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/ssh/ssh_host_ed25519_key.pub
|
||||
register: pg_transport_host_key
|
||||
|
||||
- name: 配置专用传输授权(不允许转发和 PTY)
|
||||
hosts: pg_hosts:pg_repository
|
||||
become: true
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
pg_transport_user: "{{ 'pgbackup' if inventory_hostname in groups['pg_repository'] else 'pgprod' }}"
|
||||
pg_transport_home: "{{ pg_repo_path if inventory_hostname in groups['pg_repository'] else pg_data_root ~ '/prod' }}"
|
||||
pg_transport_peers: >-
|
||||
{{ groups['pg_hosts'] if inventory_hostname in groups['pg_repository'] else groups['pg_repository'] }}
|
||||
tasks:
|
||||
- name: 限定专用账号的对端密钥
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{% for peer in pg_transport_peers %}
|
||||
restrict {{ hostvars[peer].pg_transport_key.stdout }}
|
||||
{% endfor %}
|
||||
dest: "{{ pg_transport_home }}/.ssh/authorized_keys"
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0600'
|
||||
- name: 保持专用 SSH 信任文件只由 IaC 管理
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
Host {{ pg_transport_peers | map('extract', hostvars, 'ansible_host') | join(' ') }}
|
||||
StrictHostKeyChecking yes
|
||||
UpdateHostKeys no
|
||||
IdentitiesOnly yes
|
||||
IdentityFile {{ pg_transport_home }}/.ssh/id_ed25519
|
||||
UserKnownHostsFile {{ pg_transport_home }}/.ssh/known_hosts
|
||||
dest: "{{ pg_transport_home }}/.ssh/config"
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0600'
|
||||
- name: 固定对端 SSH 主机身份
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{% for peer in pg_transport_peers %}
|
||||
{{ hostvars[peer].ansible_host }} {{ hostvars[peer].pg_transport_host_key.content | b64decode | trim }}
|
||||
{% endfor %}
|
||||
dest: "{{ pg_transport_home }}/.ssh/known_hosts"
|
||||
owner: "{{ pg_transport_user }}"
|
||||
group: "{{ pg_transport_user }}"
|
||||
mode: '0600'
|
||||
|
||||
- name: 配置主库感知的仓库与每日备份任务(不提前启用)
|
||||
hosts: pg_repository
|
||||
become: true
|
||||
gather_facts: false
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
pg_backup_members:
|
||||
- {address: '192.168.10.127'}
|
||||
- {address: '10.60.0.20'}
|
||||
tasks:
|
||||
- name: 创建配置目录
|
||||
ansible.builtin.file:
|
||||
path: /etc/homelab-pgbackrest
|
||||
state: directory
|
||||
owner: root
|
||||
group: pgbackup
|
||||
mode: '0750'
|
||||
- name: 安装仓库配置
|
||||
ansible.builtin.template:
|
||||
src: roles/pg_instance/templates/repository.conf.j2
|
||||
dest: /etc/homelab-pgbackrest/repository.conf
|
||||
owner: root
|
||||
group: pgbackup
|
||||
mode: '0640'
|
||||
- name: 安装备份服务和 timer
|
||||
ansible.builtin.template:
|
||||
src: "roles/pg_instance/templates/backup.{{ item }}.j2"
|
||||
dest: "/etc/systemd/system/homelab-postgresql-backup.{{ item }}"
|
||||
mode: '0644'
|
||||
loop: [service, timer]
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
- name: 在已启动数据库上验证首备后启用调度
|
||||
hosts: pg_repository
|
||||
become: true
|
||||
become_user: pgbackup
|
||||
gather_facts: false
|
||||
vars:
|
||||
pg_backup_command: [pgbackrest, '--config=/etc/homelab-pgbackrest/repository.conf', '--stanza=prod']
|
||||
tasks:
|
||||
- name: 检查 stanza 元数据
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_repo_path }}/backup/prod/backup.info"
|
||||
register: pg_stanza
|
||||
- name: 首次初始化 stanza
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_backup_command + ['stanza-create'] }}"
|
||||
when: not pg_stanza.stat.exists
|
||||
changed_when: true
|
||||
- name: 验证真实 WAL 归档(会触发 WAL 切换)
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_backup_command + ['check'] }}"
|
||||
changed_when: false
|
||||
- name: 查询已有备份
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_backup_command + ['--output=json', 'info'] }}"
|
||||
register: pg_backup_info
|
||||
changed_when: false
|
||||
- name: 没有成功备份时创建首个 full
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_backup_command + ['--type=full', 'backup'] }}"
|
||||
when: (pg_backup_info.stdout | from_json)[0].backup | length == 0
|
||||
changed_when: true
|
||||
- name: 验证成功后才启用 timer
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-postgresql-backup.timer
|
||||
daemon_reload: true
|
||||
enabled: true
|
||||
state: started
|
||||
become_user: root
|
||||
become: true
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
- name: 读取已有凭据
|
||||
ansible.builtin.import_playbook: credentials.yml
|
||||
- name: 检查承载和所有权
|
||||
ansible.builtin.import_playbook: preflight.yml
|
||||
- name: 显式初始化新实例,laptop 先启动
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: 要求显式首次初始化开关
|
||||
ansible.builtin.assert:
|
||||
that: pg_allow_initialize | bool
|
||||
fail_msg: 仅首次初始化使用 -e pg_allow_initialize=true;恢复和升级不走此入口。
|
||||
- name: 初始化声明的实例
|
||||
ansible.builtin.include_tasks: tasks/bootstrap-instance.yml
|
||||
loop: "{{ pg_instances }}"
|
||||
loop_control:
|
||||
loop_var: pg_instance
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
- name: 安装每日证书续签控制任务
|
||||
hosts: pg-laptop
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 创建 root 管理的程序目录
|
||||
ansible.builtin.file:
|
||||
path: /opt/homelab-postgresql-controller
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: 安装短期身份包装器
|
||||
ansible.builtin.copy:
|
||||
src: ../run.py
|
||||
dest: /opt/homelab-postgresql-controller/run.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
- name: 安装固定 Ansible 配置副本
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/"
|
||||
dest: /opt/homelab-postgresql-controller/ansible/
|
||||
owner: root
|
||||
group: root
|
||||
mode: preserve
|
||||
- name: 使用 SPIFFE 执行一次续签预检(不输出 token)
|
||||
ansible.builtin.command:
|
||||
argv: [python3, /opt/homelab-postgresql-controller/run.py, --spiffe, ansible, renew-certificates.yml]
|
||||
environment:
|
||||
PG_BAO_SPIFFE_ROLE: homelab-postgresql
|
||||
PATH: /home/panxiao81/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
become: true
|
||||
become_user: panxiao81
|
||||
changed_when: false
|
||||
- name: 安装续签 service
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/homelab-postgresql-renew.service
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Renew shared PostgreSQL certificates using short-lived SPIFFE 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
|
||||
Environment=PG_BAO_SPIFFE_ROLE=homelab-postgresql
|
||||
ExecStart=/usr/bin/python3 /opt/homelab-postgresql-controller/run.py --spiffe ansible renew-certificates.yml
|
||||
TimeoutStartSec=14min
|
||||
UMask=0077
|
||||
- name: 安装每日 timer
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/homelab-postgresql-renew.timer
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Daily shared PostgreSQL certificate renewal
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:30:00 UTC
|
||||
RandomizedDelaySec=15min
|
||||
Persistent=true
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
- name: 预检通过后启用调度
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-postgresql-renew.timer
|
||||
daemon_reload: true
|
||||
enabled: true
|
||||
state: started
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
# 只读收敛入口;秘密首次创建、删除或轮换不放进 site.yml。
|
||||
- name: 在任何主机变更前读取并校验已有凭据
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 要求有效的显式 Bao 身份
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_bao_token | length > 0
|
||||
- pg_bao_url is match('^https://')
|
||||
no_log: true
|
||||
- name: 读取两个实例的管理凭据
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/{{ pg_secret_paths[item] }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
status_code: 200
|
||||
loop: [prod, dev]
|
||||
register: pg_secret_response
|
||||
no_log: true
|
||||
- name: 整理并验证凭据结构
|
||||
ansible.builtin.set_fact:
|
||||
pg_loaded_credentials: "{{ pg_loaded_credentials | default({}) | combine({item.item: item.json.data.data}) }}"
|
||||
loop: "{{ pg_secret_response.results }}"
|
||||
no_log: true
|
||||
- name: 拒绝缺失或短密码
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_loaded_credentials[item].superuser_password | length >= 32
|
||||
- pg_loaded_credentials[item].ayatori_password | length >= 32
|
||||
- pg_loaded_credentials[item].ayatori_username == 'ayatori'
|
||||
loop: [prod, dev]
|
||||
no_log: true
|
||||
- name: 校验生产复制与 API 凭据
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_loaded_credentials.prod.replication_password | length >= 32
|
||||
- pg_loaded_credentials.prod.rest_password | length >= 32
|
||||
no_log: true
|
||||
- name: 读取已有 etcd 消费者凭据
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/{{ pg_etcd_secret_path }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
status_code: 200
|
||||
register: pg_etcd_response
|
||||
no_log: true
|
||||
- name: 校验 etcd prefix 并交付内存引用
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_etcd_response.json.data.data.username == 'patroni-pg-prod'
|
||||
- pg_etcd_response.json.data.data.prefix == '/homelab/patroni/pg-prod/'
|
||||
no_log: true
|
||||
- name: 保存短生命周期内存引用
|
||||
ansible.builtin.set_fact:
|
||||
pg_loaded_etcd: "{{ pg_etcd_response.json.data.data }}"
|
||||
no_log: true
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# 首次创建专用密码;已存在的数据与丢失秘密不能组合为自动重置。
|
||||
- name: 检查已有实例数据
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 检查各实例 PG_VERSION
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_data_root }}/{{ item }}/data/PG_VERSION"
|
||||
loop: "{{ pg_instances }}"
|
||||
register: pg_secret_data_files
|
||||
- name: 记录现有数据边界
|
||||
ansible.builtin.set_fact:
|
||||
pg_has_data: "{{ pg_secret_data_files.results | selectattr('stat.exists') | list | length > 0 }}"
|
||||
- name: 首次将随机实例凭据写入 Bao
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 要求显式初始化与管理身份
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_allow_initialize | bool
|
||||
- pg_bao_token | length > 0
|
||||
no_log: true
|
||||
- name: 逐实例确认元数据和 CAS 创建
|
||||
ansible.builtin.include_tasks: tasks/initialize-secret.yml
|
||||
loop: [prod, dev]
|
||||
loop_control:
|
||||
loop_var: pg_secret_instance
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
pg_major: 18
|
||||
pg_patroni_version: 4.1.5
|
||||
pg_bao_url: https://bao.ad.ddupan.top:8200
|
||||
pg_bao_token: "{{ lookup('env', 'BAO_TOKEN') }}"
|
||||
pg_config_root: /etc/homelab-postgresql
|
||||
pg_data_root: /var/lib/homelab-postgresql
|
||||
pg_bin_dir: /usr/lib/postgresql/18/bin
|
||||
pg_patroni_bin: /opt/homelab-patroni/bin/patroni
|
||||
pg_allow_initialize: false
|
||||
# 仅存放 Bao 路径,值必须在控制端 no_log 读取;不从失效读请求推断秘密不存在。
|
||||
pg_secret_paths:
|
||||
prod: infra/postgresql/prod
|
||||
dev: infra/postgresql/dev
|
||||
pg_etcd_secret_path: infra/etcd/consumers/patroni-pg-prod
|
||||
pg_etcd_endpoints: ['192.168.10.127:2379', '10.60.0.20:2379', '10.60.0.21:2379']
|
||||
pg_profiles:
|
||||
prod:
|
||||
user: pgprod
|
||||
port: 5432
|
||||
api_port: 8008
|
||||
dns: pg-prod.ad.ddupan.top
|
||||
memory_high: 768M
|
||||
memory_max: 1G
|
||||
shared_buffers: 128MB
|
||||
max_connections: 80
|
||||
dev:
|
||||
user: pgdev
|
||||
port: 5433
|
||||
dns: pg-dev.ad.ddupan.top
|
||||
memory_high: 256M
|
||||
memory_max: 384M
|
||||
shared_buffers: 32MB
|
||||
max_connections: 30
|
||||
# 客户端网络范围;HBA 仍需独立的 TLS + SCRAM 身份认证。
|
||||
pg_client_cidrs: ['192.168.10.0/24', '10.60.0.0/24', '10.42.0.0/16']
|
||||
pg_replication_addresses: ['192.168.10.127/32', '10.60.0.20/32']
|
||||
pg_repo_host: 10.60.0.21
|
||||
pg_repo_user: pgbackup
|
||||
pg_repo_path: /var/lib/homelab-pgbackrest
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
all:
|
||||
vars:
|
||||
ansible_user: root
|
||||
children:
|
||||
pg_hosts:
|
||||
hosts:
|
||||
pg-laptop:
|
||||
ansible_connection: local
|
||||
ansible_user: panxiao81
|
||||
ansible_host: 192.168.10.127
|
||||
pg_address: 192.168.10.127
|
||||
pg_instances: [prod, dev]
|
||||
pg-pve1:
|
||||
ansible_host: 10.60.0.20
|
||||
pg_address: 10.60.0.20
|
||||
pg_instances: [prod]
|
||||
pg_repository:
|
||||
hosts:
|
||||
pg-pve2:
|
||||
ansible_host: 10.60.0.21
|
||||
pg_pve:
|
||||
hosts:
|
||||
pve1:
|
||||
ansible_host: 192.168.10.4
|
||||
pg_lxc_id: 150
|
||||
pg_lxc_hostname: etcd-pve1
|
||||
pg_lxc_memory: 1536
|
||||
pg_lxc_mount: /var/lib/homelab-postgresql/prod
|
||||
pg_lxc_disk_gb: 16
|
||||
pve2:
|
||||
ansible_host: 192.168.10.7
|
||||
pg_lxc_id: 151
|
||||
pg_lxc_hostname: etcd-pve2
|
||||
pg_lxc_memory: 768
|
||||
pg_lxc_mount: /var/lib/homelab-pgbackrest
|
||||
pg_lxc_disk_gb: 32
|
||||
pg_proxy:
|
||||
hosts:
|
||||
pg-proxy-vyos:
|
||||
ansible_host: 192.168.10.2
|
||||
ansible_user: vyos
|
||||
ansible_connection: ssh
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# 新 HDD 卷初始同步曾伴随共享链路 PingAck 超时;仅约束本项目 mp0,不改全局 quorum/协议。
|
||||
- name: 限制本项目新 HDD 卷的后台同步速率
|
||||
hosts: pg_pve
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
tasks:
|
||||
- name: 收敛本项目数据卷同步限额
|
||||
ansible.builtin.include_tasks: tasks/limit-resync.yml
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
- name: 检查承载前提
|
||||
ansible.builtin.import_playbook: preflight.yml
|
||||
- name: 准备数据库软件,不配置或初始化实例
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
serial: 1
|
||||
any_errors_fatal: true
|
||||
roles:
|
||||
- pg_packages
|
||||
post_tasks:
|
||||
- name: 核对包安装没有自动创建 PG18 默认实例
|
||||
ansible.builtin.stat:
|
||||
path: /var/lib/postgresql/18/main/PG_VERSION
|
||||
register: pg_package_default_cluster
|
||||
- name: 默认实例应保持未初始化
|
||||
ansible.builtin.assert:
|
||||
that: not pg_package_default_cluster.stat.exists
|
||||
fail_msg: 发现默认 PG18 main 实例,停止后续初始化;本任务不会删除已有数据。
|
||||
- name: 验证独立 Patroni 环境可运行
|
||||
ansible.builtin.command:
|
||||
argv: ["{{ pg_patroni_bin }}", --version]
|
||||
changed_when: false
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: 只读核对数据库宿主与数据挂载
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
tasks:
|
||||
- name: 限定已验证的平台
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == 'Ubuntu'
|
||||
- ansible_facts.distribution_version == '24.04'
|
||||
- ansible_facts.architecture == 'x86_64'
|
||||
- pg_instances | difference(['prod', 'dev']) | length == 0
|
||||
- name: 核对共置内存预算
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.memtotal_mb >= (2048 if 'dev' in pg_instances else 1536)
|
||||
fail_msg: PG 与 etcd 共置前需通过承载 IaC 扩容;不能以当前 512 MiB LXC 直接上线。
|
||||
- name: 核对每个数据父目录已单独挂载
|
||||
ansible.builtin.command:
|
||||
argv: [findmnt, --mountpoint, "{{ pg_data_root }}/{{ item }}", --noheadings, --output, 'TARGET,FSTYPE,SOURCE']
|
||||
loop: "{{ pg_instances }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 核对已有数据所有权
|
||||
ansible.builtin.include_tasks: tasks/preflight-instance.yml
|
||||
loop: "{{ pg_instances }}"
|
||||
loop_control:
|
||||
loop_var: pg_instance
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
# 单独 HAProxy 实例复用 VyOS 的二进制与 HA 承载,避免修改发行版生成的全局模板。
|
||||
- name: 发布已经有唯一可写主库的生产入口
|
||||
hosts: pg_proxy
|
||||
become: true
|
||||
gather_facts: false
|
||||
vars:
|
||||
pg_proxy_bind: 192.168.10.2:5432
|
||||
pg_proxy_members:
|
||||
- {name: laptop, address: '192.168.10.127', port: 5432, api_port: 8008}
|
||||
- {name: pve1, address: '10.60.0.20', port: 5432, api_port: 8008}
|
||||
tasks:
|
||||
- name: 前置核对角色端点,不为未就绪实例发布入口
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ item.address }}:{{ item.api_port }}/primary"
|
||||
status_code: [200, 503]
|
||||
loop: "{{ pg_proxy_members }}"
|
||||
register: pg_proxy_primary
|
||||
check_mode: false
|
||||
- name: 必须有且只有一个 primary
|
||||
ansible.builtin.assert:
|
||||
that: pg_proxy_primary.results | selectattr('status', 'equalto', 200) | list | length == 1
|
||||
- name: 创建跨 VyOS 镜像升级保留的配置目录
|
||||
ansible.builtin.file:
|
||||
path: /config/homelab-pg
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
- name: 写入并校验角色感知 HAProxy 配置
|
||||
ansible.builtin.template:
|
||||
src: roles/pg_instance/templates/haproxy.cfg.j2
|
||||
dest: /config/homelab-pg/haproxy.cfg
|
||||
mode: '0644'
|
||||
validate: /usr/sbin/haproxy -c -f %s
|
||||
notify: 重载数据库代理
|
||||
- name: 保存独立服务定义
|
||||
ansible.builtin.copy:
|
||||
dest: /config/homelab-pg/homelab-pg-proxy.service
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Shared PostgreSQL role-aware TCP entry point
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
DynamicUser=true
|
||||
ExecStart=/usr/sbin/haproxy -W -db -f /config/homelab-pg/haproxy.cfg
|
||||
ExecReload=/bin/kill -USR2 $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
MemoryMax=64M
|
||||
LimitNOFILE=2048
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
notify: 重载数据库代理
|
||||
- name: 安装服务链接
|
||||
ansible.builtin.file:
|
||||
src: /config/homelab-pg/homelab-pg-proxy.service
|
||||
dest: /etc/systemd/system/homelab-pg-proxy.service
|
||||
state: link
|
||||
- name: 通过既有 postconfig hook 在系统升级后恢复服务链接
|
||||
ansible.builtin.blockinfile:
|
||||
path: /config/scripts/vyos-postconfig-bootup.script
|
||||
marker: '# {mark} ANSIBLE HOMELAB PG PROXY'
|
||||
insertbefore: '^exit 0'
|
||||
block: |
|
||||
ln -sfn /config/homelab-pg/homelab-pg-proxy.service /etc/systemd/system/homelab-pg-proxy.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now homelab-pg-proxy.service
|
||||
- name: 启动独立入口
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-pg-proxy
|
||||
daemon_reload: true
|
||||
enabled: true
|
||||
state: started
|
||||
handlers:
|
||||
- name: 重载数据库代理
|
||||
ansible.builtin.systemd_service:
|
||||
name: homelab-pg-proxy
|
||||
daemon_reload: true
|
||||
state: reloaded
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
# 仅扩展已经受管的两个 shared-etcd LXC;不移动/删除已有 rootfs 或数据卷。
|
||||
- name: 串行准备 standby 与备份仓库的独立 HDD 卷
|
||||
hosts: pg_pve
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
pg_etcd_health:
|
||||
- /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, "{{ pg_lxc_id }}"]
|
||||
register: pg_lxc_config
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 拒绝未知容器或已有冲突挂载
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_lxc_id in [150, 151]
|
||||
- "'shared-etcd' in pg_lxc_config.stdout"
|
||||
- "('hostname: ' ~ pg_lxc_hostname) in pg_lxc_config.stdout"
|
||||
- >-
|
||||
'mp0:' not in pg_lxc_config.stdout or
|
||||
('mp=' ~ pg_lxc_mount ~ ',') in (pg_lxc_config.stdout ~ ',')
|
||||
- "'mp0:' not in pg_lxc_config.stdout or 'mp0: pve-rg-hdd:' in pg_lxc_config.stdout"
|
||||
- name: 验证维护前全部 etcd 成员健康
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_etcd_health }}"
|
||||
delegate_to: localhost
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 首次添加独立卷
|
||||
when: "'mp0:' not in pg_lxc_config.stdout and not ansible_check_mode"
|
||||
block:
|
||||
- name: 正常停止当前单个成员
|
||||
ansible.builtin.command:
|
||||
argv: [pct, shutdown, "{{ pg_lxc_id }}", --timeout, '60']
|
||||
changed_when: true
|
||||
- name: 从指定 HDD 池创建新的 mp0
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- pct
|
||||
- set
|
||||
- "{{ pg_lxc_id }}"
|
||||
- --mp0
|
||||
- "pve-rg-hdd:{{ pg_lxc_disk_gb }},mp={{ pg_lxc_mount }},backup=1"
|
||||
changed_when: true
|
||||
- name: 容器启动前限制新卷的初始同步速率
|
||||
ansible.builtin.include_tasks: tasks/limit-resync.yml
|
||||
always:
|
||||
- name: 查询当前状态
|
||||
ansible.builtin.command:
|
||||
argv: [pct, status, "{{ pg_lxc_id }}"]
|
||||
register: pg_lxc_status
|
||||
changed_when: false
|
||||
- name: 恢复容器运行
|
||||
ansible.builtin.command:
|
||||
argv: [pct, start, "{{ pg_lxc_id }}"]
|
||||
changed_when: true
|
||||
when: "'running' not in pg_lxc_status.stdout"
|
||||
- name: 仅上调不足的内存限额,不缩容既有预算
|
||||
ansible.builtin.command:
|
||||
argv: [pct, set, "{{ pg_lxc_id }}", --memory, "{{ pg_lxc_memory }}"]
|
||||
when: >-
|
||||
((pg_lxc_config.stdout | regex_findall('(?m)^memory: ([0-9]+)$')) | first | int) < pg_lxc_memory
|
||||
changed_when: true
|
||||
- name: 确认当前成员恢复再处理下一台
|
||||
ansible.builtin.command:
|
||||
argv: "{{ pg_etcd_health }}"
|
||||
delegate_to: localhost
|
||||
changed_when: false
|
||||
register: pg_etcd_after
|
||||
retries: 18
|
||||
delay: 5
|
||||
until: pg_etcd_after.rc == 0
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# 只续签和重载证书,不安装包、不变更数据库参数或重启服务。
|
||||
- name: 串行检查实例证书续签窗口
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
any_errors_fatal: true
|
||||
tasks:
|
||||
- name: 逐实例续签
|
||||
ansible.builtin.include_tasks: tasks/renew-instance.yml
|
||||
loop: "{{ pg_instances }}"
|
||||
loop_control:
|
||||
loop_var: pg_instance
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.crypto
|
||||
version: 3.2.1
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
- name: 本地生成私钥
|
||||
community.crypto.openssl_privatekey:
|
||||
path: "{{ pg_config_dir }}/{{ pg_cert_name }}.key"
|
||||
type: ECC
|
||||
curve: secp256r1
|
||||
owner: root
|
||||
group: "{{ pg_profile.user }}"
|
||||
mode: '0640'
|
||||
- name: 本地生成 CSR
|
||||
community.crypto.openssl_csr:
|
||||
path: "{{ pg_config_dir }}/{{ pg_cert_name }}.csr"
|
||||
privatekey_path: "{{ pg_config_dir }}/{{ pg_cert_name }}.key"
|
||||
common_name: "{{ pg_cert_cn | default(omit, true) }}"
|
||||
use_common_name_for_san: false
|
||||
subject_alt_name: >-
|
||||
{{ ['DNS:' ~ pg_profile.dns, 'IP:' ~ pg_address] if pg_cert_name == 'server'
|
||||
else ['DNS:etcd-patroni-pg-prod'] }}
|
||||
extended_key_usage: "{{ pg_cert_eku }}"
|
||||
key_usage: [digitalSignature]
|
||||
mode: '0644'
|
||||
register: pg_csr
|
||||
- name: 检查证书文件
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_config_dir }}/{{ pg_cert_name }}.crt"
|
||||
register: pg_cert_file
|
||||
- name: 检查续签窗口
|
||||
community.crypto.x509_certificate_info:
|
||||
path: "{{ pg_config_dir }}/{{ pg_cert_name }}.crt"
|
||||
valid_at: {renewal: '+14d'}
|
||||
register: pg_cert_info
|
||||
when: pg_cert_file.stat.exists
|
||||
- name: 需要时请求中央 CA 签发
|
||||
when: not pg_cert_file.stat.exists or pg_csr is changed or not (pg_cert_info.valid_at.renewal | default(false))
|
||||
block:
|
||||
- name: 读取公开 CSR
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ pg_config_dir }}/{{ pg_cert_name }}.csr"
|
||||
register: pg_csr_content
|
||||
- name: 请求签发(凭据仅在控制端)
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/pki/sign/{{ pg_cert_role }}"
|
||||
method: POST
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
csr: "{{ pg_csr_content.content | b64decode }}"
|
||||
ttl: 1440h
|
||||
status_code: 200
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
register: pg_signed
|
||||
no_log: true
|
||||
- name: 保存证书和信任链
|
||||
ansible.builtin.copy:
|
||||
content: "{{ item.content }}"
|
||||
dest: "{{ pg_config_dir }}/{{ item.name }}"
|
||||
owner: root
|
||||
group: "{{ pg_profile.user }}"
|
||||
mode: '0644'
|
||||
loop:
|
||||
- name: "{{ pg_cert_name }}.crt"
|
||||
content: "{{ pg_signed.json.data.certificate }}\n{{ pg_signed.json.data.ca_chain | join('\n') }}\n"
|
||||
- name: ca.crt
|
||||
content: "{{ pg_signed.json.data.ca_chain | join('\n') }}\n"
|
||||
- name: 记录本轮已签发证书以供显式续签入口重载
|
||||
ansible.builtin.set_fact:
|
||||
pg_certificates_changed: true
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
- name: 派生实例专用路径
|
||||
ansible.builtin.set_fact:
|
||||
pg_profile: "{{ pg_profiles[pg_instance] }}"
|
||||
pg_config_dir: "{{ pg_config_root }}/{{ pg_instance }}"
|
||||
pg_parent_dir: "{{ pg_data_root }}/{{ pg_instance }}"
|
||||
pg_data_dir: "{{ pg_data_root }}/{{ pg_instance }}/data"
|
||||
pg_socket_dir: "/run/homelab-postgresql-{{ pg_instance }}"
|
||||
pg_member: "{{ inventory_hostname }}"
|
||||
pg_credentials: "{{ hostvars['localhost'].pg_loaded_credentials[pg_instance] }}"
|
||||
pg_etcd_credentials: "{{ hostvars['localhost'].pg_loaded_etcd }}"
|
||||
no_log: true
|
||||
- name: 创建实例操作系统组
|
||||
ansible.builtin.group:
|
||||
name: "{{ pg_profile.user }}"
|
||||
system: true
|
||||
- name: 创建实例操作系统账号
|
||||
ansible.builtin.user:
|
||||
name: "{{ pg_profile.user }}"
|
||||
group: "{{ pg_profile.user }}"
|
||||
home: "{{ pg_parent_dir }}"
|
||||
create_home: false
|
||||
shell: /bin/bash
|
||||
system: true
|
||||
- name: 创建隔离目录
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ pg_profile.user }}"
|
||||
group: "{{ pg_profile.user }}"
|
||||
mode: '0700'
|
||||
loop:
|
||||
- "{{ pg_parent_dir }}"
|
||||
- "{{ pg_data_dir }}"
|
||||
- "{{ pg_socket_dir }}"
|
||||
- "{{ pg_parent_dir }}/backup-lock"
|
||||
- "{{ pg_parent_dir }}/backup-spool"
|
||||
- name: 保持共享配置父目录可遍历,实例目录各自受限
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_config_root }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
- name: 创建 root 管理的配置目录
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_config_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: "{{ pg_profile.user }}"
|
||||
mode: '0750'
|
||||
- name: 签发实例服务器证书
|
||||
ansible.builtin.include_tasks: certificate.yml
|
||||
vars:
|
||||
pg_cert_name: server
|
||||
pg_cert_cn: "{{ pg_profile.dns }}"
|
||||
pg_cert_role: "homelab-pg-{{ pg_instance }}"
|
||||
pg_cert_eku: [serverAuth]
|
||||
- name: 签发 Patroni 独立无 CN 客户端证书
|
||||
ansible.builtin.include_tasks: certificate.yml
|
||||
vars:
|
||||
pg_cert_name: etcd
|
||||
pg_cert_cn: ''
|
||||
pg_cert_role: homelab-etcd-client
|
||||
pg_cert_eku: [clientAuth]
|
||||
when: pg_instance == 'prod'
|
||||
- name: 写入实例配置
|
||||
ansible.builtin.template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "{{ pg_config_dir }}/{{ item }}"
|
||||
owner: root
|
||||
group: "{{ pg_profile.user }}"
|
||||
mode: '0640'
|
||||
loop: "{{ ['patroni.yml', 'pgbackrest.conf'] if pg_instance == 'prod' else ['postgresql.conf', 'pg_hba.conf'] }}"
|
||||
no_log: true
|
||||
- name: 安装实例专用服务(不自动启动或重启)
|
||||
ansible.builtin.template:
|
||||
src: instance.service.j2
|
||||
dest: "/etc/systemd/system/homelab-postgresql-{{ pg_instance }}.service"
|
||||
mode: '0644'
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Shared PostgreSQL primary-aware full backup
|
||||
After=network-online.target
|
||||
RequiresMountsFor={{ pg_repo_path }}
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=pgbackup
|
||||
Group=pgbackup
|
||||
ExecStart=/usr/bin/pgbackrest --config=/etc/homelab-pgbackrest/repository.conf --stanza=prod --type=full backup
|
||||
TimeoutStartSec=2h
|
||||
UMask=0077
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Daily shared PostgreSQL full backup
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:40:00 UTC
|
||||
RandomizedDelaySec=10min
|
||||
Persistent=true
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,19 @@
|
||||
# 参考 Pigsty v4.5.0 的 Patroni 角色检查与旧连接摘除行为。
|
||||
global
|
||||
maxconn 256
|
||||
log stdout format raw local0
|
||||
stats socket /tmp/homelab-pg-haproxy.sock mode 600 level admin
|
||||
defaults
|
||||
mode tcp
|
||||
timeout connect 3s
|
||||
timeout client 1h
|
||||
timeout server 1h
|
||||
timeout check 3s
|
||||
listen pg-prod
|
||||
bind {{ pg_proxy_bind }}
|
||||
option httpchk GET /primary
|
||||
http-check expect status 200
|
||||
default-server inter 2s fall 3 rise 2 on-marked-down shutdown-sessions
|
||||
{% for member in pg_proxy_members %}
|
||||
server {{ member.name }} {{ member.address }}:{{ member.port }} check port {{ member.api_port }}
|
||||
{% endfor %}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=Homelab PostgreSQL {{ pg_instance }}{{ ' through Patroni' if pg_instance == 'prod' else '' }}
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
RequiresMountsFor={{ pg_parent_dir }}
|
||||
[Service]
|
||||
User={{ pg_profile.user }}
|
||||
Group={{ pg_profile.user }}
|
||||
UMask=0077
|
||||
RuntimeDirectory=homelab-postgresql-{{ pg_instance }}
|
||||
RuntimeDirectoryMode=0700
|
||||
{% if pg_instance == 'prod' %}
|
||||
Type=simple
|
||||
ExecStart={{ pg_patroni_bin }} {{ pg_config_dir }}/patroni.yml
|
||||
KillMode=process
|
||||
{% else %}
|
||||
Type=simple
|
||||
ExecStart={{ pg_bin_dir }}/postgres -D {{ pg_data_dir }} -c config_file={{ pg_config_dir }}/postgresql.conf
|
||||
KillSignal=SIGINT
|
||||
{% endif %}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=120
|
||||
MemoryHigh={{ pg_profile.memory_high }}
|
||||
MemoryMax={{ pg_profile.memory_max }}
|
||||
OOMPolicy=stop
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
ReadWritePaths={{ pg_parent_dir }} {{ pg_config_dir }} {{ pg_socket_dir }}
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,99 @@
|
||||
# 实例化部署;不引用 Pigsty 的全局 /pg 或宿主内存自动调优。
|
||||
name: {{ pg_member | to_json }}
|
||||
namespace: /homelab/patroni/
|
||||
scope: pg-prod
|
||||
restapi:
|
||||
listen: {{ pg_address }}:{{ pg_profile.api_port }}
|
||||
connect_address: {{ pg_address }}:{{ pg_profile.api_port }}
|
||||
authentication:
|
||||
username: patroni
|
||||
password: {{ pg_credentials.rest_password | to_json }}
|
||||
etcd3:
|
||||
hosts: {{ pg_etcd_endpoints | to_json }}
|
||||
protocol: https
|
||||
username: {{ pg_etcd_credentials.username | to_json }}
|
||||
password: {{ pg_etcd_credentials.password | to_json }}
|
||||
cacert: {{ pg_config_dir }}/ca.crt
|
||||
cert: {{ pg_config_dir }}/etcd.crt
|
||||
key: {{ pg_config_dir }}/etcd.key
|
||||
bootstrap:
|
||||
dcs:
|
||||
ttl: 30
|
||||
loop_wait: 10
|
||||
retry_timeout: 10
|
||||
maximum_lag_on_failover: 1048576
|
||||
check_timeline: true
|
||||
synchronous_mode: false
|
||||
failsafe_mode: false
|
||||
postgresql:
|
||||
use_pg_rewind: true
|
||||
use_slots: true
|
||||
parameters:
|
||||
max_connections: {{ pg_profile.max_connections }}
|
||||
wal_level: replica
|
||||
hot_standby: 'on'
|
||||
wal_log_hints: 'on'
|
||||
max_wal_senders: 5
|
||||
max_replication_slots: 5
|
||||
max_slot_wal_keep_size: 2GB
|
||||
max_wal_size: 1GB
|
||||
min_wal_size: 80MB
|
||||
archive_mode: 'on'
|
||||
archive_timeout: 300s
|
||||
archive_command: "pgbackrest --config={{ pg_config_dir }}/pgbackrest.conf --stanza=prod archive-push %p"
|
||||
initdb:
|
||||
- encoding: UTF8
|
||||
- locale: C.UTF-8
|
||||
- data-checksums
|
||||
postgresql:
|
||||
listen: {{ pg_address }}:{{ pg_profile.port }}
|
||||
connect_address: {{ pg_address }}:{{ pg_profile.port }}
|
||||
data_dir: {{ pg_data_dir }}
|
||||
bin_dir: {{ pg_bin_dir }}
|
||||
pgpass: {{ pg_parent_dir }}/pgpass
|
||||
use_unix_socket: true
|
||||
use_unix_socket_repl: true
|
||||
authentication:
|
||||
superuser:
|
||||
username: {{ pg_profile.user }}
|
||||
password: {{ pg_credentials.superuser_password | to_json }}
|
||||
replication:
|
||||
username: replicator
|
||||
password: {{ pg_credentials.replication_password | to_json }}
|
||||
sslmode: verify-full
|
||||
sslrootcert: {{ pg_config_dir }}/ca.crt
|
||||
parameters:
|
||||
unix_socket_directories: {{ pg_socket_dir }}
|
||||
unix_socket_permissions: '0700'
|
||||
password_encryption: scram-sha-256
|
||||
shared_buffers: {{ pg_profile.shared_buffers }}
|
||||
work_mem: 4MB
|
||||
maintenance_work_mem: 64MB
|
||||
ssl: 'on'
|
||||
ssl_cert_file: {{ pg_config_dir }}/server.crt
|
||||
ssl_key_file: {{ pg_config_dir }}/server.key
|
||||
ssl_ca_file: {{ pg_config_dir }}/ca.crt
|
||||
log_statement: none
|
||||
log_min_error_statement: panic
|
||||
pg_hba:
|
||||
- local all {{ pg_profile.user }} peer
|
||||
- local replication {{ pg_profile.user }} peer
|
||||
- local replication replicator scram-sha-256
|
||||
{% for address in pg_replication_addresses %}
|
||||
- hostssl replication replicator {{ address }} scram-sha-256
|
||||
- hostssl all {{ pg_profile.user }} {{ address }} scram-sha-256
|
||||
{% endfor %}
|
||||
{% for cidr in pg_client_cidrs %}
|
||||
- hostssl all all {{ cidr }} scram-sha-256
|
||||
{% endfor %}
|
||||
- host all all 0.0.0.0/0 reject
|
||||
- host all all ::/0 reject
|
||||
remove_data_directory_on_rewind_failure: false
|
||||
remove_data_directory_on_diverged_timelines: false
|
||||
watchdog:
|
||||
mode: 'off'
|
||||
tags:
|
||||
nofailover: false
|
||||
noloadbalance: false
|
||||
clonefrom: false
|
||||
nosync: false
|
||||
@@ -0,0 +1,6 @@
|
||||
local all {{ pg_profile.user }} peer
|
||||
{% for cidr in pg_client_cidrs %}
|
||||
hostssl all all {{ cidr }} scram-sha-256
|
||||
{% endfor %}
|
||||
host all all 0.0.0.0/0 reject
|
||||
host all all ::/0 reject
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
[global]
|
||||
{% if pg_repo_remote | default(true) %}
|
||||
repo1-host={{ pg_repo_host }}
|
||||
repo1-host-user={{ pg_repo_user }}
|
||||
repo1-host-config=/etc/homelab-pgbackrest/repository.conf
|
||||
{% endif %}
|
||||
repo1-path={{ pg_repo_path }}
|
||||
repo1-retention-full=3
|
||||
process-max=1
|
||||
compress-type=zst
|
||||
compress-level=3
|
||||
start-fast=y
|
||||
log-level-console=warn
|
||||
log-level-file=off
|
||||
lock-path={{ pg_parent_dir }}/backup-lock
|
||||
spool-path={{ pg_parent_dir }}/backup-spool
|
||||
# 不设置 archive-push-queue-max;超限丢弃 WAL 会破坏恢复链。
|
||||
[prod]
|
||||
pg1-path={{ pg_data_dir }}
|
||||
pg1-port={{ pg_profile.port }}
|
||||
pg1-socket-path={{ pg_socket_dir }}
|
||||
pg1-user={{ pg_profile.user }}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
listen_addresses = '{{ pg_address }}'
|
||||
port = {{ pg_profile.port }}
|
||||
unix_socket_directories = '{{ pg_socket_dir }}'
|
||||
unix_socket_permissions = 0700
|
||||
hba_file = '{{ pg_config_dir }}/pg_hba.conf'
|
||||
password_encryption = 'scram-sha-256'
|
||||
shared_buffers = '{{ pg_profile.shared_buffers }}'
|
||||
max_connections = {{ pg_profile.max_connections }}
|
||||
work_mem = '4MB'
|
||||
maintenance_work_mem = '32MB'
|
||||
max_wal_size = '512MB'
|
||||
ssl = on
|
||||
ssl_cert_file = '{{ pg_config_dir }}/server.crt'
|
||||
ssl_key_file = '{{ pg_config_dir }}/server.key'
|
||||
ssl_ca_file = '{{ pg_config_dir }}/ca.crt'
|
||||
archive_mode = off
|
||||
log_statement = none
|
||||
log_min_error_statement = panic
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# 备份从仓库发起,发现实际 primary,避免切换后定时器仍依赖旧主。
|
||||
[global]
|
||||
repo1-path={{ pg_repo_path }}
|
||||
repo1-retention-full=3
|
||||
process-max=1
|
||||
compress-type=zst
|
||||
compress-level=3
|
||||
start-fast=y
|
||||
log-level-console=info
|
||||
log-level-file=off
|
||||
lock-path={{ pg_repo_path }}/lock
|
||||
[prod]
|
||||
{% for member in pg_backup_members %}
|
||||
pg{{ loop.index }}-host={{ member.address }}
|
||||
pg{{ loop.index }}-host-user=pgprod
|
||||
pg{{ loop.index }}-host-config=/etc/homelab-postgresql/prod/pgbackrest.conf
|
||||
pg{{ loop.index }}-path=/var/lib/homelab-postgresql/prod/data
|
||||
pg{{ loop.index }}-port=5432
|
||||
pg{{ loop.index }}-socket-path=/run/homelab-postgresql-prod
|
||||
pg{{ loop.index }}-user=pgprod
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
- name: 配置官方软件源
|
||||
ansible.builtin.import_tasks: repository.yml
|
||||
- name: 安装集群管理基础包(不安装默认主版本元包)
|
||||
ansible.builtin.apt:
|
||||
name: postgresql-common
|
||||
state: present
|
||||
- name: 查询已安装包以避免重复制造临时配置变更
|
||||
ansible.builtin.package_facts:
|
||||
manager: apt
|
||||
- name: 临时禁止包安装自动建立默认 main 实例
|
||||
when: >-
|
||||
['postgresql-18', 'postgresql-client-18', 'pgbackrest']
|
||||
| difference(ansible_facts.packages.keys() | list) | length > 0
|
||||
block:
|
||||
- name: 确保包配置片段目录存在
|
||||
ansible.builtin.file:
|
||||
path: /etc/postgresql-common/createcluster.d
|
||||
state: directory
|
||||
mode: '0755'
|
||||
- name: 禁止本次 apt postinst 自动初始化
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/postgresql-common/createcluster.d/90-homelab-no-auto.conf
|
||||
content: "create_main_cluster = false\n"
|
||||
mode: '0644'
|
||||
- name: 安装明确的 PG 主版本与备份工具
|
||||
ansible.builtin.apt:
|
||||
name: ['postgresql-18', 'postgresql-client-18', pgbackrest]
|
||||
state: present
|
||||
install_recommends: false
|
||||
always:
|
||||
- name: 撤回临时包安装选项
|
||||
ansible.builtin.file:
|
||||
path: /etc/postgresql-common/createcluster.d/90-homelab-no-auto.conf
|
||||
state: absent
|
||||
- name: 在独立 venv 安装固定 Patroni 版本
|
||||
ansible.builtin.pip:
|
||||
name: 'patroni[etcd3,psycopg3]=={{ pg_patroni_version }}'
|
||||
virtualenv: /opt/homelab-patroni
|
||||
virtualenv_command: python3 -m venv
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
# 共用宿主机上其他第三方源故障不能阻塞本项目;仅刷新 Ubuntu/PGDG,仍强制签名验证。
|
||||
- name: 仅刷新 Ubuntu 官方基础软件源 # noqa: command-instead-of-module
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- apt-get
|
||||
- -o
|
||||
- Dir::Etc::sourcelist=/etc/apt/sources.list.d/ubuntu.sources
|
||||
- -o
|
||||
- Dir::Etc::sourceparts=-
|
||||
- -o
|
||||
- APT::Get::List-Cleanup=0
|
||||
- -o
|
||||
- APT::Update::Error-Mode=any
|
||||
- update
|
||||
changed_when: false
|
||||
- name: 安装软件源及 Python 依赖
|
||||
ansible.builtin.apt:
|
||||
name: [ca-certificates, python3-debian, python3-venv, python3-cryptography, acl]
|
||||
state: present
|
||||
- name: 安装官方 PGDG 公钥
|
||||
ansible.builtin.get_url:
|
||||
url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
dest: /usr/share/keyrings/homelab-pgdg.asc
|
||||
mode: '0644'
|
||||
register: pg_repo_key
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: pg_repo_key is succeeded
|
||||
- name: 声明 PGDG 官方软件源
|
||||
ansible.builtin.deb822_repository:
|
||||
name: homelab-pgdg
|
||||
uris: https://apt.postgresql.org/pub/repos/apt
|
||||
suites: noble-pgdg
|
||||
components: main
|
||||
architectures: amd64
|
||||
signed_by: /usr/share/keyrings/homelab-pgdg.asc
|
||||
- name: 仅刷新本项目 PGDG 软件源 # noqa: command-instead-of-module
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- apt-get
|
||||
- -o
|
||||
- Dir::Etc::sourcelist=/etc/apt/sources.list.d/homelab-pgdg.sources
|
||||
- -o
|
||||
- Dir::Etc::sourceparts=-
|
||||
- -o
|
||||
- APT::Get::List-Cleanup=0
|
||||
- -o
|
||||
- APT::Update::Error-Mode=any
|
||||
- update
|
||||
changed_when: false
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
- name: 检查承载前提
|
||||
ansible.builtin.import_playbook: preflight.yml
|
||||
- name: 验证秘密可用
|
||||
ansible.builtin.import_playbook: credentials.yml
|
||||
- name: 配置数据库实例(不初始化、不启动、不自动重启)
|
||||
hosts: pg_hosts
|
||||
become: true
|
||||
roles:
|
||||
- pg_packages
|
||||
tasks:
|
||||
- name: 配置声明的实例
|
||||
ansible.builtin.include_role:
|
||||
name: pg_instance
|
||||
loop: "{{ pg_instances }}"
|
||||
loop_control:
|
||||
loop_var: pg_instance
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# 只创建专属 dataset;PVE 新挂载需串行重启 etcd 成员,单独处理,不混入日常数据库收敛。
|
||||
- name: 创建 laptop 独立生产与开发 dataset
|
||||
hosts: pg-laptop
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: 查询专属 ZFS dataset
|
||||
ansible.builtin.command:
|
||||
argv: [zfs, list, -H, -o, name, "data/homelab-pg-{{ item }}"]
|
||||
loop: [prod, dev]
|
||||
register: pg_datasets
|
||||
changed_when: false
|
||||
failed_when: pg_datasets.rc not in [0, 1]
|
||||
check_mode: false
|
||||
- name: 检查新 dataset 挂载路径未被占用
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_data_root }}/{{ item.item }}"
|
||||
loop: "{{ pg_datasets.results }}"
|
||||
when: item.rc == 1
|
||||
register: pg_new_mounts
|
||||
- name: 禁止覆盖已有目录
|
||||
ansible.builtin.assert:
|
||||
that: not item.stat.exists
|
||||
loop: "{{ pg_new_mounts.results }}"
|
||||
when: not (item.skipped | default(false))
|
||||
- name: 新建本项目专用 ZFS dataset
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- zfs
|
||||
- create
|
||||
- -o
|
||||
- "mountpoint={{ pg_data_root }}/{{ item.item }}"
|
||||
- -o
|
||||
- recordsize=16K
|
||||
- -o
|
||||
- compression=lz4
|
||||
- -o
|
||||
- atime=off
|
||||
- -o
|
||||
- "quota={{ '32G' if item.item == 'prod' else '8G' }}"
|
||||
- -o
|
||||
- org.homelab:owner=shared-postgresql
|
||||
- "data/homelab-pg-{{ item.item }}"
|
||||
loop: "{{ pg_datasets.results }}"
|
||||
when: item.rc == 1
|
||||
changed_when: true
|
||||
- name: 核对所有权与挂载路径
|
||||
ansible.builtin.command:
|
||||
argv: [zfs, get, -H, -o, value, 'org.homelab:owner,mountpoint', "data/homelab-pg-{{ item }}"]
|
||||
loop: [prod, dev]
|
||||
register: pg_dataset_owner
|
||||
changed_when: false
|
||||
failed_when: >-
|
||||
pg_dataset_owner.rc != 0 or
|
||||
pg_dataset_owner.stdout_lines != ['shared-postgresql', pg_data_root ~ '/' ~ item]
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
- name: 读取独立管理凭据元数据
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/metadata/infra/postgresql/ayatori/{{ pg_ayatori_instance }}"
|
||||
headers: {X-Vault-Token: "{{ pg_bao_token }}"}
|
||||
status_code: [200, 404]
|
||||
register: pg_admin_metadata
|
||||
no_log: true
|
||||
- name: 只在明确缺失时写入独立的用户名密码
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/infra/postgresql/ayatori/{{ pg_ayatori_instance }}"
|
||||
method: POST
|
||||
headers: {X-Vault-Token: "{{ pg_bao_token }}"}
|
||||
body_format: json
|
||||
body:
|
||||
options: {cas: 0}
|
||||
data:
|
||||
username: "{{ pg_loaded_credentials[pg_ayatori_instance].ayatori_username }}"
|
||||
password: "{{ pg_loaded_credentials[pg_ayatori_instance].ayatori_password }}"
|
||||
status_code: 200
|
||||
when: pg_admin_metadata.status == 404
|
||||
changed_when: true
|
||||
no_log: true
|
||||
- name: 回读已有管理凭据
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/infra/postgresql/ayatori/{{ pg_ayatori_instance }}"
|
||||
headers: {X-Vault-Token: "{{ pg_bao_token }}"}
|
||||
status_code: 200
|
||||
register: pg_admin_readback
|
||||
no_log: true
|
||||
- name: 不隐式覆盖或轮换不一致凭据
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_admin_readback.json.data.data.keys() | sort == ['password', 'username']
|
||||
- pg_admin_readback.json.data.data.username == pg_loaded_credentials[pg_ayatori_instance].ayatori_username
|
||||
- pg_admin_readback.json.data.data.password == pg_loaded_credentials[pg_ayatori_instance].ayatori_password
|
||||
no_log: true
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
- name: 设置实例初始化参数
|
||||
ansible.builtin.set_fact:
|
||||
pg_profile: "{{ pg_profiles[pg_instance] }}"
|
||||
pg_parent_dir: "{{ pg_data_root }}/{{ pg_instance }}"
|
||||
pg_data_dir: "{{ pg_data_root }}/{{ pg_instance }}/data"
|
||||
pg_config_dir: "{{ pg_config_root }}/{{ pg_instance }}"
|
||||
pg_bootstrap_password: "{{ hostvars['localhost'].pg_loaded_credentials[pg_instance].ayatori_password }}"
|
||||
pg_socket_dir: "/run/homelab-postgresql-{{ pg_instance }}"
|
||||
no_log: true
|
||||
- name: 检查版本标记
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_data_dir }}/PG_VERSION"
|
||||
register: pg_version_file
|
||||
- name: 核对未初始化目录为空
|
||||
ansible.builtin.find:
|
||||
paths: "{{ pg_data_dir }}"
|
||||
hidden: true
|
||||
file_type: any
|
||||
register: pg_initial_files
|
||||
when: not pg_version_file.stat.exists
|
||||
- name: 禁止覆盖不完整或未知数据
|
||||
ansible.builtin.assert:
|
||||
that: pg_version_file.stat.exists or pg_initial_files.matched == 0
|
||||
- name: 初始化独立开发实例
|
||||
when: pg_instance == 'dev' and not pg_version_file.stat.exists
|
||||
block:
|
||||
- name: 临时写入开发管理员密码
|
||||
ansible.builtin.copy:
|
||||
content: "{{ hostvars['localhost'].pg_loaded_credentials.dev.superuser_password }}\n"
|
||||
dest: "{{ pg_parent_dir }}/.init-password"
|
||||
owner: "{{ pg_profile.user }}"
|
||||
mode: '0600'
|
||||
no_log: true
|
||||
- name: 仅在空目录初始化
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ pg_bin_dir }}/initdb"
|
||||
- -D
|
||||
- "{{ pg_data_dir }}"
|
||||
- --username={{ pg_profile.user }}
|
||||
- --pwfile={{ pg_parent_dir }}/.init-password
|
||||
- --auth-local=peer
|
||||
- --auth-host=scram-sha-256
|
||||
- --encoding=UTF8
|
||||
- --locale=C.UTF-8
|
||||
- --data-checksums
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
changed_when: true
|
||||
no_log: true
|
||||
always:
|
||||
- name: 删除临时初始化密码文件
|
||||
ansible.builtin.file:
|
||||
path: "{{ pg_parent_dir }}/.init-password"
|
||||
state: absent
|
||||
- name: 登记专属数据目录(启动失败后仍可辨认来源)
|
||||
ansible.builtin.copy:
|
||||
content: "shared-postgresql/{{ pg_instance }}\n"
|
||||
dest: "{{ pg_parent_dir }}/.homelab-owned"
|
||||
owner: root
|
||||
mode: '0644'
|
||||
- name: 启动配置完成的新实例
|
||||
ansible.builtin.systemd_service:
|
||||
name: "homelab-postgresql-{{ pg_instance }}"
|
||||
daemon_reload: true
|
||||
enabled: true
|
||||
state: started
|
||||
- name: 等待本机 SQL 可连接
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- '{{ pg_bin_dir }}/psql'
|
||||
- -X
|
||||
- -At
|
||||
- -h
|
||||
- '{{ pg_socket_dir }}'
|
||||
- -p
|
||||
- '{{ pg_profile.port | string }}'
|
||||
- -d
|
||||
- postgres
|
||||
- -c
|
||||
- SELECT 1
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
register: pg_ready
|
||||
until: pg_ready.rc == 0
|
||||
retries: 30
|
||||
delay: 2
|
||||
changed_when: false
|
||||
- name: 核对本节点是否可写
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- '{{ pg_bin_dir }}/psql'
|
||||
- -X
|
||||
- -At
|
||||
- -h
|
||||
- '{{ pg_socket_dir }}'
|
||||
- -p
|
||||
- '{{ pg_profile.port | string }}'
|
||||
- -d
|
||||
- postgres
|
||||
- -c
|
||||
- SELECT NOT pg_is_in_recovery()
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
register: pg_is_primary
|
||||
changed_when: false
|
||||
- name: 在可写实例创建 Ayatori 最小管理账号
|
||||
when: pg_is_primary.stdout | trim == 't'
|
||||
block:
|
||||
- name: 查询 Ayatori 账号
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- '{{ pg_bin_dir }}/psql'
|
||||
- -X
|
||||
- -At
|
||||
- -h
|
||||
- '{{ pg_socket_dir }}'
|
||||
- -p
|
||||
- '{{ pg_profile.port | string }}'
|
||||
- -d
|
||||
- postgres
|
||||
- -c
|
||||
- SELECT count(*) FROM pg_roles WHERE rolname = 'ayatori'
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
register: pg_ayatori_exists
|
||||
changed_when: false
|
||||
- name: 首次建立受限角色(不静默轮换已存在密码)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- '{{ pg_bin_dir }}/psql'
|
||||
- -X
|
||||
- -v
|
||||
- ON_ERROR_STOP=1
|
||||
- -h
|
||||
- '{{ pg_socket_dir }}'
|
||||
- -p
|
||||
- '{{ pg_profile.port | string }}'
|
||||
- -d
|
||||
- postgres
|
||||
stdin: >-
|
||||
CREATE ROLE ayatori LOGIN NOSUPERUSER CREATEDB CREATEROLE NOREPLICATION NOBYPASSRLS
|
||||
PASSWORD '{{ pg_bootstrap_password | replace("'", "''") }}';
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
when: pg_ayatori_exists.stdout | trim == '0'
|
||||
changed_when: true
|
||||
no_log: true
|
||||
- name: 以实际保存密码及 verify-full 验证权限
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ pg_bin_dir }}/psql"
|
||||
- -X
|
||||
- -At
|
||||
- -c
|
||||
- >-
|
||||
SELECT NOT rolsuper AND rolcreatedb AND rolcreaterole AND NOT pg_is_in_recovery()
|
||||
FROM pg_roles WHERE rolname = current_user
|
||||
environment:
|
||||
PGHOST: "{{ pg_profile.dns }}"
|
||||
PGHOSTADDR: "{{ pg_address }}"
|
||||
PGPORT: "{{ pg_profile.port | string }}"
|
||||
PGDATABASE: postgres
|
||||
PGUSER: ayatori
|
||||
PGPASSWORD: "{{ hostvars['localhost'].pg_loaded_credentials[pg_instance].ayatori_password }}"
|
||||
PGSSLMODE: verify-full
|
||||
PGSSLROOTCERT: "{{ pg_config_dir }}/ca.crt"
|
||||
register: pg_ayatori_check
|
||||
changed_when: false
|
||||
failed_when: pg_ayatori_check.rc != 0 or pg_ayatori_check.stdout | trim != 't'
|
||||
no_log: true
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
- name: 核对秘密元数据(非 404 错误不能视为不存在)
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/metadata/{{ pg_secret_paths[pg_secret_instance] }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
status_code: [200, 404]
|
||||
register: pg_secret_metadata
|
||||
no_log: true
|
||||
- name: 明确不存在时创建一次
|
||||
when: pg_secret_metadata.status == 404
|
||||
block:
|
||||
- name: 拒绝为已有实例重造丢失秘密
|
||||
ansible.builtin.assert:
|
||||
that: >-
|
||||
groups['pg_hosts'] | map('extract', hostvars, 'pg_has_data')
|
||||
| select('equalto', true) | list | length == 0
|
||||
- name: 随机生成只在本次执行内存存在的值
|
||||
ansible.builtin.set_fact:
|
||||
pg_new_secret:
|
||||
ayatori_username: ayatori
|
||||
ayatori_password: "{{ lookup('password', '/dev/null', length=48, chars=['ascii_letters', 'digits']) }}"
|
||||
superuser_password: "{{ lookup('password', '/dev/null', length=48, chars=['ascii_letters', 'digits']) }}"
|
||||
replication_password: "{{ lookup('password', '/dev/null', length=48, chars=['ascii_letters', 'digits']) }}"
|
||||
rest_password: "{{ lookup('password', '/dev/null', length=48, chars=['ascii_letters', 'digits']) }}"
|
||||
no_log: true
|
||||
- name: 使用 KV v2 CAS=0 防覆盖写入
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/{{ pg_secret_paths[pg_secret_instance] }}"
|
||||
method: POST
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
options: {cas: 0}
|
||||
data: "{{ pg_new_secret }}"
|
||||
status_code: 200
|
||||
changed_when: true
|
||||
no_log: true
|
||||
- name: 确认已有秘密数据可读,软删除必须失败
|
||||
ansible.builtin.uri:
|
||||
url: "{{ pg_bao_url }}/v1/kv/data/{{ pg_secret_paths[pg_secret_instance] }}"
|
||||
headers:
|
||||
X-Vault-Token: "{{ pg_bao_token }}"
|
||||
status_code: 200
|
||||
no_log: true
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
- name: 从受管容器配置定位资源
|
||||
ansible.builtin.command:
|
||||
argv: [pct, config, "{{ pg_lxc_id }}"]
|
||||
register: pg_resync_config
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 核对容器和专属挂载
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- pg_lxc_id in [150, 151]
|
||||
- "'shared-etcd' in pg_resync_config.stdout"
|
||||
- "('mp=' ~ pg_lxc_mount ~ ',') in pg_resync_config.stdout"
|
||||
- "pg_resync_config.stdout | regex_findall('(?m)^mp0: pve-rg-hdd:(pm-[0-9a-f]+)_') | length == 1"
|
||||
- name: 提取明确匹配的 LINSTOR resource definition
|
||||
ansible.builtin.set_fact:
|
||||
pg_resync_resource: "{{ (pg_resync_config.stdout | regex_findall('(?m)^mp0: pve-rg-hdd:(pm-[0-9a-f]+)_'))[0] }}"
|
||||
- name: 读取本资源已有配置
|
||||
ansible.builtin.command:
|
||||
argv: [linstor, resource-definition, list-properties, "{{ pg_resync_resource }}"]
|
||||
register: pg_resync_properties
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
- name: 每个新卷将自适应同步速率上限设为 10 MiB/s
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- linstor
|
||||
- resource-definition
|
||||
- set-property
|
||||
- "{{ pg_resync_resource }}"
|
||||
- DrbdOptions/PeerDevice/c-max-rate
|
||||
- '10240'
|
||||
when: pg_resync_properties.stdout is not search('c-max-rate\s*│\s*10240\s*│')
|
||||
changed_when: true
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: 检查现有 PGDATA
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_data_root }}/{{ pg_instance }}/data/PG_VERSION"
|
||||
register: pg_existing_version
|
||||
- name: 检查本项目所有权标记
|
||||
ansible.builtin.stat:
|
||||
path: "{{ pg_data_root }}/{{ pg_instance }}/.homelab-owned"
|
||||
register: pg_owned_marker
|
||||
- name: 拒绝接管未知现有实例
|
||||
ansible.builtin.assert:
|
||||
that: not pg_existing_version.stat.exists or pg_owned_marker.stat.exists
|
||||
fail_msg: 发现未登记 PGDATA,停止;导入和恢复必须使用独立流程。
|
||||
- name: 核对已登记实例
|
||||
when: pg_owned_marker.stat.exists
|
||||
block:
|
||||
- name: 读取实例标记
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ pg_data_root }}/{{ pg_instance }}/.homelab-owned"
|
||||
register: pg_marker_content
|
||||
- name: 验证实例身份
|
||||
ansible.builtin.assert:
|
||||
that: (pg_marker_content.content | b64decode | trim) == ('shared-postgresql/' ~ pg_instance)
|
||||
- name: 核对已有实例主版本,禁止隐式升级
|
||||
when: pg_existing_version.stat.exists
|
||||
block:
|
||||
- name: 读取 PG_VERSION
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ pg_data_root }}/{{ pg_instance }}/data/PG_VERSION"
|
||||
register: pg_existing_version_content
|
||||
- name: 主版本必须匹配
|
||||
ansible.builtin.assert:
|
||||
that: (pg_existing_version_content.content | b64decode | trim) == (pg_major | string)
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
- name: 设置现有实例路径
|
||||
ansible.builtin.set_fact:
|
||||
pg_profile: "{{ pg_profiles[pg_instance] }}"
|
||||
pg_config_dir: "{{ pg_config_root }}/{{ pg_instance }}"
|
||||
pg_socket_dir: "/run/homelab-postgresql-{{ pg_instance }}"
|
||||
pg_certificates_changed: false
|
||||
- name: 续签前必须存在运行中的受管服务
|
||||
ansible.builtin.command:
|
||||
argv: [systemctl, is-active, "homelab-postgresql-{{ pg_instance }}.service"]
|
||||
changed_when: false
|
||||
- name: 检查服务器证书续签
|
||||
ansible.builtin.include_role:
|
||||
name: pg_instance
|
||||
tasks_from: certificate
|
||||
vars:
|
||||
pg_cert_name: server
|
||||
pg_cert_cn: "{{ pg_profile.dns }}"
|
||||
pg_cert_role: "homelab-pg-{{ pg_instance }}"
|
||||
pg_cert_eku: [serverAuth]
|
||||
- name: 检查生产 DCS 客户端证书续签
|
||||
ansible.builtin.include_role:
|
||||
name: pg_instance
|
||||
tasks_from: certificate
|
||||
vars:
|
||||
pg_cert_name: etcd
|
||||
pg_cert_cn: ''
|
||||
pg_cert_role: homelab-etcd-client
|
||||
pg_cert_eku: [clientAuth]
|
||||
when: pg_instance == 'prod'
|
||||
- name: 重载已续签实例(Patroni 或 PostgreSQL 主进程接收 HUP)
|
||||
ansible.builtin.command:
|
||||
argv: [systemctl, kill, --kill-whom=main, --signal=HUP, "homelab-postgresql-{{ pg_instance }}.service"]
|
||||
when: pg_certificates_changed
|
||||
changed_when: true
|
||||
- name: 检查重载后的本机 SQL 可用
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ pg_bin_dir }}/psql"
|
||||
- -X
|
||||
- -At
|
||||
- -h
|
||||
- "{{ pg_socket_dir }}"
|
||||
- -p
|
||||
- "{{ pg_profile.port | string }}"
|
||||
- -d
|
||||
- postgres
|
||||
- -c
|
||||
- SELECT 1
|
||||
become: true
|
||||
become_user: "{{ pg_profile.user }}"
|
||||
changed_when: false
|
||||
register: pg_renew_sql
|
||||
retries: 6
|
||||
delay: 2
|
||||
until: pg_renew_sql.rc == 0 and pg_renew_sql.stdout | trim == '1'
|
||||
Reference in New Issue
Block a user