Skip to main content

Bootstrap from a VolumeSnapshot

A standalone MSSQLInstance can start from a copy of another SQL Server's data instead of an empty data directory: a VolumeSnapshot of the other instance's data volume (or of one replica of an Availability Group). On copy-on-write storage (OpenEBS ZFS LocalPV, for example) the copy is made in seconds whatever the size, which is what database branching tools such as the Branching Plugin build on.

apiVersion: mssql.solanica.io/v1alpha1
kind: MSSQLInstance
metadata:
name: orders-pr-42
spec:
size: 1 # required with bootstrap
acceptEULA: true
saPasswordSecret: orders-pr-42-sa
storage:
size: 30Gi # at least the snapshot's restoreSize
storageClassName: openebs-zfspv
bootstrap:
volumeSnapshot:
name: orders-0-snap # a VolumeSnapshot of the source's mssql-data PVC
saPasswordSecret: orders-sa # MSSQL_SA_PASSWORD stored in the snapshot
# keepAgentJobs: false # the default: the copy's Agent jobs are disabled

spec.bootstrap can only be set at creation, needs size: 1, and can't be combined with storage.volumes: two snapshots of separate data and log volumes aren't taken at the same instant, so SQL Server could find a log that doesn't match its data files.

What the snapshot must be​

A crash-consistent snapshot of one volume holding all data and log files, the default single-PVC layout. SQL Server writes a commit to its log before it acknowledges it, so starting on such a snapshot is like starting after a power cut: it redoes committed work and rolls back what was in flight. No freeze is needed. A CHECKPOINT right before the snapshot shortens that recovery.

The copy sees every transaction committed before the snapshot, as of the moment it was cut. To branch an Availability Group, snapshot a synchronized secondary once its last_hardened_lsn has reached the primary's: it keeps the work off the primary.

What the operator does​

  1. Data. While the StatefulSet doesn't exist yet, the operator waits for the VolumeSnapshot to be ready, then creates mssql-data-<name>-0 from it. If that PVC already exists and wasn't made from the snapshot, the instance stays Initializing with a message: it never adopts other data.

  2. SA password. The snapshot's master database holds the source's SA password. The operator seeds <name>-sa-applied with the password from bootstrap.volumeSnapshot.saPasswordSecret, then its normal rotation switches SQL Server to the password in spec.saPasswordSecret: the copy doesn't keep the source's password.

  3. Fenced start. The copied master still describes the source: its Availability Groups, endpoints and certificates, backup credentials, Agent jobs and server name. Started as is, a copy of an AG replica rejoins the source's Availability Group as that replica and disrupts it. So the pods first start fenced:

    • a NetworkPolicy <name>-fence denies them all egress, and all ingress except from the operator's namespace (also keeping clients off a copy that isn't clean yet);
    • their DNS resolves nothing (dnsPolicy: None), in case the cluster doesn't enforce NetworkPolicies;
    • HADR is on, so Availability Groups can be dropped, and SQL Server Agent is off, so no copied job runs.
  4. Clean-up. Once SQL Server accepts connections, the operator records which databases belonged to Availability Groups (in status.sanitize, so a retry still knows), then:

    • force-promotes, locally, the Availability Groups the copy held as a secondary, and drops every Availability Group; the databases stay online (any left RESTORING are recovered);
    • drops database mirroring endpoints and the operator's AG certificate (ag_cert);
    • drops the s3://… credentials the operator's backups create, so the copy can't write to the source's backup bucket;
    • disables every SQL Server Agent job, unless keepAgentJobs: true;
    • renames the server to its pod name (sp_dropserver/sp_addserver).

    It then checks nothing is left before going on.

  5. Unfenced restart. The pods restart normally, which applies the new server name. The operator checks @@SERVERNAME, records the volume in status.sanitizedVolumes and removes the NetworkPolicy. The instance becomes Ready only now.

Logins, users, databases and their data travel with the copy unchanged.

Replacing the data volume later​

On an instance with spec.bootstrap, the operator treats every data PVC it hasn't recorded in status.sanitizedVolumes as copied data, and goes through steps 3–5 again. This is how a branch is reset or rolled back:

  1. set spec.paused: true and wait for status.pause.phase: Paused;
  2. delete mssql-data-<name>-0 and create it again from another VolumeSnapshot;
  3. if the new data has another SA password, set it in <name>-sa-applied (key MSSQL_SA_PASSWORD): the operator logs in with it and rotates SA back to spec.saPasswordSecret;
  4. set spec.paused: false.

The operator notices the new PVC while the instance is paused and starts it fenced. Replace volumes only while paused: a pod restarted by Kubernetes on a volume swapped under a running instance would start before the operator fences it.

Instances without spec.bootstrap are never cleaned up, whatever their PVCs were made from (a CSI or Velero restore of a production instance, for example).

Status​

status:
state: Initializing
message: "cleaning up copied data (fencing): inspected the copy on orders-pr-42-0"
sanitize:
phase: Fencing # then Unfencing
startedAt: "2026-11-12T15:33:16Z"
volumes:
- replica: 0
pvcUID: 1b0fb1ed-…
inspected: true
agDatabases: [shop]
cleaned: true
actions:
- promoted availability group agx locally
- dropped availability group agx
- dropped endpoint hadr_endpoint
- dropped certificate ag_cert
- renamed server ag-1 to orders-pr-42-0
sanitizedVolumes:
- replica: 0
pvcUID: 1b0fb1ed-…
at: "2026-11-12T15:35:18Z"

When the pod can't be scheduled, the message says why. On node-local storage the copy must run on its source's node, which needs room for it.

Measured​

On Linode LKE with OpenEBS ZFS LocalPV and SQL Server 2025, with the sources under a continuous write load:

CaseFrom creating the instance to Ready
Standalone source, 4.5 GB database65 s, most of it crash recovery of the copy
AG secondary, 1 GB database51 s; the source AG saw no disconnects
Reset of a branch (pause, swap, resume)72 s

Not covered​

  • Separate data and log volumes (see above).
  • Other outbound features in the copied data: linked servers, Service Broker routes, Database Mail profiles and replication aren't changed. The fence holds while the copy is cleaned up, not after; restrict a branch's egress with your own NetworkPolicy if it must never reach production.
  • Contained Availability Groups have their own master and msdb databases; their jobs and credentials aren't cleaned up.