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 kubectl command-line tool must be configured to communicate with your cluster.

  • Install KubeDB operator with the kubedb-courier operator enabled in your cluster following the steps here.

  • The source MSSQL Server instance must be network-reachable from within your Kubernetes cluster.

  • The source MSSQL Server database should have a login with sysadmin privileges available for the migration. On a self-hosted instance this is typically the built-in sa login; 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 KubeDB concepts:

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 sa login, and the master user is not a member of sysadmin (it has db_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 standard sys.sp_cdc_enable_db. Enable it yourself before starting the migration and set spec.source.streaming.autoEnableCDC: false on the MSSQLServerMigration CR.
  • The source must be accessible from your Kubernetes cluster (configure the VPC security group to allow inbound connections on port 1433 from 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: -C tells sqlcmd to 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.type is set to kubedb.com/mssqlserver.
  • spec.clientConfig.url is the connection URL of the source MSSQL Server instance in the format sqlserver://<host>:<port>.
  • spec.secret.name references the secret we created earlier, containing the MSSQL authentication information.
  • spec.tlsSecret.name references the TLS CA certificate secret (optional — omit if TLS is not required).

For a KubeDB-managed database, an AppBinding is 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.storage based 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 the AppBinding for the source or target MSSQL Server instance.
  • database — the database used as the initial connection entry point (typically master).

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.