This project demonstrates how Debezium and Kafka can replicate MySQL changes from a primary virtual machine to a secondary virtual machine that is connected over the public internet. Version 2 hardens the stack with TLS on the Kafka listener and introduces an automated end-to-end check so you can measure eventual consistency across the WAN link.
- Primary VM – Hosts MySQL, Kafka, Kafka Connect (Debezium source) and the FastAPI application that inserts data into MySQL.
- Secondary VM – Hosts MySQL, a Kafka Connect JDBC sink, and the FastAPI application that reads the replicated data.
This section documents the standalone setup for the AIOD Catalogue nodes. Keep the
existing primary/ and secondary/ folders as the concrete targets for each node:
primary/ holds the source side and secondary/ holds the sink side. The same
scripts can be reused across different AIOD deployments, but the folder mapping
remains the same.
Use this side when you want to run the AIOD Catalogue MySQL instance as the source of truth.
- Configure the environment in
primary/.env. - Use
primary/scripts/register_mysql_connector.shto create or update the Debezium source connector. - Keep the topic prefix, database name, and history topic aligned with the catalogue database you want to track.
- Start the services from the
primary/folder withdocker compose up -d --build. - Validate the source connector with
primary/scripts/check_debezium_connect.sh.
Use this side when you want a second AIOD Catalogue node to consume the changes published by the primary.
- Configure the environment in
secondary/.env. - Use
secondary/scripts/check_sink_connector.shto validate the JDBC sink. - Point
BOOTSTRAP_SERVERSto the primary node Kafka endpoint. - Keep the sink topic filters aligned with the catalogue topic prefix.
- Start the services from the
secondary/folder withdocker compose up -d --build. - Validate replication by running
scripts/internet_consistency_check.shonce both nodes are up.
Prepare two Linux VMs (or physical servers) that can reach each other over the internet. Both machines need:
- Docker Engine
- Docker Compose plugin (the
docker composesubcommand) - Git
- OpenSSL (required for TLS assets)
- An outbound path on TCP/9093 from the secondary to the primary
🔐 Only expose TCP/9093 (Kafka over TLS) from the primary VM to the secondary VM's public IP. Kafka Connect (port 8083) and MySQL (3306) remain private to each host.
Run on each VM:
git clone https://github.com/aiondemand/aiod-mdc-sync.git
cd aiod-mdc-sync/aiod-mdc-syncYou will work with the primary/, secondary/, and app/ directories. All
instructions assume you remain inside aiod-mdc-sync/.
Copy the provided examples and edit them with identical credentials on both machines.
cd primary
cp .env.example .envEdit primary/.env and set:
MYSQL_ROOT_PASSWORD,MYSQL_DATABASE,MYSQL_USER,MYSQL_PASSWORD– Shared MySQL credentials used by both stacks.PRIMARY_PUB_IP– The public DNS name or IP address that the secondary VM can reach.KAFKA_SSL_PASSWORD– Password that protects the generated Kafka keystore and truststore (store this securely).
cd secondary
cp .env.example .envEdit secondary/.env and set:
- The same MySQL credentials you defined on the primary.
BOOTSTRAP_SERVERS–<PRIMARY_PUB_IP>:9093using the value from the primary.envfile.- Optionally adjust the TLS truststore path/password if you copy the artifacts to a different location.
Return to aiod-mdc-sync/ when you finish editing on each VM.
Run the helper to create a small certificate authority, broker certificate, and
client truststore. The script uses the variables from primary/.env by default.
Use PRIMARY_PUB_IP as the public domain, ej. kf-aiod-dev.iti.es
./scripts/generate_kafka_tls.sh kf-aiod-dev.iti.esThe artifacts are written to:
primary/secrets/– Broker keystore, truststore, and CA certificatesecondary/secrets/– Client truststore and CA certificate to copy to the secondary VM
Copy the contents of secondary/secrets/ to the secondary VM (for example with
scp) and place them under aiod-mdc-sync/secondary/secrets/. Keep the
password from KAFKA_SSL_PASSWORD handy—it becomes
CONNECT_SSL_TRUSTSTORE_PASSWORD in secondary/.env.
On the primary VM, restrict ingress so that only the secondary VM can reach
TCP/9093:
# Example using ufw
sudo ufw allow from <SECONDARY_PUBLIC_IP> to any port 9093 proto tcp
sudo ufw enableAll other Kafka ports remain internal to Docker. Confirm that the secondary VM can
reach the port via openssl s_client -connect <PRIMARY_PUB_IP>:9093 (it should show
an established TLS session signed by the generated CA).
Check that KAFKA_ADVERTISED_LISTENERS variable is using the current public port. In our project, we made a redirection from 50010 to 9093 and the following parameter had to be changed: KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,SSL://${PRIMARY_PUB_IP}:9093 50010.
For external (internet) access to the Kafka broker, configure NGnix to proxy the
TLS stream so clients can reach 9093 while the broker stays on the private IP.
This keeps the broker address stable and avoids exposing Docker networking
directly to the internet.
stream {
server {
listen 0.0.0.0:9093;
proxy_pass 10.151.16.160:9093;
ssl_preread on;
}
}Always start the primary side first so Kafka and Debezium are ready before the sink connects.
cd primary
docker compose up -d --buildOnce containers are running, validate the setup in this order (still on the primary VM):
../../scripts/verify_setup.sh– Confirms Docker/Compose, environment files, running containers, and the Kafka Connect REST API../scripts/register_mysql_connector.sh– Deploys the Debezium source connector. There are two key parameters to be configured: TABLES="${TABLES:-test_db.} . INCLUDE_SCHEMA_CHANGES="${INCLUDE_SCHEMA_CHANGES:-true}"./scripts/check_debezium_connect.sh --container db-connect --connector-name mysql-source --show-config --validate-running– Verifies the worker status. Add--check-topics --kafka-container db-kafkato confirm Kafka internal topics.
After the primary stack is healthy, move to the secondary VM:
cd secondary
docker compose up -d --buildThe sink connector container generates its own connect-distributed.properties
including TLS settings. Validate the stack with:
./scripts/check_sink_connector.sh --show-configAdd --verbose for raw connector status or --timeout <seconds> if the worker
needs longer to initialise.
From either VM (the machine just needs network access to both APIs), run the helper that creates a record on the primary API and waits for it to appear on the secondary API:
./scripts/internet_consistency_check.sh \
--primary http://<PRIMARY_PUB_IP>:8000 \
--secondary http://<SECONDARY_PUBLIC_OR_PRIVATE_IP>:8001 \
--timeout 180 --interval 5The script reports how many seconds it took for the item to replicate. A failure indicates connectivity or connector issues—check Docker logs on both VMs for more information.
aiod-mdc-sync/
├─ app/ # Shared FastAPI application (CRUD for items)
├─ primary/ # Debezium source stack (MySQL, Kafka, Kafka Connect, API)
│ └─ scripts/ # Helper scripts for the source connector
├─ secondary/ # JDBC sink stack (MySQL, Kafka Connect sink, API)
│ └─ scripts/ # Health checks for the sink connector
└─ scripts/
├─ generate_kafka_tls.sh # Generates TLS artifacts for Kafka/Connect
├─ internet_consistency_check.sh # Cross-VM replication smoke test
└─ verify_setup.sh # Primary-side environment validation
- Use
docker compose logs -f <service>to inspect individual container logs. openssl s_client -connect <PRIMARY_PUB_IP>:9093verifies the TLS certificate chain from outside Docker.secondary/scripts/check_sink_connector.sh --verboseshows connector errors if the sink cannot reach Kafka or MySQL.
Detailed test cases and validation notes are documented in TEST_CASES.md.
- Rerun
scripts/generate_kafka_tls.sh --forceif you need to regenerate certificates (remember to copy the new truststore to the secondary VM).