Summary
This article shows how to copy a SQL database that is in an availability group into Kubernetes and use Portworx to snapshot the databases in the pod to provide multiple copies for testing and development.
This article on SQL Server distributed availability groups initially appeared on Andrew Pruski’s blog. It has been republished with the author’s credit and consent.
I was talking with a colleague last week and they asked, “How can we copy a SQL database that is already in an availability group into Kubernetes and then provide copies of that database for development or testing?”
One of the great things about Kubernetes is the ability to quickly and easily spin up SQL instances, but getting databases, say from a production environment, into Kubernetes is an entirely different challenge.
If the production SQL instance is already in an availability group (which is fairly standard practice), then we can provision a distributed availability group to seed databases from the Windows AG into a pod in Kubernetes. And then from there, we can use Portworx® to snapshot the databases in the pod to provide multiple copies for testing and development.
So let’s run through the process!
Here is the existing Windows availability group:
Just a standard, two-node AG with one database already synchronized across the nodes. It’s that database we’re going to seed over to the pod running on the Kubernetes cluster using a distributed availability group.
Here is the Kubernetes cluster:
1 |
kubectl get nodes |
Four nodes, one control plane node, and three worker nodes.
To provide persistent storage for the SQL instance in the Kubernetes cluster, Portworx has been installed:
1 |
$PX_POD=$(kubectl get pods –l name=portworx –n portworx –o |
1 |
jsonpath=‘{.items[0].metadata.name}’) |
1 |
kubectl exec $PX_POD –n portworx — /opt/pwx/bin/pxctl status |
I won’t go through the installation of Portworx, but you can view the documentation here.
After Portworx is installed, a storage class can be created to provision persistent volumes dynamically for our SQL instance.
Here is the manifest for the storage class:
1 |
apiVersion: storage.k8s.io/v1 |
1 |
kind: StorageClass |
1 |
metadata: |
1 |
name: px–mssql–sc |
1 |
parameters: |
1 |
repl: “3” |
1 |
provisioner: pxd.portworx.com |
1 |
reclaimPolicy: Delete |
1 |
volumeBindingMode: Immediate |
Once we have the storage class, we can now deploy a statefulset running one SQL Server pod:
1 |
apiVersion: apps/v1 |
1 |
kind: StatefulSet |
1 |
metadata: |
1 |
name: mssql–statefulset |
1 |
spec: |
1 |
serviceName: “mssql” |
1 |
replicas: 1 |
1 |
podManagementPolicy: Parallel |
1 |
selector: |
1 |
matchLabels: |
1 |
name: mssql–pod |
1 |
template: |
1 |
metadata: |
1 |
labels: |
1 |
name: mssql–pod |
1 |
annotations: |
1 |
stork.libopenstorage.org/disableHyperconvergence: “true” |
1 |
spec: |
1 |
securityContext: |
1 |
fsGroup: 10001 |
1 |
hostAliases: |
1 |
– ip: “10.225.115.129” |
1 |
hostnames: |
1 |
– “z-ap-sql-10” |
1 |
containers: |
1 |
– name: mssql–container |
1 |
image: mcr.microsoft.com/mssql/server:2022–CU15–ubuntu–20.04 |
1 |
ports: |
1 |
– containerPort: 1433 |
1 |
name: mssql–port |
1 |
env: |
1 |
– name: MSSQL_PID |
1 |
value: “Developer” |
1 |
– name: ACCEPT_EULA |
1 |
value: “Y” |
1 |
– name: MSSQL_AGENT_ENABLED |
1 |
value: “1” |
1 |
– name: MSSQL_ENABLE_HADR |
1 |
value: “1” |
1 |
– name: MSSQL_SA_PASSWORD |
1 |
value: “Testing1122” |
1 |
volumeMounts: |
1 |
– name: sqlsystem |
1 |
mountPath: /var/opt/mssql |
1 |
volumeClaimTemplates: |
1 |
– metadata: |
1 |
name: sqlsystem |
1 |
spec: |
1 |
accessModes: |
1 |
– ReadWriteOnce |
1 |
resources: |
1 |
requests: |
1 |
storage: 1Gi |
1 |
storageClassName: px–mssql–sc |
This is pretty stripped down. No resources limits, tolerations, etc. It has one persistent volume for the system databases from the px-mssql-sc storage class.
One thing to note:
1 |
hostAliases: |
1 |
– ip: “10.225.115.129” |
1 |
hostnames: |
1 |
– “z-ap-sql-10” |
Here an entry in the pod’s hosts file is being created for the listener of the Windows availability group.
We can also view the persistent volume created in Portworx:
1 |
$PX_POD=$(kubectl get pods –l name=portworx –n portworx –o |
1 |
jsonpath=‘{.items[0].metadata.name}’) |
1 |
kubectl exec $PX_POD –n portworx — /opt/pwx/bin/pxctl volume list |
This is the volume that we will snapshot to provide clones of the database once it has been seeded over into the Kubernetes cluster using a distributed availability group.
The next thing to do is deploy two services: one so that we can connect to the SQL instance (on port 1433) and one for the AG (port 5022):
1 |
apiVersion: v1 |
1 |
kind: Service |
1 |
metadata: |
1 |
name: mssql–service |
1 |
spec: |
1 |
ports: |
1 |
– name: mssql–ports |
1 |
port: 1433 |
1 |
targetPort: 1433 |
1 |
selector: |
1 |
name: mssql–pod |
1 |
type: LoadBalancer |
1 |
—– |
1 |
apiVersion: v1 |
1 |
kind: Service |
1 |
metadata: |
1 |
name: mssql–ha–service |
1 |
spec: |
1 |
ports: |
1 |
– name: mssql–ha–ports |
1 |
port: 5022 |
1 |
targetPort: 5022 |
1 |
selector: |
1 |
name: mssql–pod |
1 |
type: LoadBalancer |
Note: We could use just one service with multiple ports configured, but I’m keeping them separate here to try and keep things as clear as possible.
Check that everything looks OK:
1 |
kubectl get all |
Now, we need to create a master key, login, and user in all instances:
1 |
CREATE MASTER KEY ENCRYPTION BY PASSWORD = ‘<C0m9L3xP@55w0rd!>’; |
1 |
CREATE LOGIN [dbm_login] WITH PASSWORD = ‘<C0m9L3xP@55w0rd!>’; |
1 |
CREATE USER dbm_user FOR LOGIN dbm_login; |
Then, create a certificate in the SQL instance in the pod:
1 |
CREATE CERTIFICATE dbm_certificate WITH SUBJECT = ‘Mirroring_certificate’, EXPIRY_DATE = ‘20301031’ |
Back up that certificate:
1 |
BACKUP CERTIFICATE dbm_certificate |
1 |
TO FILE = ‘/var/opt/mssql/data/dbm_certificate.cer’ |
1 |
WITH PRIVATE KEY ( |
1 |
FILE = ‘/var/opt/mssql/data/dbm_certificate.pvk’, |
1 |
ENCRYPTION BY PASSWORD = ‘<C0m9L3xP@55w0rd!>’ |
1 |
); |
Copy the certificate locally:
1 |
kubectl cp mssql–statefulset–0:var/opt/mssql/data/dbm_certificate.cer ./dbm_certificate.cer |
1 |
kubectl cp mssql–statefulset–0:var/opt/mssql/data/dbm_certificate.pvk ./dbm_certificate.pvk |
And then copy the files to the Windows boxes:
1 |
Copy–Item dbm_certificate.cer \\z–ap–sql–02\E$\SQLBackup1\ –Force |
1 |
Copy–Item dbm_certificate.pvk \\z–ap–sql–02\E$\SQLBackup1\ –Force |
1 |
Copy–Item dbm_certificate.cer \\z–ap–sql–03\E$\SQLBackup1\ –Force |
1 |
Copy–Item dbm_certificate.pvk \\z–ap–sql–03\E$\SQLBackup1\ –Force |
Once the files are on the Windows boxes, we can create the certificate in each Windows SQL instance:
1 |
CREATE CERTIFICATE dbm_certificate |
1 |
AUTHORIZATION dbm_user |
1 |
FROM FILE = ‘E:\SQLBackup1\dbm_certificate.cer’ |
1 |
WITH PRIVATE KEY ( |
1 |
FILE = ‘E:\SQLBackup1\dbm_certificate.pvk’, |
1 |
DECRYPTION BY PASSWORD = ‘<C0m9L3xP@55w0rd!>’ |
1 |
) |
OK, great! Now we need to create a mirroring endpoint in the SQL instance in the pod and grant connect to the login created earlier:
1 |
CREATE ENDPOINT [Hadr_endpoint] |
1 |
STATE = STARTED |
1 |
AS TCP ( |
1 |
LISTENER_PORT = 5022, |
1 |
LISTENER_IP = ALL) |
1 |
FOR DATA_MIRRORING ( |
1 |
ROLE = ALL, |
1 |
AUTHENTICATION = WINDOWS CERTIFICATE [dbm_certificate], |
1 |
ENCRYPTION = REQUIRED ALGORITHM AES |
1 |
); |
1 |
ALTER ENDPOINT [Hadr_endpoint] STATE = STARTED; |
1 |
GRANT CONNECT ON ENDPOINT::[Hadr_endpoint] TO [dbm_login]; |
There are already endpoints in the Windows instances, but we need to update them to use the certificate for authentication:
1 |
ALTER ENDPOINT [Hadr_endpoint] |
1 |
STATE = STARTED |
1 |
AS TCP ( |
1 |
LISTENER_PORT = 5022, |
1 |
LISTENER_IP = ALL) |
1 |
FOR DATABASE_MIRRORING ( |
1 |
AUTHENTICATION = WINDOWS CERTIFICATE [dbm_certificate] |
1 |
, ENCRYPTION = REQUIRED ALGORITHM AES |
1 |
); |
1 |
GRANT CONNECT ON ENDPOINT::[Hadr_endpoint] TO [dbm_login]; |
Now we can create a one-node clusterless availability group in the SQL instance in the pod:
1 |
CREATE AVAILABILITY GROUP [AG2] |
1 |
WITH (CLUSTER_TYPE=NONE) |
1 |
FOR |
1 |
REPLICA ON |
1 |
‘mssql-statefulset-0’ WITH |
1 |
( |
1 |
ENDPOINT_URL = ‘TCP://mssql-statefulset-0.com:5022’, |
1 |
FAILOVER_MODE = MANUAL |
1 |
,AVAILABILITY_MODE = SYNCHRONOUS_COMMIT |
1 |
,BACKUP_PRIORITY = 50 |
1 |
,SEEDING_MODE = AUTOMATIC |
1 |
,SECONDARY_ROLE(ALLOW_CONNECTIONS = NO) |
1 |
) |
No listener here; we’re going to use the mssql-ha-service as the endpoint for the distributed availability group.
OK, so on the primary node of the Windows availability group, we can create the distributed availability group:
1 |
CREATE AVAILABILITY GROUP [DistributedAG] |
1 |
WITH (DISTRIBUTED) |
1 |
AVAILABILITY GROUP ON |
1 |
‘AG1’ WITH |
1 |
( |
1 |
LISTENER_URL = ‘tcp://Z-AP-SQL-10:5022’, |
1 |
AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT, |
1 |
FAILOVER_MODE = MANUAL, |
1 |
SEEDING_MODE = AUTOMATIC |
1 |
), |
1 |
‘AG2’ WITH |
1 |
( |
1 |
LISTENER_URL = ‘tcp://10.225.115.131:5022’, |
1 |
AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT, |
1 |
FAILOVER_MODE = MANUAL, |
1 |
SEEDING_MODE = AUTOMATIC |
1 |
); |
We could use a host file entry for the URL in AG2, but here, we’ll just use the IP address of the mssql-ha-service.
OK, nearly there! We now have to join the availability group in the SQL instance in the pod:
1 |
ALTER AVAILABILITY GROUP [DistributedAG] |
1 |
JOIN |
1 |
AVAILABILITY GROUP ON |
1 |
‘AG1’ WITH |
1 |
( |
1 |
LISTENER_URL = ‘tcp://Z-AP-SQL-10:5022’, |
1 |
AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT, |
1 |
FAILOVER_MODE = MANUAL, |
1 |
SEEDING_MODE = AUTOMATIC |
1 |
), |
1 |
‘AG2’ WITH |
1 |
( |
1 |
LISTENER_URL = ‘tcp://10.225.115.131:5022’, |
1 |
AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT, |
1 |
FAILOVER_MODE = MANUAL, |
1 |
SEEDING_MODE = AUTOMATIC |
1 |
); |
And that should be it! If we now connect to the SQL instance in the pod…the database is there!
There it is! OK, one thing I haven’t gone through here is how to get auto-seeding working from Windows into a Linux SQL instance. As long as the database data and log files are located under the Windows SQL instance’s default data and log path, they will auto-seed to the Linux SQL instance’s default data and log paths.
Now that the database has been seeded over to the SQL instance running in the pod, we can use Portworx volume snapshots to provide multiple copies!
The first thing to set up is a VolumeSnapshotClass:
1 |
apiVersion: snapshot.storage.k8s.io/v1 |
1 |
kind: VolumeSnapshotClass |
1 |
metadata: |
1 |
name: px–snapshotclass |
1 |
annotations: |
1 |
snapshot.storage.kubernetes.io/is–default–class: “true” |
1 |
driver: pxd.portworx.com |
1 |
deletionPolicy: Delete |
This will allow us to take a snapshot of the persistent volume on which the SQL databases are hosted.
Once the VolumeSnapshotClass is created, we can take the snapshot using:
1 |
apiVersion: volumesnapshot.external–storage.k8s.io/v1 |
1 |
kind: VolumeSnapshot |
1 |
metadata: |
1 |
name: mssql–snapshot |
1 |
spec: |
1 |
persistentVolumeClaimName: sqlsystem–mssql–statefulset–0 |
Make sure that the persistentVolumeClaimName matches the persistent volume claim that was created for the statefulset the database was seeded into.
To check that Portworx created the snapshot:
1 |
$PX_POD=$(kubectl get pods –l name=portworx –n portworx –o |
1 |
jsonpath=‘{.items[0].metadata.name}’) |
1 |
kubectl exec $PX_POD –n portworx — /opt/pwx/bin/pxctl volume list |
And there we have the snapshot of the persistent volume that the database is on. Now we can create a new persistent volume claim from that snapshot and deploy another instance of SQL Server in a statefulset referencing that claim.
So, let’s create the persistent volume claim from the snapshot:
1 |
apiVersion: v1 |
1 |
kind: PersistentVolumeClaim |
1 |
metadata: |
1 |
name: mssql–clone |
1 |
annotations: |
1 |
snapshot.alpha.kubernetes.io/snapshot: mssql–snapshot |
1 |
spec: |
1 |
accessModes: |
1 |
– ReadWriteOnce |
1 |
storageClassName: stork–snapshot–sc |
1 |
resources: |
1 |
requests: |
1 |
storage: 1Gi |
This references the stork-snapshot-sc storageclass that is installed with Portworx and provides the snapshotting functionality.
Once that has been run, we can check the PVCs in the cluster:
1 |
kubectl get pvc |
And there it is! We can reference it when deploying another SQL Server statefulset.
The manifest for the new statefulset is:
1 |
apiVersion: apps/v1 |
1 |
kind: StatefulSet |
1 |
metadata: |
1 |
name: mssql–statefulset–snap |
1 |
spec: |
1 |
serviceName: “mssql-snap” |
1 |
replicas: 1 |
1 |
podManagementPolicy: Parallel |
1 |
selector: |
1 |
matchLabels: |
1 |
name: mssql–pod–snap |
1 |
template: |
1 |
metadata: |
1 |
labels: |
1 |
name: mssql–pod–snap |
1 |
annotations: |
1 |
stork.libopenstorage.org/disableHyperconvergence: “true” |
1 |
spec: |
1 |
securityContext: |
1 |
fsGroup: 10001 |
1 |
containers: |
1 |
– name: mssql–container–snap |
1 |
image: mcr.microsoft.com/mssql/server:2022–CU15–ubuntu–20.04 |
1 |
ports: |
1 |
– containerPort: 1433 |
1 |
name: mssql–port–snap |
1 |
env: |
1 |
– name: MSSQL_PID |
1 |
value: “Developer” |
1 |
– name: ACCEPT_EULA |
1 |
value: “Y” |
1 |
– name: MSSQL_AGENT_ENABLED |
1 |
value: “1” |
1 |
– name: MSSQL_ENABLE_HADR |
1 |
value: “1” |
1 |
– name: MSSQL_SA_PASSWORD |
1 |
value: “Testing1122” |
1 |
volumeMounts: |
1 |
– name: mssql–clone |
1 |
mountPath: /var/opt/mssql |
1 |
volumes: |
1 |
– name: mssql–clone |
1 |
persistentVolumeClaim: |
1 |
claimName: mssql–clone |
Confirm the new statefulset has deployed successfully:
1 |
kubectl get all |
To connect to the new SQL instance, we need a service:
1 |
apiVersion: v1 |
1 |
kind: Service |
1 |
metadata: |
1 |
name: mssql–service–snap |
1 |
spec: |
1 |
ports: |
1 |
– name: mssql–ports–snap |
1 |
port: 1433 |
1 |
targetPort: 1433 |
1 |
selector: |
1 |
name: mssql–pod–snap |
1 |
type: LoadBalancer |
And confirm the new service:
1 |
kubectl get services |
The final thing to do is to connect to the new instance of SQL in SSMS. As we cloned the volume with the system databases on, the instance thinks it is in the distributed availability group. So we need to delete that and the corresponding availability group:
1 |
ALTER DATABASE [AdventureWorks2019] SET HADR OFF; |
1 |
DROP AVAILABILITY GROUP [DistributedAG]; |
1 |
DROP AVAILABILITY GROUP [AG2]; |
1 |
RESTORE DATABASE [AdventureWorks2019] WITH RECOVERY; |
And now we have a copy of the database, online, in the new SQL instance:
If we need to provision further copies and don’t want to have to remove the AG configuration, we can now snapshot this SQL instance!
So that’s how to seed a database from a Windows-based SQL instance in an existing availability group to Kubernetes and provision clones of that database using Portworx.
Try Portworx
Test drive the cloud-native storage software platform for Kubernetes.