diff --git a/roles/postgresql_replica_logical.rb b/roles/postgresql_replica_logical.rb new file mode 100644 index 0000000..fd2af28 --- /dev/null +++ b/roles/postgresql_replica_logical.rb @@ -0,0 +1,7 @@ +name "postgresql_replica_logical" + +run_list %w( + kosmos_postgresql::hostsfile + kosmos_postgresql::replica_logical + kosmos_postgresql::firewall +) diff --git a/site-cookbooks/kosmos_postgresql/files/create_publications.sh b/site-cookbooks/kosmos_postgresql/files/create_publications.sh new file mode 100644 index 0000000..249bc3d --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/files/create_publications.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +echo "== Creating publication in each database ==" + +for db in $(psql -Atqc "SELECT datname FROM pg_database WHERE datallowconn AND datname NOT IN ('template0','template1')"); do + echo "Processing DB: $db" + + # Create publication (idempotent) + psql -d "$db" -v ON_ERROR_STOP=1 </dev/null || true + + # Drop subscription if exists + psql -d "$db" -v ON_ERROR_STOP=1 < globals.sql) && \ +psql -Atqc "SELECT datname FROM pg_database WHERE datallowconn AND datname NOT IN (''template0'')" | \ +xargs -I{} -P4 sh -c " + pg_dump -Fd -j 4 -d \"{}\" -f dump_{} && + tar -cf - dump_{} | zstd -19 -T0 > dump_{}.tar.zst && + rm -rf dump_{} +" diff --git a/site-cookbooks/kosmos_postgresql/files/list_publications.sh b/site-cookbooks/kosmos_postgresql/files/list_publications.sh new file mode 100644 index 0000000..270b4cf --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/files/list_publications.sh @@ -0,0 +1,5 @@ +#!/bin/bash +for db in $(psql -Atqc "SELECT datname FROM pg_database WHERE datallowconn"); do + echo "DB: $db" + psql -d "$db" -Atqc "SELECT pubname FROM pg_publication;" +done diff --git a/site-cookbooks/kosmos_postgresql/files/list_replication_slots.sh b/site-cookbooks/kosmos_postgresql/files/list_replication_slots.sh new file mode 100644 index 0000000..591479b --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/files/list_replication_slots.sh @@ -0,0 +1,5 @@ +#!/bin/bash +psql -c " +SELECT slot_name, +pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) +FROM pg_replication_slots;" diff --git a/site-cookbooks/kosmos_postgresql/files/list_subscriptions.sh b/site-cookbooks/kosmos_postgresql/files/list_subscriptions.sh new file mode 100644 index 0000000..d2522ba --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/files/list_subscriptions.sh @@ -0,0 +1,5 @@ +#!/bin/bash +for db in $(psql -Atqc "SELECT datname FROM pg_database WHERE datallowconn AND datname NOT IN ('template0','template1')"); do + echo "==== DB: $db ====" + psql -d "$db" -c "SELECT * FROM pg_stat_subscription;" +done diff --git a/site-cookbooks/kosmos_postgresql/files/restore_all_databases.sh b/site-cookbooks/kosmos_postgresql/files/restore_all_databases.sh new file mode 100644 index 0000000..c80445d --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/files/restore_all_databases.sh @@ -0,0 +1,8 @@ +#!/bin/bash +cd /tmp +for f in dump_*.tar.zst; do + db=$(echo $f | sed "s/dump_\(.*\)\.tar\.zst/\1/") + echo "Restoring $db" + zstd -d "$f" -c | tar -xf - + pg_restore -j 4 -d "$db" dump_$db +done diff --git a/site-cookbooks/kosmos_postgresql/recipes/primary.rb b/site-cookbooks/kosmos_postgresql/recipes/primary.rb index 406e2ae..91e6008 100644 --- a/site-cookbooks/kosmos_postgresql/recipes/primary.rb +++ b/site-cookbooks/kosmos_postgresql/recipes/primary.rb @@ -7,3 +7,37 @@ postgresql_custom_server postgresql_version do role "primary" end +cookbook_file "/usr/local/bin/pg_dump_all_databases" do + source "dump_all_databases.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_create_replication_publications" do + source "create_publications.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_drop_replication_publications" do + source "drop_publications.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_list_replication_publications" do + source "list_publications.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_list_replication_slots" do + source "list_replication_slots.sh" + user "postgres" + group "postgres" + mode "0744" +end diff --git a/site-cookbooks/kosmos_postgresql/recipes/replica_logical.rb b/site-cookbooks/kosmos_postgresql/recipes/replica_logical.rb new file mode 100644 index 0000000..b881a90 --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/recipes/replica_logical.rb @@ -0,0 +1,50 @@ +# +# Cookbook:: kosmos_postgresql +# Recipe:: replica_logical +# + +service postgresql_service do + supports restart: true, status: true, reload: true +end + +postgresql_custom_server postgresql_version do + role "replica_logical" +end + +postgresql_data_bag_item = data_bag_item('credentials', 'postgresql') + +primary = postgresql_primary + +if primary.nil? + Chef::Log.warn("No PostgreSQL primary node found. Skipping replication setup.") + return +end + +template "/usr/local/bin/pg_create_replication_subscriptions" do + source "create_subscriptions.sh.erb" + user "postgres" + group "postgres" + mode "0740" + sensitive true +end + +cookbook_file "/usr/local/bin/pg_drop_replication_subscriptions" do + source "drop_subscriptions.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_list_replication_subscriptions" do + source "list_subscriptions.sh" + user "postgres" + group "postgres" + mode "0744" +end + +cookbook_file "/usr/local/bin/pg_restore_all_databases" do + source "restore_all_databases.sh" + user "postgres" + group "postgres" + mode "0744" +end diff --git a/site-cookbooks/kosmos_postgresql/resources/server.rb b/site-cookbooks/kosmos_postgresql/resources/server.rb index f08a70d..031820d 100644 --- a/site-cookbooks/kosmos_postgresql/resources/server.rb +++ b/site-cookbooks/kosmos_postgresql/resources/server.rb @@ -56,7 +56,9 @@ action :create do timezone: "UTC", # default is GMT listen_addresses: "0.0.0.0", promote_trigger_file: "#{postgresql_data_dir}/failover.trigger", - wal_keep_size: 4096 # 256 segments, 16MB each + wal_level: "logical", + wal_keep_size: 4096, # 256 segments, 16MB each + max_replication_slots: 16 } postgresql_server_conf "main" do diff --git a/site-cookbooks/kosmos_postgresql/templates/create_subscriptions.sh.erb b/site-cookbooks/kosmos_postgresql/templates/create_subscriptions.sh.erb new file mode 100644 index 0000000..1d8cb34 --- /dev/null +++ b/site-cookbooks/kosmos_postgresql/templates/create_subscriptions.sh.erb @@ -0,0 +1,33 @@ +set -e + +echo "== Creating subscriptions for all databases ==" + +for db in $(psql -Atqc "SELECT datname FROM pg_database WHERE datallowconn AND datname NOT IN ('template0','template1')"); do + echo "Processing DB: $db" + + SLOT="migrate_slot_${db}" + SUB="migrate_sub_${db}" + + psql -d "$db" -v ON_ERROR_STOP=1 < port=<%= @pg_port %> dbname=$db user=<%= @pg_user %> password=<%= @pg_pass %>' + PUBLICATION migrate_pub + WITH ( + slot_name = '$SLOT', + create_slot = false, + copy_data = false, + enabled = true + ); + END IF; +END +\$\$; +SQL + +done + +echo "== Done =="