New to KubeDB? Please start here.
MariaDB Database Migration
This guide will show you how to use KubeDB Migration to migrate an existing MariaDB database — such as one running on AWS RDS or any external instance — entirely into a KubeDB-managed MariaDB with minimal downtime.
Before You Begin
At first, you need to have a Kubernetes cluster, and the
kubectlcommand-line tool must be configured to communicate with your cluster.Install
KubeDBoperator with the kubedb-courier operator enabled in your cluster following the steps here.The source
MariaDBinstance must be network-reachable from within your Kubernetes cluster.The source
MariaDBinstance must have binary logging enabled withbinlog_format=ROWandbinlog_row_image=FULL. The database user provided for migration must have replication privileges.You should be familiar with the following
KubeDBconcepts:
To keep everything isolated, we are going to use a separate namespace called demo throughout this tutorial.
$ kubectl create ns demo
namespace/demo created
Prepare Source Database
We will use an AWS RDS MariaDB instance as the source. Connect to it as the admin user to verify the prerequisites and set up the migration user and test data.
Configuring your source instance.
Self-hosted MariaDB
Add the following to your my.cnf and restart MariaDB:
[mysqld]
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
Then grant the required privileges to the migration user:
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '<migration-user>'@'%';
FLUSH PRIVILEGES;
AWS RDS MariaDB
Enable Automated Backups on the instance (this activates binary logging). Set binlog_format = ROW and binlog_row_image = FULL in an RDS Parameter Group. Then grant replication privileges via SQL as shown above.
Azure Database for MariaDB
Binary logging is enabled automatically when backups are on. Set binlog_format and binlog_row_image under Server Parameters in the Azure Portal. Then grant replication privileges via SQL as shown above.
See the official MariaDB Binary Log docs for more details.
Verify prerequisites
$ mysql -h <rds-endpoint>.rds.amazonaws.com -u admin -p
-- Verify binary logging is enabled
SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
-- Verify binlog format and row image
SHOW VARIABLES LIKE 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
SHOW VARIABLES LIKE 'binlog_row_image';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| binlog_row_image | FULL |
+------------------+-------+
Create a dedicated migration user
Create a dedicated user with the minimum required privileges:
CREATE USER 'migrator'@'%' IDENTIFIED BY '<password>';
-- For CDC (binlog streaming)
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'migrator'@'%';
-- For schema migration (mariadb-dump) and snapshot
GRANT SELECT, SHOW DATABASES, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS ON *.* TO 'migrator'@'%';
FLUSH PRIVILEGES;
-- Verify
SHOW GRANTS FOR 'migrator'@'%';
The migrator user is referenced in the Kubernetes secret and AppBinding for the rest of this guide.
Create table and seed data
CREATE DATABASE shop;
USE shop;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
product VARCHAR(100) NOT NULL,
quantity INT NOT NULL DEFAULT 1,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO orders (customer_name, product, quantity, status) VALUES
('Alice', 'Laptop', 1, 'shipped'),
('Bob', 'Headphones', 2, 'pending'),
('Carol', 'Keyboard', 3, 'delivered');
SELECT * FROM orders;
+----+---------------+------------+----------+-----------+---------------------+
| id | customer_name | product | quantity | status | created_at |
+----+---------------+------------+----------+-----------+---------------------+
| 1 | Alice | Laptop | 1 | shipped | 2026-06-29 08:00:00 |
| 2 | Bob | Headphones | 2 | pending | 2026-06-29 08:00:01 |
| 3 | Carol | Keyboard | 3 | delivered | 2026-06-29 08:00:02 |
+----+---------------+------------+----------+-----------+---------------------+
3 rows in set (0.00 sec)
Prepare Source Connection Information
First, create an authentication secret using the migrator user credentials:
$ kubectl create secret generic source-mariadb-auth -n demo \
--type=kubernetes.io/basic-auth \
--from-literal=username=migrator \
--from-literal=password=<password>
If your database has TLS enabled, create a secret with the CA certificate:
kubectl create secret generic ca-secret \
--from-file=ca.crt=$CERT_PATH/ca.crt \
--namespace=demo
Note: For mTLS, also include the client certificate and key by appending
--from-file=tls.crt=$CERT_PATH/tls.crt--from-file=tls.key=$CERT_PATH/tls.key
to the command above.
Now create an AppBinding with the necessary information. The kubedb-courier operator reads the source MariaDB connection information from this AppBinding CR. Use the following YAML to create your AppBinding:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
metadata:
name: source-mariadb
namespace: demo
spec:
type: mariadb
version: "10.5.23"
clientConfig:
url: "mariadb://<rds-endpoint>.rds.amazonaws.com:3306"
secret:
name: source-mariadb-auth
tlsSecret: # omit if TLS is disabled
name: ca-secret
Here,
spec.clientConfig.urlis the connection URL of the source MariaDB instance.spec.secret.nameis the reference to the secret we created earlier, containing the MariaDB authentication information.
For a
KubeDB-managed database, anAppBindingis created by default. So there is no need to create one for the target database.
Create Target MariaDB Database
KubeDB implements a MariaDB CRD to define the specification of a MariaDB database. Follow the MariaDB object to create the target database.
apiVersion: kubedb.com/v1
kind: MariaDB
metadata:
name: target-mariadb
namespace: demo
spec:
version: "10.5.23"
storageType: Durable
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
deletionPolicy: WipeOut
$ kubectl create -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mariadb/migration/target-mariadb.yaml
mariadb.kubedb.com/target-mariadb created
Note: Adjust the
resources.requests.storagebased on the source database size.
Wait until target-mariadb has status Ready.
Apply Migration CR
To migrate the database we have to create a Migration CR. Below is the YAML of the Migration CR that we are going to create:
apiVersion: courier.kubedb.com/v1alpha1
kind: Migration
metadata:
name: mariadb-migrate
namespace: demo
spec:
source:
mariadb:
connectionInfo:
appBinding:
name: source-mariadb
namespace: demo
dbName: "mysql"
maxConnections: 100
schema:
enabled: true
database:
- shop
excludeDatabase: []
snapshot:
enabled: true
pipeline:
workers: 3
sinkers: 4
buffer: 12
write_batch_size: 200
read_batch_size: 1000
streaming:
enabled: true
target:
mariadb:
connectionInfo:
appBinding:
name: target-mariadb
namespace: demo
dbName: "mysql"
maxConnections: 100
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mariadb/migration/mariadb-migrate.yaml
migration.courier.kubedb.com/mariadb-migrate created
Here we scope the migration to the shop database (schema.database: [shop]), enable both the bulk snapshot and CDC streaming phases, and cap connections at 100 on each side. For a full description of every field, see the Migration CRD reference.
Watch Migration Progress
Let’s wait for the LAG to reach near zero. Run the following command to watch Migration CR:
Every 2.0s: kubectl get migration -n demo
NAME PHASE DBTYPE STAGE LAG PROGRESS AGE
mariadb-migrate Running mariadb Streaming 0B 100% 4h36m
Verify initial snapshot on target
Once the migration reaches the Streaming stage, exec into the KubeDB target pod and confirm all seed rows were copied over:
$ kubectl exec -it -n demo target-mariadb-0 -- mysql -u root -p<root-password>
USE shop;
SELECT * FROM orders;
+----+---------------+------------+----------+-----------+---------------------+
| id | customer_name | product | quantity | status | created_at |
+----+---------------+------------+----------+-----------+---------------------+
| 1 | Alice | Laptop | 1 | shipped | 2026-06-29 08:00:00 |
| 2 | Bob | Headphones | 2 | pending | 2026-06-29 08:00:01 |
| 3 | Carol | Keyboard | 3 | delivered | 2026-06-29 08:00:02 |
+----+---------------+------------+----------+-----------+---------------------+
3 rows in set (0.00 sec)
Test live CDC streaming
With the migration still running, connect to the source RDS instance and run some DML:
$ mysql -h <rds-endpoint>.rds.amazonaws.com -u migrator -p
-- Insert a new order
INSERT INTO shop.orders (customer_name, product, quantity, status)
VALUES ('Dave', 'Mouse', 1, 'pending');
-- Mark Bob's headphones as delivered
UPDATE shop.orders SET status = 'delivered' WHERE id = 2;
-- Remove the already-shipped laptop order
DELETE FROM shop.orders WHERE id = 1;
Wait a few seconds for the binlog events to propagate, then re-query the target:
USE shop;
SELECT * FROM orders;
+----+---------------+------------+----------+-----------+---------------------+
| id | customer_name | product | quantity | status | created_at |
+----+---------------+------------+----------+-----------+---------------------+
| 2 | Bob | Headphones | 2 | delivered | 2026-06-29 08:00:01 |
| 3 | Carol | Keyboard | 3 | delivered | 2026-06-29 08:00:02 |
| 4 | Dave | Mouse | 1 | pending | 2026-06-29 08:10:00 |
+----+---------------+------------+----------+-----------+---------------------+
3 rows in set (0.00 sec)
The INSERT, UPDATE, and DELETE are all reflected on the target — CDC streaming is working correctly.
Cutover
Once the LAG drops to near zero, stop all writes to the source database. Wait until the LAG reaches exactly zero — at that point both databases are fully in sync.
Now delete the Migration CR to stop the migration process:
$ kubectl delete migration -n demo mariadb-migrate
migration.courier.kubedb.com "mariadb-migrate" deleted
Finally, update your application’s connection string to point to the target KubeDB-managed MariaDB database. The migration is complete.































