Bucardo is a powerful replication system for PostgreSQL, enabling asynchronous multi-master replication and other advanced replication configurations. Installing Bucardo within Docker allows you to easily deploy, manage, and scale replication setups in isolated containers, which can streamline database management in production environments. This guide will walk you through the process of setting up Bucardo inside Docker as a table-based postgresql replication system.


Preparations

We need to setup the schema from source to target database.

# dump database dari source
pg_dump -U postgres -h 192.168.12.16 -p 5432 -d my-db -s > schema.sql

# restore ke database target
psql -U postgres -h 192.168.12.18 -p 5432 my-db < schema.sql


# by table
pg_dump -U postgres -h 192.168.12.16 -p 5432 -d my-db -s -t tabel1 -t tabel2 > schema.sql
pg_dump -U postgres -h 10.41.1.45 -p 5432 -d skm -s -t tabel1 -t tabel2 > skm.sql

Folder Structure

Create Dockerfile
# Gunakan Ubuntu 22.04 LTS sebagai basis
FROM ubuntu:22.04

ARG DEBIAN_FRONTEND=noninteractive

# Update dan instal PostgreSQL, Bucardo, dan dependensi dasar
RUN apt-get update \
    && apt-get install -y \
        wget \
        iputils-ping \
        postgresql \
        postgresql-contrib \
        bucardo \
        jq \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Salin konfigurasi
COPY etc/pg_hba.conf /etc/postgresql/14/main/
COPY etc/bucardorc /etc/bucardorc
COPY etc/.pgpass /root/.pgpass

# Set permissions dan setup Bucardo
RUN chmod 600 /root/.pgpass
RUN chown postgres /etc/postgresql/14/main/pg_hba.conf \
    && chown postgres /etc/bucardorc \
    && chown postgres /var/log/bucardo \
    && mkdir -p /var/run/bucardo && chown postgres /var/run/bucardo \
    && usermod -aG bucardo postgres \
    && echo "listen_addresses='*'" >> /etc/postgresql/14/main/postgresql.conf

# Salin skrip entrypoint
COPY lib/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Volume untuk Bucardo
VOLUME "/media/bucardo"

EXPOSE 5432

# Command untuk menjalankan PostgreSQL, konfigurasi dan Bucardo

CMD ["/bin/bash", "-c", "\
    service postgresql start && \
    sleep 5 && \
    su - postgres -c \"psql -tc \\\"SELECT 1 FROM pg_roles WHERE rolname='bucardo'\\\" | grep -q 1 || psql -c \\\"CREATE USER bucardo WITH SUPERUSER PASSWORD 'password';\\\"\" && \
    su - postgres -c \"psql -tc \\\"SELECT 1 FROM pg_database WHERE datname='bucardo'\\\" | grep -q 1 || createdb -O postgres bucardo\" && \
    su - postgres -c \"bucardo install --batch\" && \
    /entrypoint.sh"]

Create docker-compose.yaml
services:
  bucardo:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: bucardo_container
    networks:
      - bucardo_network
    volumes:
      - ./bucardo-config/bucardo.json:/media/bucardo/bucardo.json
    ports:
      - "6432:5432"

networks:
  bucardo_network:
    driver: bridge

Create new folder named it bucardo-config

create bucardo.json inside that folder

{
    "databases": [
        {
            "id": 0,
            "dbname": "my-db",
            "host": "192.168.12.16",
            "port": "5432",
            "user": "postgres",
            "pass": "mypassW0Rd"
        },
        {
            "id": 1,
            "dbname": "my-db",
            "host": "192.168.12.18",
            "port": "5432",
            "user": "postgres",
            "pass": "myOtherDBmypassW0Rd"
        }
    ],
    "syncs": [
        {
            "sources": [
                0
            ],
            "targets": [
                1
            ],
            "tables": "all",
            "onetimecopy": 1
        },
        {
            "sources": [
                1
            ],
            "targets": [
                2
            ],
            "tables": "all",
            "onetimecopy": 1
        }
    ]
}

create new folder named it etc

create 3 files:

  • .pgpass
  • bucardorc
  • pg_hba.conf
.pgpass
192.168.12.16:5432:my-db:bucardo:passw789WOrdxnaord123
192.168.12.18:5432:my-db:bucardo:passw789WOrdxnaord123
bucardorc
dbport = 5432
dbuser = bucardo
dbname = bucardo
pg_hba.conf
local   all             postgres                                trust
local   bucardo         bucardo                                 trust
local   all             all                                     md5
host    all             all             0.0.0.0/0               md5

Create folder lib

create entrypoint.sh inside that folder

#/bin/bash

# Exits with an error when there are any invalid chars
check_invalid_chars() {
  local attr_name=$1
  local invalid_chars=$2
  if [[ "$invalid_chars" != "" ]]; then
    echo "[ERROR] Invalid value \"$string_attr\" for attribute $attr_name!" 1>&2
    exit 1
  fi
}

# An Integer is only allowed to be an Integer
validate_integer_attr() {
  local attr_name=$1
  local string_attr=$2
  local invalid_chars=$(echo $string_attr | sed -e "s/[[:digit:]]//g")
  check_invalid_chars $attr_name $invalid_chars
}

# A String is allowed to have alphanumeric chars,
# dots, dashes, and underscores.
validate_string_attr() {
  local attr_name=$1
  local string_attr=$2
  local invalid_chars=$(echo $string_attr | sed -e "s/[[:alnum:]]//g" \
                                                -e "s/\(.\|-\|_\)//g")
  check_invalid_chars $attr_name $invalid_chars
}

# A List is allowed to have have alphanumeric chars,
# dots, dashes, underscores, and commas.
validate_list_attr() {
  local attr_name=$1
  local string_attr=$2
  local invalid_chars=$(echo $string_attr | sed -e "s/\,//g")
  validate_string_attr $attr_name $invalid_chars
}

run_bucardo_command() {
  local comm=$1
  su - postgres -c "bucardo $comm"
}

start_postgres() {
  service postgresql start
  local status=false
  while [[ $status == false ]]; do
    [[ $(run_bucardo_command "status") ]] && status=true
    sleep 5
  done
}

db_attr() {
  local database=$1
  local attr=$2
  local attr_type=$3
  local value="$(jq ".databases[$database].$attr" /media/bucardo/bucardo.json)"
  case "$attr_type" in
    "string") validate_string_attr "$attr" "$value" ;;
    "list") validate_list_attr "$attr" "$value" ;;
    "integer") validate_integer_attr "$attr" "$value" ;;
    *) echo "Invalid type for $attr." 1>&2; exit 2;;
  esac
  echo $value
}

sync_attr() {
  local sync=$1
  local attr=$2
  local attr_type=$3
  local value="$(jq ".syncs[$sync].$attr" /media/bucardo/bucardo.json)"
  case "$attr_type" in
    "string") validate_string_attr "$attr" "$value" ;;
    "list") validate_list_attr "$attr" "$value" ;;
    "integer") validate_integer_attr "$attr" "$value" ;;
    *) echo "Invalid type for $attr." 1>&2; exit 2;;
  esac
  echo $value
}

#First validate onetimecopy is an integer and then validate if it's 0,1 or 2
one_time_copy_attr() {
  local sync_index=$1
  local value=$(sync_attr $sync_index onetimecopy integer)
  local invalid_chars=$(echo $string_attr | sed -e "s/[0,1,2]//")
  check_invalid_chars $value $invalid_chars
  echo $value
}

load_db_pass() {
  local database=$1
  local pass=$(db_attr $database pass string)
  local id=$(db_attr $database id integer)
  if [[ "$pass" == "\"env\"" ]]; then
    echo "$(env | grep "BUCARDO_DB$id" | cut -d'=' -f2)"
  else
    echo "$pass"
  fi
}

add_databases_to_bucardo() {
  echo "[CONTAINER] Registrasi databases ke Bucardo Bosque..."
  local db_id db_pass
  local db_index=0
  NUM_DBS=$(jq '.databases' /media/bucardo/bucardo.json | grep dbname | wc -l)
  while [[ $db_index -lt $NUM_DBS ]]; do
    echo "[CONTAINER] Menambahkan db$db_index nama database: $(db_attr $db_index dbname string)"
    db_id=$(db_attr $db_index id integer)
    db_pass=$(load_db_pass $db_index)
    run_bucardo_command "del db db$db_id --force"
    run_bucardo_command "add db db$db_id dbname=\"$(db_attr $db_index dbname string)\" \
                                user=\"$(db_attr $db_index user string)\" \
                                pass=\"$db_pass\" \
                                host=\"$(db_attr $db_index host string)\" \
                                port=\"$(db_attr $db_index port string)\""
    db_index=$(expr $db_index + 1)
  done
}

db_sync_entities() {
  local sync_index=$1
  local entity=$2
  local db_index=0
  local sync_entity

  sync_entity=$(sync_attr $sync_index $entity"s[$db_index]" string)
  while [[ "$sync_entity" != null ]]; do
    [[ "$DB_STRING" != "" ]] && DB_STRING="$DB_STRING,"
    DB_STRING=$DB_STRING"db"$sync_entity":$entity"
    db_index=$(expr $db_index + 1)
    sync_entity=$(sync_attr $sync_index $entity"s[$db_index]" string)
  done
}

db_sync_string() {
  local sync_index=$1
  DB_STRING=""
  db_sync_entities $sync_index "source"
  db_sync_entities $sync_index "target"
}

add_syncs_to_bucardo() {
  local sync_index=0
  local num_syncs=$(jq '.syncs' /media/bucardo/bucardo.json | grep tables | wc -l)
  while [[ $sync_index -lt $num_syncs ]]; do
    echo "[CONTAINER] Menambahkan sync$sync_index ke Bucardo..."
    db_sync_string $sync_index
    local one_time_copy="$(one_time_copy_attr $sync_index)"

    # Periksa apakah "tables" bernilai "all"
    local tables_attr=$(sync_attr $sync_index tables string)
    if [[ "$tables_attr" == "\"all\"" ]]; then
      tables_attr="all"
    fi

    run_bucardo_command "del sync sync$sync_index"
    run_bucardo_command "add sync sync$sync_index \
                         dbs=$DB_STRING \
                         tables=$tables_attr \
                         onetimecopy=$one_time_copy"
    sync_index=$(expr $sync_index + 1)
  done
}


start_bucardo() {
  echo "[CONTAINER] Starting Bucardo..."
  run_bucardo_command "start"
}

bucardo_status() {
  echo "[CONTAINER] Update status replica terkini..."
  local run=true
  while [[ $run ]]; do
    run_bucardo_command "status"
    sleep 10
  done
}

main() {
  start_postgres 2> /dev/null
  add_databases_to_bucardo
  add_syncs_to_bucardo
  start_bucardo
  bucardo_status
}

main

Run Bucardo using docker
docker-compose up -d --build

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *