Creating Your First Resource
Example walkthrough of defining a recovery plan and triggering a test recovery
This page walks you through defining a complete disaster-recovery plan for a KubeVirt VirtualMachine and then validating that plan with a non-disruptive test recovery. By the end you will have a ProtectionGroup grouping your VM, a ProtectionRequest driving block-level DRBD replication to your DR cluster, and a TestFailover that proves your VM can boot from a snapshot on the standby side — without touching production workloads. All operations are declared through Kubernetes custom resources in the siterecovery.trilio.io/v1alpha1 API group and applied against the TSR-Manager cluster.
Before starting this walkthrough, confirm the following are in place:
- Trilio Site Recovery installed: The OLM-based operator is running on your TSR-Manager cluster and a ProtectionZone CR exists in a
dr-<name>namespace withstatus.phase: Availableand theClustersValidatedcondition set toTrue. Operator workload components (trilio-site-recovery-systemnamespace) must be healthy on both workload clusters. - OpenShift 4.17 or later on the TSR-Manager cluster and both workload clusters. Upstream Kubernetes workload clusters are supported on versions corresponding to OpenShift 4.17 and later.
- Two registered workload clusters (
cluster1andcluster2) in the ProtectionZone, each reachable by the zone credential created withtsrctl zone-credential. - A KubeVirt VirtualMachine (
kubevirt.io/v1) running on the cluster you designate ascluster1, with at least one bound PVC backed by a StorageClass that supports block-mode volumes. - TCP ports 7000–8000 inclusive open between primary and DR worker nodes. Each replicated VM requires one port from this range; port 8000 is mandatory — a firewall rule that stops at 7999 will silently prevent replication on one volume.
kubectlconfigured with a context pointing to the TSR-Manager cluster for all TSR-Manager CR operations, and separate contexts for each workload cluster when inspecting workload-side resources.oc(OpenShift CLI) available for must-gather support bundle collection if needed.tsrctlavailable — supplied by Trilio with the release. Contact Trilio support to obtain it.- The VM is not currently live-migrating. Protected VMs are pinned to one node per cluster for the lifetime of protection; live migration and cold migration are denied once protection is active.
There is no additional installation step for this walkthrough — the operator and workload stacks are already deployed by the ProtectionZone controller. The steps below create the application-layer custom resources that define your recovery plan.
Step 1 — Confirm zone readiness
Before creating any protection resources, verify that the ProtectionZone has validated both clusters. All CRs in this guide are created in the zone's namespace on the TSR-Manager cluster (e.g. dr-prod). Substitute your actual zone namespace throughout.
# Run against the TSR-Manager cluster
kubectl get pz -n dr-prod
The STATUS column must show Available. If it shows Degraded or Unavailable, inspect the zone conditions before continuing:
kubectl describe pz <zone-name> -n dr-prod
Look for the ClustersValidated condition. It must be True — the TSR-Manager webhooks will reject ProtectionGroup and ProtectionRequest creates until it is.
Step 2 — Create a ProtectionGroup
A ProtectionGroup bundles one or more KubeVirt VMs for coordinated failover. Create the following manifest and apply it to the TSR-Manager cluster in your zone namespace.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: my-app-pg
namespace: dr-prod
spec:
displayName: "My Application Protection Group"
virtualMachines:
- name: my-app-vm
namespace: my-app-namespace
bootOrder: 1
desiredState: running
sourceCluster: cluster1
sla:
rtoTargetSeconds: 300
rpoTargetSeconds: 0
kubectl apply -f protectiongroup.yaml
Key fields:
spec.virtualMachines[].namespace— the namespace where the VM lives on the workload cluster.spec.sourceCluster— the cluster ID (cluster1orcluster2) where the VM currently runs.spec.sla.rtoTargetSeconds— required field; sets your recovery time target for compliance reporting.spec.sla.rpoTargetSeconds: 0— correct for synchronous Protocol C replication (RPO=0 is the only supported mode).
Step 3 — Create a ProtectionRequest
A ProtectionRequest declares protection for a single VM. The controller provisions a DRBD resource pair (one CR per workload cluster), wires the VM to DRBD-backed frontend PVCs, and drives the VM through to Protected phase.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-my-app-vm
namespace: dr-prod
spec:
sourceCluster: cluster1
virtualMachine:
name: my-app-vm
namespace: my-app-namespace
replicationConfig:
replicationProtocol: C
rpo: 0
drbdMetadataConfig:
storageClassName: ocs-storagecluster-ceph-rbd
size: "128Mi"
kubectl apply -f protectionrequest.yaml
Key fields:
spec.sourceCluster— must match the cluster where the VM runs; also acts as the failover lever (patching it triggers a role flip).spec.replicationConfig.replicationProtocol: C— Protocol C synchronous replication is the only supported mode. Protocol A is rejected by the admission webhook.spec.replicationConfig.drbdMetadataConfig.storageClassName— the StorageClass used for per-volume DRBD external-metadata PVCs. Must bind immediately;WaitForFirstConsumerclasses are rejected during validation.
Watch the ProtectionRequest progress through its phases:
kubectl get pr protect-my-app-vm -n dr-prod -w
The phase sequence is: Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected.
Initial sync duration depends on the volume size. The Sync column shows percentage completion.
The custom resources in this walkthrough expose the following key configuration fields. All fields below are in the siterecovery.trilio.io/v1alpha1 API group.
ProtectionGroup
| Field | Type | Default | Effect |
|---|---|---|---|
spec.virtualMachines[].bootOrder | integer | — | Lower values boot first during failover. VMs sharing a value boot in parallel. VMs without a value boot together in a final batch. |
spec.desiredState | running | stopped | running | Declares whether VMs should be running or stopped on the active cluster. |
spec.sourceCluster | cluster1 | cluster2 | — | Records the protect-time home cluster; the failover controller reads this to classify a failover direction (failover vs. failback) for hook selection. |
spec.sla.rtoTargetSeconds | integer | required | Recovery time objective in seconds, recorded for compliance reporting. |
spec.sla.rpoTargetSeconds | integer | 0 | Feeds the replication monitor's RPO objective (ReplicationGroupStatus.spec.rpoObjectiveSeconds). Must be 0 for Protocol C synchronous replication. |
spec.hooks | array | — | Binds reusable Hook definitions to lifecycle points (pre-failover, post-failover, pre-test, post-test, test-verification, etc.). Up to 32 hooks. |
spec.vmIdentity.macAddresses | Preserve | Clear | Preserve | Controls whether interface MAC addresses are preserved or cleared on the DR copy. |
ProtectionRequest
| Field | Type | Default | Effect |
|---|---|---|---|
spec.sourceCluster | cluster1 | cluster2 | required | The cluster where the VM currently runs. This is the failover lever: patching it to the DR cluster triggers the DRBD role flip that constitutes a failover for this VM. |
spec.replicationConfig.replicationProtocol | C | C | DRBD wire protocol. Only C (synchronous, RPO=0) is supported. A is rejected at admission. |
spec.replicationConfig.drbdMetadataConfig.size | quantity | 128Mi | Size of the per-volume DRBD external-metadata PVC. 128 Mi accommodates backing volumes up to 4 TiB without resizing. |
spec.standbyPlacement | object | — | Overrides node-level scheduling rules on the DR copy only (node selector, node affinity, tolerations). Immutable once set; unprotect and re-protect to change. |
TestFailover
| Field | Type | Default | Effect |
|---|---|---|---|
spec.cleanupPolicy | Manual | Automatic | Automatic | Automatic removes test resources after retentionTime elapses. Manual keeps them until cleanupRequested: true is set or the CR is deleted. |
spec.retentionTime | duration string | 2h | How long test resources are kept before automatic cleanup (e.g. 30m, 4h). |
spec.timeout | duration string | — | Maximum wall-clock duration before the test is marked Failed. Leave unset to avoid spurious failures on large protection groups. |
spec.verification.reachability | array | — | Optional per-VM TCP or HTTP reachability probes run after the automatic boot/agent/filesystem checks. |
spec.schedulingCheck | Fail | Warn | Fail | What to do when the DR scheduler cannot place a member VM: Fail marks the test failed; Warn continues and records the verdict. |
spec.skipHooks | boolean | false | When true, all hooks (including test-verification hooks) are recorded as Skipped rather than executed. |
DRBDReplicationPolicy (auto-managed)
The ProtectionZone controller automatically creates one default policy named <zoneName>-default with isDefault: true in the zone namespace. You do not create or modify this object. The controller selects it for all ProtectionRequests in the namespace. Protocol C synchronous replication is always used.
Monitoring protection status
After applying a ProtectionRequest, track progress on the TSR-Manager cluster:
# Summary view — Phase and Sync columns
kubectl get pr -n dr-prod
# Detailed status including conditions and sync progress
kubectl describe pr protect-my-app-vm -n dr-prod
The status.slowestVolumeSyncProgress.percentage field shows the percentage of initial sync complete for the slowest volume. Once all volumes reach 100% sync, the phase advances to Protected.
To inspect the DRBD resource on a workload cluster, note that DRBDResource CRs live on the workload clusters (not on the TSR-Manager), in the VM's namespace:
# Run against the primary workload cluster
kubectl get dres -n my-app-namespace
# Detailed view — connection state, phase, sync progress
kubectl describe dres <drbd-resource-name> -n my-app-namespace
DRBD resource names follow the pattern drbd-<namespace>-<vm>-<hash8>.
To identify which VMs are protected on a workload cluster:
kubectl get vm -A -l siterecovery.trilio.io/protected-vm=true
Monitoring replication health
The replication monitor auto-creates a ReplicationGroupStatus CR per ProtectionGroup, named <protection-group-name>-replication:
kubectl get rgs my-app-pg-replication -n dr-prod
kubectl describe rgs my-app-pg-replication -n dr-prod
Key status fields:
status.rpoStatus—Met,Degraded,Violated, orUnknown.status.aggregated.connectionState— worst-case DRBD connection state across all VMs in the group.status.aggregated.outOfSyncBytes— total unsynced bytes; should be0in steady state with Protocol C.status.lastFullySyncedAt— when every VM in the group was last fully in sync.
RPO events (status transitions such as sync completed, connection lost, RPO violated) are recorded as immutable RPOEvent CRs:
kubectl get rpoe -n dr-prod -l siterecovery.trilio.io/protection-group=my-app-pg
Running a test failover
A TestFailover validates DR readiness without disrupting production. The controller spins up isolated VM copies on the standby cluster from DRBD snapshots, inside a controller-generated namespace fenced by a default-deny NetworkPolicy. The test namespace name is reported in status.testNamespace after the CR is created — it is generated by the controller and never user-provided.
Create a TestFailover CR on the TSR-Manager cluster:
apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
name: my-app-pg-test-01
namespace: dr-prod
spec:
protectionGroupRef:
name: my-app-pg
cleanupPolicy: Automatic
retentionTime: 2h
schedulingCheck: Fail
kubectl apply -f testfailover.yaml
Monitor progress:
kubectl get tfo my-app-pg-test-01 -n dr-prod -w
The phase sequence is: Queued → PreparingEnvironment → CreatingSnapshots → CreatingVolumes → CreatingVMs → StartingVMs → Verifying → Succeeded (or Failed).
The controller selects the target cluster automatically (the cluster the production VMs are not running on) and records its reasoning in status.targetClusterReason.
After the test completes, inspect the result:
kubectl describe tfo my-app-pg-test-01 -n dr-prod
Check status.result (Succeeded or Failed), status.verificationResults.passed, and status.schedulingCheck.verdict.
With cleanupPolicy: Automatic, test resources are removed after the retentionTime window. To trigger cleanup immediately without deleting the record:
kubectl patch tfo my-app-pg-test-01 -n dr-prod \
--type merge -p '{"spec":{"cleanupRequested":true}}'
To delete the CR and all associated test resources:
kubectl delete tfo my-app-pg-test-01 -n dr-prod
Example 1 — Minimal ProtectionGroup for a single VM
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: my-app-pg
namespace: dr-prod
spec:
displayName: "My Application"
virtualMachines:
- name: my-app-vm
namespace: my-app-namespace
desiredState: running
sourceCluster: cluster1
sla:
rtoTargetSeconds: 300
rpoTargetSeconds: 0
Example 2 — ProtectionRequest with custom metadata storage
This example specifies a dedicated StorageClass for DRBD external-metadata PVCs and a larger metadata volume to accommodate a large boot disk.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-my-app-vm
namespace: dr-prod
spec:
sourceCluster: cluster1
virtualMachine:
name: my-app-vm
namespace: my-app-namespace
replicationConfig:
replicationProtocol: C
rpo: 0
drbdMetadataConfig:
storageClassName: ocs-storagecluster-ceph-rbd
size: "256Mi"
Example 3 — ProtectionGroup with boot ordering for a multi-tier application
Here the database VM boots first (order 1), then the application server (order 2), allowing you to control failover sequencing.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: my-app-tiered-pg
namespace: dr-prod
spec:
displayName: "Tiered App — DB first"
virtualMachines:
- name: my-db-vm
namespace: my-app-namespace
bootOrder: 1
- name: my-app-vm
namespace: my-app-namespace
bootOrder: 2
desiredState: running
sourceCluster: cluster1
sla:
rtoTargetSeconds: 600
rpoTargetSeconds: 0
Example 4 — TestFailover with reachability probes and manual cleanup
This test adds HTTP reachability verification against port 8080 of a specific VM, and keeps test resources available for manual inspection until you explicitly request cleanup.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
name: my-app-pg-drill-01
namespace: dr-prod
spec:
protectionGroupRef:
name: my-app-tiered-pg
cleanupPolicy: Manual
retentionTime: 4h
schedulingCheck: Fail
verification:
reachability:
- type: http
vmName: my-app-vm
port: 8080
endpoint: /healthz
expectedStatus: 200
timeout: 30s
- type: tcp
vmName: my-db-vm
port: 5432
timeout: 15s
# Apply the test
kubectl apply -f testfailover-drill.yaml
# Watch phases
kubectl get tfo my-app-pg-drill-01 -n dr-prod -w
# After Succeeded — find the generated test namespace
kubectl get tfo my-app-pg-drill-01 -n dr-prod \
-o jsonpath='{.status.testNamespace}'
# Check verification results
kubectl get tfo my-app-pg-drill-01 -n dr-prod \
-o jsonpath='{.status.verificationResults}'
# When done inspecting, request cleanup without deleting the record
kubectl patch tfo my-app-pg-drill-01 -n dr-prod \
--type merge -p '{"spec":{"cleanupRequested":true}}'
Example 5 — Checking replication health for a ProtectionGroup
# List the auto-created ReplicationGroupStatus (short name: rgs)
kubectl get rgs -n dr-prod
# Detailed metrics — out-of-sync bytes, connection state, RPO status
kubectl describe rgs my-app-pg-replication -n dr-prod
# RPO audit events for this group (filter by label)
kubectl get rpoe -n dr-prod \
-l siterecovery.trilio.io/protection-group=my-app-pg
Example 6 — Inspect DRBD resources on a workload cluster
# Run against the primary workload cluster context
kubectl get dres -n my-app-namespace \
--context <primary-cluster-context>
# Run against the DR workload cluster context
kubectl get dres -n my-app-namespace \
--context <dr-cluster-context>
Each DRBDResource CR is named drbd-<namespace>-<vm>-<hash8>. The kubectl get printer columns show Side, Role, Node, State, and Phase.
Example 7 — Adjusting the runtime log level without restarting pods
Edit the site-recovery-logging ConfigMap in the zone namespace on the TSR-Manager cluster to increase verbosity for the control plane:
kubectl patch configmap site-recovery-logging -n dr-prod \
--type merge \
-p '{"data":{"level":"info","level.control-plane":"debug"}}'
Accepted values: error, info, debug, trace. The change takes effect within approximately 15 seconds without pod restarts.
Use the following as a reference for common issues you may encounter when creating protection resources or running a test recovery.
ProtectionRequest stuck in Validating phase
Symptom: kubectl get pr shows Phase: Validating for more than a few minutes.
Likely causes:
- The
ClustersValidatedcondition on the ProtectionZone is notTrue. - The VM does not exist in the namespace specified, or the namespace does not exist on the workload cluster.
- The VM's PVCs use a
WaitForFirstConsumerStorageClass for metadata — this is rejected during validation.
Fix:
# Check zone condition
kubectl describe pz <zone-name> -n dr-prod | grep -A5 ClustersValidated
# Check ProtectionRequest conditions for a specific error message
kubectl describe pr protect-my-app-vm -n dr-prod
# Verify the VM exists on the primary workload cluster
kubectl get vm my-app-vm -n my-app-namespace --context <primary-cluster-context>
ProtectionRequest reaches Failed phase
Symptom: kubectl get pr shows Phase: Failed.
Likely causes:
- DRBD port exhaustion — the zone already has 1,000 protected VMs (one port per VM, range 7000–8000).
- The specified metadata StorageClass does not exist or cannot provision immediately.
- The DR cluster's drbd-node-agent DaemonSet is not healthy.
Fix:
# Check ProtectionRequest conditions for the failure reason
kubectl describe pr protect-my-app-vm -n dr-prod
# Check the drbd-node-agent DaemonSet on the DR workload cluster
kubectl get daemonset -n trilio-site-recovery-system \
-l app.kubernetes.io/component=drbd-node-agent \
--context <dr-cluster-context>
# Check node agent pod logs on the DR cluster (two-step: get pod name, then logs)
kubectl get pods -n trilio-site-recovery-system \
--field-selector spec.nodeName=<node-name> \
-l app.kubernetes.io/component=drbd-node-agent \
-o jsonpath='{.items[0].metadata.name}' \
--context <dr-cluster-context>
kubectl logs <pod-name> -n trilio-site-recovery-system \
--context <dr-cluster-context>
ProtectionGroup webhooks reject ProtectionRequest create
Symptom: kubectl apply returns an admission error such as "ClustersValidated is not True".
Likely cause: The ProtectionZone's ClustersValidated condition is False or Unknown. This gates all ProtectionGroup and ProtectionRequest creates and spec changes.
Fix: Resolve the zone condition first.
kubectl describe pz <zone-name> -n dr-prod
Common reasons and their resolutions:
InsufficientClusterPermissions— rotate the zone credential withtsrctl zone-credential rotate --refresh-rbac.ClusterAlreadyClaimed— the workload cluster is already registered to another zone.ClusterIdentityChanged— the credential now reaches a different cluster (differentkube-systemUID). There is no override; restore the original cluster or re-create the zone.KubeVirtNotInstalled— install KubeVirt on the workload cluster before proceeding.
TestFailover stuck in PreparingEnvironment
Symptom: kubectl get tfo shows Phase: PreparingEnvironment for an extended period.
Likely cause: The controller could not create the fenced test namespace or apply the default-deny NetworkPolicy on the target workload cluster. This may indicate a permissions issue or an unhealthy workload control plane.
Fix:
# Check TestFailover conditions
kubectl describe tfo my-app-pg-test-01 -n dr-prod
# Check the workload control plane manager on the DR cluster
kubectl get pods -n trilio-site-recovery-system \
-l app.kubernetes.io/component=workload-control-plane \
--context <dr-cluster-context>
TestFailover shows Succeeded but verificationResults.passed is false
Symptom: The test phase is Succeeded but status.verificationResults.passed is false.
Likely cause: One or more reachability probes failed (the VM booted but was not reachable on the probed port within the timeout), or a test-verification hook returned a non-zero exit code.
Fix:
# Inspect verification detail
kubectl get tfo my-app-pg-test-01 -n dr-prod \
-o jsonpath='{.status.verificationResults.checks}'
# Find the generated test namespace (controller-assigned)
kubectl get tfo my-app-pg-test-01 -n dr-prod \
-o jsonpath='{.status.testNamespace}'
# List test VMs in the fenced namespace (on DR workload cluster)
kubectl get vm -n <test-namespace> --context <dr-cluster-context>
If the VMs booted but the reachability probe timed out, check that the probed service is running inside the guest and that the port is not blocked by a NetworkPolicy inside the test namespace.
DRBD replication shows status.aggregated.connectionState: Disconnected
Symptom: kubectl get rgs my-app-pg-replication -n dr-prod shows Connection: Disconnected.
Likely cause: Network connectivity between primary and DR worker nodes is disrupted on TCP ports 7000–8000. A firewall rule that stops at 7999 will silently leave one VM unable to replicate.
Fix:
# Check which VM's DRBD resource is disconnected
kubectl describe rgs my-app-pg-replication -n dr-prod
# Inspect the DRBDResource on the primary workload cluster
kubectl get dres -n my-app-namespace --context <primary-cluster-context>
kubectl describe dres <resource-name> -n my-app-namespace \
--context <primary-cluster-context>
# Check recent RPO events for connection loss
kubectl get rpoe -n dr-prod \
-l siterecovery.trilio.io/protection-group=my-app-pg
Verify that TCP ports 7000–8000 inclusive are open in both directions between the worker nodes. Port 8000 is not optional.
Collecting a support bundle
If you cannot resolve an issue, collect a diagnostic bundle from the TSR-Manager cluster (it automatically reaches through to all zone workload clusters):
oc adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:<release-tag>
For a narrower collection window:
oc adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:<release-tag> \
-- /usr/bin/gather --since 2h
For a single zone only:
oc adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:<release-tag> \
-- /usr/bin/gather --zone dr-prod
Replace <release-tag> with the pinned image tag supplied with your release. The bundle includes logs, CRs, and DRBD state from all clusters. Secret data and kubeconfigs are never written into the archive.