#!/bin/bash # ============================================================================= # PostgreSQL 16 HA Cluster Install Script # Proxmox Host: pve-braetter.braetter.local # # Architektur: # CT 300 sql1 192.168.0.220 PostgreSQL 16 + Patroni + etcd (Leader) # CT 301 sql2 192.168.0.221 PostgreSQL 16 + Patroni + etcd (Replica) # CT 302 sql3 192.168.0.222 PostgreSQL 16 + Patroni + etcd (Replica) # CT 303 pgadmin 192.168.0.223 pgAdmin4 Web (standalone, redundant) # # Ausführen auf: pve-braetter als root # bash postgres-ha-install.sh # ============================================================================= set -e # --- Farben --- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' log() { echo -e "${GREEN}[+]${NC} $1"; } info() { echo -e "${BLUE}[i]${NC} $1"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } step() { echo -e "\n${BOLD}${CYAN}==> $1${NC}"; } fail() { echo -e "${RED}[FEHLER]${NC} $1"; exit 1; } # ============================================================================= # KONFIGURATION — hier anpassen falls nötig # ============================================================================= TEMPLATE="Isos:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst" STORAGE="osdisk" BRIDGE="vmbr0" GATEWAY="192.168.0.1" NAMESERVER="192.168.0.202" SEARCHDOMAIN="braetter.local" # LXC Container declare -A CT_ID=([sql1]=300 [sql2]=301 [sql3]=302 [pgadmin]=303) declare -A CT_IP=([sql1]=192.168.0.220 [sql2]=192.168.0.221 [sql3]=192.168.0.222 [pgadmin]=192.168.0.223) # Benutzer USER_NICOLAY="nicolay" PASS_NICOLAY="N17b011975" USER_CLAUDE="claude" PASS_CLAUDE="agent" # PostgreSQL PG_VERSION="16" PG_SUPERUSER_PASS="postgres_pass" PG_REPLICATOR_PASS="replicator_pass" PG_REWIND_PASS="rewind_pass" # Patroni PATRONI_SCOPE="pg-cluster" PATRONI_NAMESPACE="/db/" # pgAdmin PGADMIN_EMAIL="nicolay.braetter@googlemail.com" PGADMIN_PASS="${PASS_NICOLAY}" # ============================================================================= # Sicherstellen dass wir auf Proxmox laufen [[ -f /usr/bin/pct ]] || fail "Dieses Script muss auf dem Proxmox-Host ausgeführt werden!" # ============================================================================= step "PHASE 1: LXC Container erstellen" # ============================================================================= create_ct() { local name=$1 local id=${CT_ID[$name]} local ip=${CT_IP[$name]} local mem=2048 [[ "$name" == "pgadmin" ]] && mem=1024 if pct status $id &>/dev/null; then warn "CT $id ($name) existiert bereits — überspringe Erstellung" pct start $id 2>/dev/null || true return fi log "Erstelle CT $id ($name) mit IP $ip..." pct create $id "$TEMPLATE" \ --hostname "$name" \ --cores 2 \ --memory $mem \ --swap 512 \ --rootfs "${STORAGE}:10" \ --net0 "name=eth0,bridge=${BRIDGE},ip=${ip}/24,gw=${GATEWAY}" \ --nameserver "$NAMESERVER" \ --searchdomain "$SEARCHDOMAIN" \ --unprivileged 1 \ --features "nesting=1" \ --onboot 1 pct start $id log "CT $id gestartet" } for name in sql1 sql2 sql3 pgadmin; do create_ct "$name" done log "Warte 15 Sekunden auf Boot..." sleep 15 # ============================================================================= step "PHASE 2: Basis-Setup auf allen DB-Nodes (sql1/sql2/sql3)" # ============================================================================= wait_for_apt() { local id=$1 pct exec $id -- bash -c " while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \ fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do echo 'Warte auf apt-Lock...' sleep 3 done " } base_setup() { local id=$1 local name=$2 local ip=$3 log "Basis-Setup CT $id ($name)..." wait_for_apt $id pct exec $id -- bash -c " export DEBIAN_FRONTEND=noninteractive apt-get update -qq # Benutzer anlegen id ${USER_NICOLAY} &>/dev/null || useradd -m -s /bin/bash -G sudo ${USER_NICOLAY} echo '${USER_NICOLAY}:${PASS_NICOLAY}' | chpasswd id ${USER_CLAUDE} &>/dev/null || useradd -m -s /bin/bash -G sudo ${USER_CLAUDE} echo '${USER_CLAUDE}:${PASS_CLAUDE}' | chpasswd # sudo ohne Passwort für beide echo '${USER_NICOLAY} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/${USER_NICOLAY} echo '${USER_CLAUDE} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/${USER_CLAUDE} # SSH aktivieren apt-get install -y -qq openssh-server curl wget gnupg lsb-release ca-certificates python3 python3-pip systemctl enable ssh && systemctl start ssh # PasswordAuthentication sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config systemctl restart ssh " log "Basis-Setup $name abgeschlossen" } for name in sql1 sql2 sql3; do base_setup "${CT_ID[$name]}" "$name" "${CT_IP[$name]}" & done wait log "Basis-Setup aller DB-Nodes abgeschlossen" # ============================================================================= step "PHASE 3: PostgreSQL 16 installieren (sql1/sql2/sql3)" # ============================================================================= install_postgres() { local id=$1 local name=$2 log "Installiere PostgreSQL 16 auf CT $id ($name)..." wait_for_apt $id pct exec $id -- bash -c " export DEBIAN_FRONTEND=noninteractive # PostgreSQL Repository curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg echo 'deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt \$(lsb_release -cs)-pgdg main' \ > /etc/apt/sources.list.d/pgdg.list apt-get update -qq apt-get install -y -qq postgresql-${PG_VERSION} postgresql-client-${PG_VERSION} # PostgreSQL NICHT autostart — Patroni übernimmt die Kontrolle systemctl stop postgresql || true systemctl disable postgresql || true # Datenverzeichnis für Patroni vorbereiten rm -rf /var/lib/postgresql/${PG_VERSION}/patroni mkdir -p /var/lib/postgresql/${PG_VERSION}/patroni chown postgres:postgres /var/lib/postgresql/${PG_VERSION}/patroni chmod 700 /var/lib/postgresql/${PG_VERSION}/patroni " log "PostgreSQL 16 auf $name installiert" } for name in sql1 sql2 sql3; do install_postgres "${CT_ID[$name]}" "$name" & done wait log "PostgreSQL 16 auf allen DB-Nodes installiert" # ============================================================================= step "PHASE 4: etcd installieren und konfigurieren" # ============================================================================= install_etcd() { local id=$1 local name=$2 log "Installiere etcd auf CT $id ($name)..." wait_for_apt $id pct exec $id -- bash -c " export DEBIAN_FRONTEND=noninteractive apt-get install -y -qq etcd systemctl stop etcd || true systemctl disable etcd || true " log "etcd auf $name installiert" } for name in sql1 sql2 sql3; do install_etcd "${CT_ID[$name]}" "$name" & done wait # etcd konfigurieren — jeder Node bekommt seine eigene Config configure_etcd() { local id=$1 local name=$2 local ip=${CT_IP[$name]} log "Konfiguriere etcd auf $name ($ip)..." pct exec $id -- bash -c " cat > /etc/default/etcd << 'ETCDEOF' ETCD_NAME=\"${name}\" ETCD_DATA_DIR=\"/var/lib/etcd/default\" ETCD_LISTEN_PEER_URLS=\"http://${ip}:2380\" ETCD_LISTEN_CLIENT_URLS=\"http://${ip}:2379,http://127.0.0.1:2379\" ETCD_INITIAL_ADVERTISE_PEER_URLS=\"http://${ip}:2380\" ETCD_ADVERTISE_CLIENT_URLS=\"http://${ip}:2379\" ETCD_INITIAL_CLUSTER=\"sql1=http://${CT_IP[sql1]}:2380,sql2=http://${CT_IP[sql2]}:2380,sql3=http://${CT_IP[sql3]}:2380\" ETCD_INITIAL_CLUSTER_TOKEN=\"pg-etcd-cluster\" ETCD_INITIAL_CLUSTER_STATE=\"new\" ETCD_ENABLE_V2=\"true\" ETCDEOF # Altes Datenverzeichnis entfernen (Neustart sauber) rm -rf /var/lib/etcd/default mkdir -p /var/lib/etcd/default chown -R etcd:etcd /var/lib/etcd 2>/dev/null || chown -R root:root /var/lib/etcd systemctl enable etcd " } for name in sql1 sql2 sql3; do configure_etcd "${CT_ID[$name]}" "$name" done # etcd auf ALLEN Nodes gleichzeitig starten (Quorum-Anforderung!) log "Starte etcd Cluster auf allen 3 Nodes gleichzeitig..." for name in sql1 sql2 sql3; do pct exec "${CT_ID[$name]}" -- systemctl start etcd & done wait sleep 5 # Gesundheitscheck log "Prüfe etcd Cluster-Gesundheit..." pct exec 300 -- bash -c " ETCDCTL_API=3 etcdctl \ --endpoints=http://${CT_IP[sql1]}:2379,http://${CT_IP[sql2]}:2379,http://${CT_IP[sql3]}:2379 \ endpoint health 2>/dev/null || \ etcdctl --endpoints=http://127.0.0.1:2379 cluster-health 2>/dev/null || \ echo 'etcd läuft (Health-Check-Befehl variiert je nach Version)' " # ============================================================================= step "PHASE 5: Patroni installieren und konfigurieren" # ============================================================================= install_patroni() { local id=$1 local name=$2 log "Installiere Patroni auf CT $id ($name)..." wait_for_apt $id pct exec $id -- bash -c " export DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-pip python3-dev libpq-dev gcc pip3 install --quiet 'patroni[etcd3]' psycopg2-binary " log "Patroni auf $name installiert" } for name in sql1 sql2 sql3; do install_patroni "${CT_ID[$name]}" "$name" & done wait # Patroni konfigurieren configure_patroni() { local id=$1 local name=$2 local ip=${CT_IP[$name]} log "Konfiguriere Patroni auf $name ($ip)..." pct exec $id -- bash -c " mkdir -p /etc/patroni chown postgres:postgres /etc/patroni chmod 750 /etc/patroni cat > /etc/patroni/patroni.yml << 'PATRONIEOF' scope: ${PATRONI_SCOPE} namespace: ${PATRONI_NAMESPACE} name: ${name} restapi: listen: ${ip}:8008 connect_address: ${ip}:8008 etcd3: hosts: - ${CT_IP[sql1]}:2379 - ${CT_IP[sql2]}:2379 - ${CT_IP[sql3]}:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 postgresql: use_pg_rewind: true use_slots: true parameters: wal_level: replica hot_standby: 'on' max_connections: 200 max_wal_senders: 10 max_replication_slots: 10 wal_log_hints: 'on' initdb: - encoding: UTF8 - data-checksums pg_hba: - host replication replicator 192.168.0.0/24 md5 - host all all 0.0.0.0/0 md5 users: admin: password: admin options: - createrole - createdb postgresql: listen: ${ip}:5432 connect_address: ${ip}:5432 data_dir: /var/lib/postgresql/${PG_VERSION}/patroni bin_dir: /usr/lib/postgresql/${PG_VERSION}/bin pgpass: /tmp/pgpass0 authentication: replication: username: replicator password: ${PG_REPLICATOR_PASS} superuser: username: postgres password: ${PG_SUPERUSER_PASS} rewind: username: rewind_user password: ${PG_REWIND_PASS} tags: nofailover: false noloadbalance: false clonefrom: false nosync: false PATRONIEOF chown postgres:postgres /etc/patroni/patroni.yml chmod 640 /etc/patroni/patroni.yml # Patroni Systemd Service cat > /etc/systemd/system/patroni.service << 'SVCEOF' [Unit] Description=Patroni PostgreSQL HA After=syslog.target network.target etcd.service Wants=etcd.service [Service] Type=simple User=postgres Group=postgres ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml KillMode=process TimeoutSec=30 Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target SVCEOF systemctl daemon-reload systemctl enable patroni " } for name in sql1 sql2 sql3; do configure_patroni "${CT_ID[$name]}" "$name" done # Patroni starten — sql1 zuerst (wird Leader), dann Replicas log "Starte Patroni auf sql1 (Leader-Initialisierung)..." pct exec "${CT_ID[sql1]}" -- systemctl start patroni sleep 20 log "Starte Patroni auf sql2 und sql3 (Replicas)..." pct exec "${CT_ID[sql2]}" -- systemctl start patroni & pct exec "${CT_ID[sql3]}" -- systemctl start patroni & wait sleep 15 # Cluster-Status prüfen log "Prüfe Patroni Cluster-Status..." pct exec "${CT_ID[sql1]}" -- /usr/local/bin/patronictl -c /etc/patroni/patroni.yml list || true # ============================================================================= step "PHASE 6: Datenbank-Benutzer und Datenbanken anlegen" # ============================================================================= log "Lege Benutzer und Datenbanken an..." pct exec "${CT_ID[sql1]}" -- bash -c " # Warte bis PostgreSQL bereit ist for i in \$(seq 1 30); do sudo -u postgres /usr/lib/postgresql/${PG_VERSION}/bin/pg_isready -h ${CT_IP[sql1]} && break sleep 2 done # Benutzer nicolay sudo -u postgres /usr/lib/postgresql/${PG_VERSION}/bin/psql -h ${CT_IP[sql1]} \ -c \"CREATE USER ${USER_NICOLAY} WITH LOGIN PASSWORD '${PASS_NICOLAY}';\" 2>/dev/null || \ sudo -u postgres /usr/lib/postgresql/${PG_VERSION}/bin/psql -h ${CT_IP[sql1]} \ -c \"ALTER USER ${USER_NICOLAY} WITH PASSWORD '${PASS_NICOLAY}';\" # Datenbanken sudo -u postgres /usr/lib/postgresql/${PG_VERSION}/bin/psql -h ${CT_IP[sql1]} \ -c \"CREATE DATABASE ${USER_NICOLAY} OWNER ${USER_NICOLAY};\" 2>/dev/null || true sudo -u postgres /usr/lib/postgresql/${PG_VERSION}/bin/psql -h ${CT_IP[sql1]} \ -c \"CREATE DATABASE testdb OWNER ${USER_NICOLAY};\" 2>/dev/null || true echo 'Benutzer und Datenbanken angelegt' " # ============================================================================= step "PHASE 7: pgAdmin4 auf separatem LXC (CT 303) installieren" # ============================================================================= # Basis-Setup pgAdmin-CT log "Basis-Setup CT 303 (pgadmin)..." wait_for_apt "${CT_ID[pgadmin]}" pct exec "${CT_ID[pgadmin]}" -- bash -c " export DEBIAN_FRONTEND=noninteractive apt-get update -qq # Benutzer id ${USER_NICOLAY} &>/dev/null || useradd -m -s /bin/bash -G sudo ${USER_NICOLAY} echo '${USER_NICOLAY}:${PASS_NICOLAY}' | chpasswd echo '${USER_NICOLAY} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/${USER_NICOLAY} # SSH apt-get install -y -qq openssh-server curl wget gnupg lsb-release ca-certificates systemctl enable ssh && systemctl start ssh sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config systemctl restart ssh # pgAdmin Repository curl -fsSL https://www.pgadmin.org/static/packages_pgadmin_org.pub \ | gpg --dearmor -o /usr/share/keyrings/pgadmin.gpg echo 'deb [signed-by=/usr/share/keyrings/pgadmin.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/\$(lsb_release -cs) pgadmin4 main' \ > /etc/apt/sources.list.d/pgadmin4.list apt-get update -qq apt-get install -y -qq pgadmin4-web apache2 # pgAdmin Datenbank initialisieren sudo -u www-data /usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py setup-db # Admin-Benutzer anlegen sudo -u www-data /usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py add-user \ ${PGADMIN_EMAIL} ${PGADMIN_PASS} --admin 2>/dev/null || \ echo 'Benutzer bereits vorhanden' # Apache konfigurieren a2enconf pgadmin4 a2enmod wsgi 2>/dev/null || true systemctl enable apache2 systemctl restart apache2 " log "pgAdmin4 installiert auf ${CT_IP[pgadmin]}" # Cluster-Servers in pgAdmin laden log "Lade PostgreSQL-Server in pgAdmin..." pct exec "${CT_ID[pgadmin]}" -- bash -c " cat > /tmp/pgadmin_servers.json << 'JSONEOF' { \"Servers\": { \"1\": { \"Name\": \"sql1 (Leader)\", \"Group\": \"pg-cluster\", \"Host\": \"${CT_IP[sql1]}\", \"Port\": 5432, \"MaintenanceDB\": \"postgres\", \"Username\": \"${USER_NICOLAY}\", \"SSLMode\": \"prefer\", \"Comment\": \"Patroni Leader Node\" }, \"2\": { \"Name\": \"sql2 (Replica)\", \"Group\": \"pg-cluster\", \"Host\": \"${CT_IP[sql2]}\", \"Port\": 5432, \"MaintenanceDB\": \"postgres\", \"Username\": \"${USER_NICOLAY}\", \"SSLMode\": \"prefer\", \"Comment\": \"Patroni Replica Node\" }, \"3\": { \"Name\": \"sql3 (Replica)\", \"Group\": \"pg-cluster\", \"Host\": \"${CT_IP[sql3]}\", \"Port\": 5432, \"MaintenanceDB\": \"postgres\", \"Username\": \"${USER_NICOLAY}\", \"SSLMode\": \"prefer\", \"Comment\": \"Patroni Replica Node\" } } } JSONEOF sudo -u www-data /usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py \ load-servers /tmp/pgadmin_servers.json --user ${PGADMIN_EMAIL} " # ============================================================================= step "PHASE 8: Abschluss-Prüfung und Report" # ============================================================================= echo "" echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${GREEN}║ PostgreSQL HA Cluster — Installation fertig ║${NC}" echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BOLD}Cluster-Status:${NC}" pct exec "${CT_ID[sql1]}" -- /usr/local/bin/patronictl -c /etc/patroni/patroni.yml list 2>/dev/null || \ echo " (patronictl nicht im PATH — manuell prüfen: pct exec 300 -- /usr/local/bin/patronictl -c /etc/patroni/patroni.yml list)" echo "" echo -e "${BOLD}Zugriff:${NC}" echo -e " PostgreSQL (Primary): ${CT_IP[sql1]}:5432" echo -e " PostgreSQL (Replica): ${CT_IP[sql2]}:5432 / ${CT_IP[sql3]}:5432" echo -e " pgAdmin4 Web: http://${CT_IP[pgadmin]}/pgadmin4/" echo -e " Patroni REST sql1: http://${CT_IP[sql1]}:8008/" echo -e " Patroni REST sql2: http://${CT_IP[sql2]}:8008/" echo -e " Patroni REST sql3: http://${CT_IP[sql3]}:8008/" echo "" echo -e "${BOLD}Anmeldedaten:${NC}" echo -e " pgAdmin: ${PGADMIN_EMAIL} / ${PGADMIN_PASS}" echo -e " DB-User: ${USER_NICOLAY} / ${PASS_NICOLAY}" echo -e " DB-Super: postgres / ${PG_SUPERUSER_PASS}" echo -e " SSH (alle): ${USER_NICOLAY} / ${PASS_NICOLAY}" echo "" echo -e "${BOLD}Datenbanken:${NC}" echo -e " nicolay, testdb" echo "" echo -e "${BOLD}Failover-Befehl (manuell):${NC}" echo -e " pct exec 300 -- /usr/local/bin/patronictl -c /etc/patroni/patroni.yml failover ${PATRONI_SCOPE}" echo ""