Files
aqualog/postgres-ha-install.sh
Nicolay Braetter a1f6a21828 Initial commit: Aquarium Logbuch React/Vite App
- React 18 + Vite Frontend
- Node.js/Express Backend
- Vollstaendige Logbuch-Funktionalitaet fuer Aquarien
- Deploy-Script fuer aqualog CT 211 (192.168.0.246)
2026-04-15 09:24:31 +02:00

590 lines
19 KiB
Bash

#!/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 ""