New to KubeDB? Please start here.
MSSQL Server Database Migration
This guide will show you how to use KubeDB Migration to migrate an existing MSSQL Server database — such as one running on AWS RDS for SQL Server or any external instance — entirely into a KubeDB-managed MSSQLServer 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
MSSQL Serverinstance must be network-reachable from within your Kubernetes cluster.The source
MSSQL Serverdatabase should have a login withsysadminprivileges available for the migration. On a self-hosted instance this is typically the built-insalogin; on a managed instance (AWS RDS, Azure SQL Managed Instance) it’s the master/administrator login you configured at creation time — see the provider-specific notes below.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 for SQL Server instance as the source. Below is how to verify the prerequisites, set up the migration user, and insert test data.
Configuring your source instance.
AWS RDS for SQL Server
AWS RDS for SQL Server provides a fully managed instance. Refer to the AWS documentation for creating an RDS SQL Server instance. For migration, you need:
- The RDS endpoint (e.g.,
mydb.xxxx.us-east-1.rds.amazonaws.com) - The master user name and password you configured when creating the RDS instance — RDS does not create a default
salogin, and the master user is not a member ofsysadmin(it hasdb_owner-equivalent privileges instead). - CDC enablement is provider-specific on RDS: use the RDS procedure
EXEC msdb.dbo.rds_cdc_enable_db '<database>'instead of the standardsys.sp_cdc_enable_db. Enable it yourself before starting the migration and setspec.source.streaming.autoEnableCDC: falseon theMSSQLServerMigrationCR. - The source must be accessible from your Kubernetes cluster (configure the VPC security group to allow inbound connections on port
1433from your cluster’s CIDR range)
Self-hosted SQL Server
For a self-hosted SQL Server instance, make sure TCP/IP is enabled on port 1433 and the instance is accessible from your Kubernetes cluster. Use the sa account or create a login with sysadmin server role.
Azure SQL Managed Instance
Azure SQL Managed Instance works similarly with a public or private endpoint. Use the administrator login you selected when the instance was deployed — Managed Instance does not create a default sa login. This login is a member of sysadmin, so the standard sys.sp_cdc_enable_db procedure and autoEnableCDC: true both apply.
See the official AWS RDS SQL Server docs for more details.
Create a database and seed data
Connect to the source instance using sqlcmd or your preferred SQL client:
sqlcmd -S <rds-endpoint> -U <source-login> -P '<password>' -C
Note:
-Ctellssqlcmdto trust the server certificate without validating it — convenient for this tutorial. For production connections, validate against your CA instead of bypassing certificate checks.
-- Create a test database
CREATE DATABASE RestaurantMigrationDB;
GO
-- Switch to the new database
USE RestaurantMigrationDB;
GO
-- Create a table
CREATE TABLE Customers (
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(100),
City NVARCHAR(50),
CreatedAt DATETIME2 DEFAULT GETDATE()
);
GO
-- Insert sample data
INSERT INTO Customers (Name, Email, City) VALUES
('Alice', 'alice@example.com', 'NYC'),
('Bob', 'bob@example.com', 'SF'),
('Carol', 'carol@example.com', 'Chicago');
GO
-- Verify
SELECT * FROM Customers;
GO
Expected output (CreatedAt reflects GETDATE() at insert time, so actual values will differ per run):
CustomerID Name Email City CreatedAt
----------- ------ ------------------ -------- -----------------------
1 Alice alice@example.com NYC 2026-07-10 12:00:00.000
2 Bob bob@example.com SF 2026-07-10 12:00:00.000
3 Carol carol@example.com Chicago 2026-07-10 12:00:00.000
Prepare Source Connection Information
First, create an authentication secret using the source login’s credentials:
kubectl create secret generic source-mssql-auth -n demo \
--type=kubernetes.io/basic-auth \
--from-literal=username=<source-login> \
--from-literal=password=<password>
If your source database has TLS enabled (RDS and Azure SQL use TLS by default), create a secret with the CA certificate:
kubectl create secret generic ca-secret \
--from-file=ca.crt=<path-to-ca-cert> \
--namespace=demo
Note: For mTLS, also include the client certificate and key by appending
--from-file=tls.crt=<path-to-client-cert>--from-file=tls.key=<path-to-client-key>
to the command above.
Now create an AppBinding with the necessary information. The kubedb-courier operator reads the source MSSQL Server connection information from this AppBinding CR. Use the following YAML to create your AppBinding:
apiVersion: appcatalog.appscode.com/v1alpha1
kind: AppBinding
metadata:
name: mssqlserver-source
namespace: demo
spec:
type: kubedb.com/mssqlserver
clientConfig:
url: "sqlserver://<rds-endpoint>:1433"
secret:
name: source-mssql-auth
tlsSecret: # omit if TLS is disabled
name: ca-secret
Here,
spec.typeis set tokubedb.com/mssqlserver.spec.clientConfig.urlis the connection URL of the source MSSQL Server instance in the formatsqlserver://<host>:<port>.spec.secret.namereferences the secret we created earlier, containing the MSSQL authentication information.spec.tlsSecret.namereferences the TLS CA certificate secret (optional — omit if TLS is not required).
For a
KubeDB-managed database, anAppBindingis created by default. So there is no need to create one for the target database.
Create a TLS Certificate Issuer (for the Target)
KubeDB-managed MSSQL Servers use TLS certificates by default. If you don’t have a cert-manager issuer in the demo namespace, create one:
# Create a self-signed CA
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ca.key -out ca.crt \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-subj "/CN=mssqlserver-ca"
# Create a TLS secret
kubectl create secret tls mssqlserver-ca \
--cert=ca.crt --key=ca.key \
-n demo
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: mssqlserver-ca-issuer
namespace: demo
spec:
ca:
secretName: mssqlserver-ca
kubectl apply -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mssqlserver/migration/source-issuer.yaml
Note: This step is only required if you don’t already have a cert-manager issuer configured in the namespace. KubeDB uses cert-manager to issue TLS certificates for the MSSQL Server pods.
Create Target MSSQL Server Database
KubeDB implements a MSSQLServer CRD to define the specification of an MSSQL Server database. Use the following MSSQLServer object to create the target database.
apiVersion: kubedb.com/v1alpha2
kind: MSSQLServer
metadata:
name: mssqlserver-standalone
namespace: demo
spec:
version: "2022-cu12"
replicas: 1
storageType: Durable
tls:
issuerRef:
name: mssqlserver-ca-issuer
kind: Issuer
apiGroup: "cert-manager.io"
clientTLS: false
podTemplate:
spec:
containers:
- name: mssql
env:
- name: ACCEPT_EULA
value: "Y"
- name: MSSQL_PID
value: Developer # for testing only; use a licensed edition in production
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/mssqlserver/migration/target-mssqlserver.yaml
mssqlserver.kubedb.com/mssqlserver-standalone created
Note: Adjust the
resources.requests.storagebased on the source database size.
Wait until mssqlserver-standalone has status Ready.
$ kubectl get mssqlserver -n demo
NAME VERSION STATUS AGE
mssqlserver-standalone 2022-cu12 Ready 5m
Apply MSSQLServerMigration CR
To migrate the database we have to create an MSSQLServerMigration CR. KubeDB uses SqlPackage for schema migration and built-in CDC for streaming changes. Below is the YAML of the MSSQLServerMigration CR:
apiVersion: courier.kubedb.com/v1alpha1
kind: MSSQLServerMigration
metadata:
name: mssqlserver-migration
namespace: demo
spec:
source:
connectionInfo:
appBinding:
name: mssqlserver-source
namespace: demo
database: master
schema:
enabled: true
database:
- RestaurantMigrationDB
snapshot:
enabled: true
pipeline:
workers: 3
sinkers: 5
buffer: 16
read_batch_size: 1000
write_batch_size: 100
streaming:
enabled: true
autoEnableCDC: true
batchSize: 1000
target:
connectionInfo:
appBinding:
name: mssqlserver-standalone
namespace: demo
database: master
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.6.19/docs/examples/mssqlserver/migration/mssqlserver-migration.yaml
mssqlservermigration.courier.kubedb.com/mssqlserver-migration created
Here,
spec.source / spec.target — connectionInfo:
appBinding.name/appBinding.namespace— references theAppBindingfor the source or target MSSQL Server instance.database— the database used as the initial connection entry point (typicallymaster).
spec.source.schema — schema migration:
enabled: true— enables the schema migration phase.database— list of databases to migrate. The schema (tables, indexes, stored procedures, etc.) is copied from source to target.
spec.source.snapshot — bulk data copy:
enabled: true— enables the snapshot phase.pipeline.workers— number of parallel reader workers (default: 3).pipeline.sinkers— number of parallel writer workers (default: 5).
spec.source.streaming — CDC change streaming:
enabled: true— enables the streaming phase.autoEnableCDC: true— automatically enables CDC on the source database and tables.
For a full description of every field, see the MSSQLServerMigration CRD reference.
Watch Migration Progress
Let’s wait for the Migration to finish the schema and snapshot phases and enter the streaming phase. Run the following command to watch MSSQLServerMigration CR:
watch kubectl get mssqlservermigrations -n demo
During the Schema stage:
NAME PHASE STAGE LAG PROGRESS AGE
mssqlserver-migration Running Schema 0.0 12s
During the Snapshot stage, you’ll see progress advancing to 100%:
NAME PHASE STAGE LAG PROGRESS AGE
mssqlserver-migration Running Snapshot 45.3 45s
When the Streaming stage begins with LAG at zero, both databases are fully in sync:
NAME PHASE STAGE LAG PROGRESS AGE
mssqlserver-migration Running Streaming 0 100.0 2m
View detailed progress via pod logs
You can also see stage-wise progress and per-database details by checking the migration pod logs:
kubectl logs -n demo migrator-<migration-pod-name>
Example output during the snapshot stage:
2026-07-10T12:00:30.632Z INFO mssqlserver/mssqlserver.go:144 Starting snapshot for database {"database": "RestaurantMigrationDB"}
2026-07-10T12:00:45.123Z INFO mssqlserver/mssqlserver.go:148 Completed snapshot for database {"database": "RestaurantMigrationDB"}
Example output during streaming — showing per-database CDC lag:
2026-07-10T12:01:30.632Z INFO mssqlserver/mssqlserver.go:329 Starting CDC streaming for database {"database": "RestaurantMigrationDB"}
Verify initial snapshot on target
Store the target MSSQL Server’s sa password in a shell variable, so it’s never printed to your terminal or shell history:
SA_PASSWORD=$(kubectl get secret -n demo mssqlserver-standalone-auth -o jsonpath='{.data.password}' | base64 -d)
Once the migration reaches the Streaming stage, exec into the KubeDB target pod and confirm all seed documents were copied over:
kubectl exec -it -n demo mssqlserver-standalone-0 -- /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$SA_PASSWORD" -C -Q "SELECT * FROM RestaurantMigrationDB.dbo.Customers"
Expected output (CreatedAt reflects GETDATE() at insert time, so actual values will differ per run):
CustomerID Name Email City CreatedAt
----------- ------ ------------------ -------- -----------------------
1 Alice alice@example.com NYC 2026-07-10 12:00:00.000
2 Bob bob@example.com SF 2026-07-10 12:00:00.000
3 Carol carol@example.com Chicago 2026-07-10 12:00:00.000
Test live CDC streaming
With the migration still running, connect to the source AWS RDS instance and run some DML:
sqlcmd -S <rds-endpoint> -U <source-login> -P '<password>' -C
USE RestaurantMigrationDB;
GO
-- Insert a new customer
INSERT INTO Customers (Name, Email, City) VALUES
('Dave', 'dave@example.com', 'Boston');
GO
-- Update Bob's city
UPDATE Customers SET City = 'Seattle' WHERE Name = 'Bob';
GO
-- Delete Alice
DELETE FROM Customers WHERE Name = 'Alice';
GO
Wait a few seconds for the CDC events to propagate, then re-query the target (reusing the $SA_PASSWORD variable set earlier):
kubectl exec -it -n demo mssqlserver-standalone-0 -- /opt/mssql-tools18/bin/sqlcmd \
-S localhost -U sa -P "$SA_PASSWORD" -C -Q "SELECT * FROM RestaurantMigrationDB.dbo.Customers"
Expected output (CreatedAt reflects GETDATE() at insert time, so actual values will differ per run):
CustomerID Name Email City CreatedAt
----------- ------ ------------------ -------- -----------------------
2 Bob bob@example.com Seattle 2026-07-10 12:00:00.000
3 Carol carol@example.com Chicago 2026-07-10 12:00:00.000
4 Dave dave@example.com Boston 2026-07-10 12:15:00.000
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 MSSQLServerMigration CR to stop the migration process:
$ kubectl delete mssqlservermigrations -n demo mssqlserver-migration
mssqlservermigration.courier.kubedb.com "mssqlserver-migration" deleted
Finally, update your application’s connection string to point to the target KubeDB-managed MSSQLServer database. The migration is complete.































