feat: 纳管共享 etcd 与 k3s 外 PostgreSQL 高可用及备份
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user