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
-
Data. While the StatefulSet doesn't exist yet, the operator waits for the VolumeSnapshot to be ready, then creates
mssql-data-<name>-0from it. If that PVC already exists and wasn't made from the snapshot, the instance staysInitializingwith a message: it never adopts other data. -
SA password. The snapshot's
masterdatabase holds the source's SA password. The operator seeds<name>-sa-appliedwith the password frombootstrap.volumeSnapshot.saPasswordSecret, then its normal rotation switches SQL Server to the password inspec.saPasswordSecret: the copy doesn't keep the source's password. -
Fenced start. The copied
masterstill 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>-fencedenies 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.
- a NetworkPolicy
-
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
RESTORINGare 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.
- force-promotes, locally, the Availability Groups the copy held as a
secondary, and drops every Availability Group; the databases stay
online (any left
-
Unfenced restart. The pods restart normally, which applies the new server name. The operator checks
@@SERVERNAME, records the volume instatus.sanitizedVolumesand removes the NetworkPolicy. The instance becomesReadyonly 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:
- set
spec.paused: trueand wait forstatus.pause.phase: Paused; - delete
mssql-data-<name>-0and create it again from another VolumeSnapshot; - if the new data has another SA password, set it in
<name>-sa-applied(keyMSSQL_SA_PASSWORD): the operator logs in with it and rotates SA back tospec.saPasswordSecret; - 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:
| Case | From creating the instance to Ready |
|---|---|
| Standalone source, 4.5 GB database | 65 s, most of it crash recovery of the copy |
| AG secondary, 1 GB database | 51 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
masterandmsdbdatabases; their jobs and credentials aren't cleaned up.