PostgreSQL Active/Passive Replication with Load Balancer & DNS
Simulate. Automate. Failover.
- Features
- Infrastructure as Code (IaC)
- Safe Playground for Real-World Scenarios
- Architecture
- Network Topology
- Requirements
- Quick Start
- Installation
- Testing the Application
- Advanced Tests (Optional)
- Access from Host Machine
- Project Structure
- Common Issues and Fixes
- Frequently Asked Questions
- Contributing
- License
- Author
- π’ 2 Data Centers β Simulated with Vagrant (DC1 and DC2)
- ποΈ PostgreSQL 16 β Active/Passive with Streaming Replication
- π Flask Application β Deployed on both DCs
- βοΈ HAProxy β Load balancer with round-robin distribution
- π‘ Bind9 β DNS server for dynamic failover
- π€ Fully Automated β Single command deployment (
vagrant up) - π Replication β Real-time data synchronization
- π§ͺ Optional Tests β Switchover and Disaster Recovery scenarios
This project is built on Infrastructure as Code principles:
- Vagrant defines the entire infrastructure (VMs, networks, resources) in a single
Vagrantfile. - Provisioning scripts automate the installation and configuration of all software (PostgreSQL, Flask, HAProxy, Bind9).
With a single command (vagrant up), you can recreate the entire 2-data-center environment from scratch. This is the power of IaC: consistent, repeatable, and automated infrastructure.
π‘ Infrastructure as Code is a core practice in DevOps and SRE (Site Reliability Engineering). It enables teams to manage infrastructure with the same rigor as application code.
One of the greatest benefits of this lab is that it provides a safe, isolated environment to test and learn from real-world infrastructure scenarios without any risk to production systems.
| Scenario | What you can test |
|---|---|
| π Switchover | Promote a replica to primary and reconfigure the old primary as a replica β all without downtime. |
| π§― Disaster Recovery (DR) | Simulate the complete loss of a data center (DC1) and verify that the system continues to work using DC2. |
| π οΈ Failover Testing | Test how the system behaves when a database or application server goes down. |
| π‘ DNS Failover | Update DNS records dynamically to point to a new primary database. |
| βοΈ Load Balancing | Test HAProxy's round-robin distribution and see how it handles traffic. |
| π§ Configuration Changes | Modify PostgreSQL, Flask, or HAProxy settings and observe the impact in real time. |
| π§ͺ Rollback Simulations | Practice reincorporating a failed node back into the cluster after recovery. |
β‘ Key advantage: All of these tests can be performed safely and repeatedly in your local environment. Break things, learn from the failures, and rebuild everything with
vagrant destroy -f && vagrant upβ it's that simple.
This approach helps build the confidence and skills needed to manage real production environments, where mistakes are costly and downtime is unacceptable.
| Machine | DC | IP (Host-Only) | Role | RAM | CPU |
|---|---|---|---|---|---|
| db1 | 1 | 10.10.1.20 |
PostgreSQL Primary | 2048 MB | 2 |
| app1 | 1 | 10.10.1.10 |
Flask App DC1 | 1024 MB | 1 |
| db2 | 2 | 10.10.2.20 |
PostgreSQL Replica | 2048 MB | 2 |
| app2 | 2 | 10.10.2.10 |
Flask App DC2 | 1024 MB | 1 |
| lb | - | 10.10.1.100 |
HAProxy Load Balancer | 512 MB | 1 |
| dns | - | 10.10.1.200 |
Bind9 DNS Server | 512 MB | 1 |
| Requirement | Minimum | Recommended |
|---|---|---|
| Vagrant | 2.4+ | Latest |
| VirtualBox | 7.0+ | Latest |
| RAM | 8 GB | 16 GB+ |
| Disk Space | 20 GB | 40 GB+ |
| OS | Windows 10/11, macOS, Linux | Any with VirtualBox support |
β Tested on: Windows 11 Pro with VirtualBox 7.2.12 and Vagrant 2.4.9. Compatible with macOS and Linux (may require minor adjustments).
π‘ For a detailed step-by-step guide, see Installation below.
# 1. Clone the repository
git clone https://github.com/rafajimenezdev/2dc-ha-lab.git
cd 2dc-ha-lab
# 2. Start the environment (this will create 6 VMs)
vagrant up
# 3. Test the application
curl http://10.10.1.100/items
# 4. Insert data
curl -X POST http://10.10.1.100/items -H "Content-Type: application/json" -d '{"id": 42}'
# 5. Verify replication
curl http://10.10.1.100/itemsgit clone https://github.com/rafajimenezdev/2dc-ha-lab.git
cd 2dc-ha-lab# 1. Install VirtualBox: https://www.virtualbox.org/
# 2. Install Vagrant: https://www.vagrantup.com/
# 3. Ensure Hyper-V is disabled (if using VirtualBox)# Install VirtualBox and Vagrant
sudo apt update
sudo apt install virtualbox vagrant -ybrew install --cask virtualbox
brew install vagrantvagrant --version
virtualbox --helpcurl http://10.10.1.100/Expected output:
Flask App is running! (DC1)
or
Flask App is running! (DC2)
curl -X POST http://10.10.1.100/items \
-H "Content-Type: application/json" \
-d '{"id": 42}'Expected output:
{"status":"inserted","id":42}curl http://10.10.1.100/itemsExpected output:
[42]Β‘ME HA ENCANTADO TU ENTORNO, RAFAEL! π₯
Β‘Me alegra un montΓ³n que haya funcionado todo a la primera! π
Y sΓ, claro que podemos integrar esta baterΓa de pruebas en el README. Es exactamente lo que un usuario necesita para validar que su laboratorio estΓ‘ funcionando correctamente. Y que lo hayas disfrutado es la mejor seΓ±al de que el proyecto estΓ‘ bien hecho.
Te voy a dar la secciΓ³n exacta para que la pegues en tu README, dentro de la secciΓ³n de "Testing the Application". He mantenido el estilo profesional y los emojis que te gustan.
After a successful vagrant up, run these tests to ensure the entire lab is functioning correctly.
vagrant statusAll machines should show running (virtualbox).
ping 10.10.1.10 # app1
ping 10.10.1.20 # db1
ping 10.10.2.10 # app2
ping 10.10.2.20 # db2
ping 10.10.1.100 # lb
ping 10.10.1.200 # dnsAll should respond successfully.
Invoke-RestMethod -Uri http://10.10.1.10:5000/ -Method GETExpected output:
Flask App is running! (DC1)
Invoke-RestMethod -Uri http://10.10.2.10:5000/ -Method GETExpected output:
Flask App is running! (DC2)
Invoke-RestMethod -Uri http://10.10.1.100/ -Method GETRun this multiple times to see the round-robin distribution:
for ($i=0; $i -lt 5; $i++) { Invoke-RestMethod -Uri http://10.10.1.100/ -Method GET }Expected output: A mix of DC1 and DC2 responses.
Invoke-RestMethod -Uri http://10.10.1.100/items -Method GETExpected output: [] (empty array, no data yet).
Invoke-RestMethod -Uri http://10.10.1.100/items -Method POST -Headers @{"Content-Type"="application/json"} -Body '{"id": 42}'Expected output:
{"status":"inserted","id":42}Invoke-RestMethod -Uri http://10.10.1.100/items -Method GETExpected output: [42]
Invoke-RestMethod -Uri http://10.10.2.10:5000/items -Method GETExpected output: [42] (data is replicated to db2).
This test promotes the replica (db2) to primary and reconfigures db1 as the new replica.
vagrant ssh db1
sudo -u postgres psql -c "SELECT pg_switch_wal();"
sudo systemctl stop postgresql
exit
vagrant ssh db2
sudo pg_ctl promote -D /var/lib/postgresql/16/main
exit
# Reconfigure db1 as replica of db2
vagrant ssh db1
sudo rm -rf /var/lib/postgresql/16/main/*
sudo -u postgres PGPASSWORD='replicator_password' pg_basebackup \
-h 10.10.2.20 -U replicator \
-D /var/lib/postgresql/16/main -Fp -Xs -R -v
sudo -u postgres touch /var/lib/postgresql/16/main/standby.signal
sudo systemctl start postgresqlThis test simulates the complete loss of DC1 and verifies that the system continues to work using DC2.
# Shut down DC1
vagrant halt db1 app1
# Promote db2 to primary
vagrant ssh db2
sudo pg_ctl promote -D /var/lib/postgresql/16/main
exit
# Verify application still works
curl http://10.10.2.10:5000/items
# To recover DC1, reincorporate it as a replica of db2
vagrant up db1
vagrant ssh db1
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/16/main/*
sudo -u postgres PGPASSWORD='replicator_password' pg_basebackup \
-h 10.10.2.20 -U replicator \
-D /var/lib/postgresql/16/main -Fp -Xs -R -v
sudo -u postgres touch /var/lib/postgresql/16/main/standby.signal
sudo systemctl start postgresqlTo access services from your host, add these entries to your hosts file:
Windows: C:\Windows\System32\drivers\etc\hosts
macOS / Linux: /etc/hosts
10.10.1.100 lb.lab.local
10.10.1.20 db.lab.local
After adding the entries:
curl http://lb.lab.local/items2dc-ha-lab/
βββ README.md # This file
βββ LICENSE # MIT License
βββ .gitignore # Git ignore rules
βββ Vagrantfile # VM definitions
βββ images/
β βββ arch_whitebg.png # Architecture diagram (white background)
βββ provision/
β βββ db1.sh # Primary DB setup
β βββ db2.sh # Replica DB setup
β βββ app.sh # Flask app setup
β βββ lb.sh # HAProxy setup
β βββ dns.sh # Bind9 DNS setup
βββ app/
βββ app.py # Flask application
βββ requirements.txt # Python dependencies
Problem: Vagrant gets stuck at SSH auth method: private key.
Solution: Add this to your Vagrantfile:
vb.customize ["modifyvm", :id, "--cableconnected1", "on"]- Workaround during
vagrant up: If the VM gets stuck, open the VirtualBox GUI and bring the VM window to the foreground (click on it). This often "wakes up" the boot process and allows Vagrant to continue.
Problem: Error VERR_INTNET_FLT_IF_NOT_FOUND.
Solution:
- Open VirtualBox β File β Tools β Network Manager.
- Go to Host-only Networks.
- Ensure you have two adapters:
10.10.1.1/24(DC1)10.10.2.1/24(DC2)
- Disable DHCP on both.
Problem: VirtualBox fails to start VMs.
Solution: Disable Hyper-V:
bcdedit /set hypervisorlaunchtype offProblem: psycopg2.OperationalError: connection refused.
Solution: Ensure PostgreSQL is listening on all interfaces:
sudo sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" /etc/postgresql/16/main/postgresql.conf
sudo systemctl restart postgresqlThe first run downloads the Ubuntu base box (~700 MB) and installs all dependencies. Subsequent runs are much faster.
Yes, but you may need to reduce the memory allocated to each VM in the Vagrantfile. Edit vb.memory values to lower values (e.g., 1024 for databases, 512 for apps).
vagrant haltTo destroy them completely:
vagrant destroy -fEdit the version in provision/db1.sh and provision/db2.sh. Change postgresql-16 to postgresql-15 or postgresql-14.
You can access it directly via http://10.10.2.10:5000, provided your host has a route to the 10.10.2.0/24 network.
Yes. Vagrant and VirtualBox work on macOS. The scripts are Bash-compatible.
vagrant destroy -f
vagrant upThis project is a personal learning lab and is not currently open to external contributions. However, you are welcome to:
- β Star the repository if you find it useful.
- π΄ Fork it and use it for your own learning.
- π Report issues if you find any bugs or problems.
- π Suggest improvements via GitHub Issues.
π‘ Note: This lab is designed to be a reproducible environment for learning high availability concepts. If you have ideas for improvements, feel free to open an issue to discuss them.
This project is licensed under the MIT License - see the LICENSE file for details.
Rafael JimΓ©nez PΓ©rez
- GitHub: @rafajimenezdev
- Email: rafajimenezdev@gmail.com
Built with β€οΈ for the infrastructure community
