New to KubeDB? Please start here.
MySQL Database Migration
This guide will show you how to use KubeDB Migration to migrate an existing MySQL database — such as one running on AWS RDS or any external instance — entirely into a KubeDB-managed MySQL 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
MySQLinstance must be network-reachable from within your Kubernetes cluster.The source
MySQLinstance 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 MySQL 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 MySQL
Add the following to your my.cnf and restart MySQL:
[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 MySQL
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 MySQL
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.
Google Cloud SQL for MySQL
Enable binary logging under Backups in the Cloud Console, then set binlog_format = ROW and binlog_row_image = FULL under Database flags.
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 (mysqldump) 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-mysql-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 MySQL connection information from this AppBinding CR. Use the following YAML to create your AppBinding:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
metadata:
name: source-mysql
namespace: demo
spec:
type: mysql
version: "8.4.8"
clientConfig:
url: "mysql://<rds-endpoint>.rds.amazonaws.com:3306"
secret:
name: source-mysql-auth
tlsSecret: # omit if TLS is disabled
name: ca-secret
Here,
spec.clientConfig.urlis the connection URL of the source MySQL instance.spec.secret.nameis the reference to the secret we created earlier, containing the MySQL 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 MySQL Database
KubeDB implements a MySQL CRD to define the specification of a MySQL database. Follow the MySQL object to create the target database.
apiVersion: kubedb.com/v1
kind: MySQL
metadata:
name: target-mysql
namespace: demo
spec:
version: "8.4.8"
storageType: Durable
storage:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 30Gi
deletionPolicy: Delete
$ kubectl create -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mysql/migration/target-mysql.yaml
mysql.kubedb.com/target-mysql created
Note: Adjust the
resources.requests.storagebased on source database.
Wait untill target-mysql has status Ready
Apply Migration CR
To Migrate 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: mysql-migrate
namespace: demo
spec:
source:
mysql:
connectionInfo:
appBinding:
name: source-mysql
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:
mysql:
connectionInfo:
appBinding:
name: target-mysql
namespace: demo
dbName: "mysql"
maxConnections: 100
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mysql/migration/mysql-migrate.yaml
migration.courier.kubedb.com/mysql-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
mysql-migrate Running mysql 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-mysql-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 mysql-migrate
migration.courier.kubedb.com "mysql-migrate" deleted
Finally, update your application’s connection string to point to the target KubeDB-managed MySQL database. The migration is complete.































