Trilio Site Recovery for Kubernetes
Guide

Creating Your First Resource

Example walkthrough of defining a recovery plan and triggering a test recovery


Overview

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.


Prerequisites

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 with status.phase: Available and the ClustersValidated condition set to True. Operator workload components (trilio-site-recovery-system namespace) 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 (cluster1 and cluster2) in the ProtectionZone, each reachable by the zone credential created with tsrctl zone-credential.
  • A KubeVirt VirtualMachine (kubevirt.io/v1) running on the cluster you designate as cluster1, 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.
  • kubectl configured 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.
  • tsrctl available — 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.

Installation

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 (cluster1 or cluster2) 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; WaitForFirstConsumer classes 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.


Configuration

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

FieldTypeDefaultEffect
spec.virtualMachines[].bootOrderinteger—Lower values boot first during failover. VMs sharing a value boot in parallel. VMs without a value boot together in a final batch.
spec.desiredStaterunning | stoppedrunningDeclares whether VMs should be running or stopped on the active cluster.
spec.sourceClustercluster1 | 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.rtoTargetSecondsintegerrequiredRecovery time objective in seconds, recorded for compliance reporting.
spec.sla.rpoTargetSecondsinteger0Feeds the replication monitor's RPO objective (ReplicationGroupStatus.spec.rpoObjectiveSeconds). Must be 0 for Protocol C synchronous replication.
spec.hooksarray—Binds reusable Hook definitions to lifecycle points (pre-failover, post-failover, pre-test, post-test, test-verification, etc.). Up to 32 hooks.
spec.vmIdentity.macAddressesPreserve | ClearPreserveControls whether interface MAC addresses are preserved or cleared on the DR copy.

ProtectionRequest

FieldTypeDefaultEffect
spec.sourceClustercluster1 | cluster2requiredThe 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.replicationProtocolCCDRBD wire protocol. Only C (synchronous, RPO=0) is supported. A is rejected at admission.
spec.replicationConfig.drbdMetadataConfig.sizequantity128MiSize of the per-volume DRBD external-metadata PVC. 128 Mi accommodates backing volumes up to 4 TiB without resizing.
spec.standbyPlacementobject—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

FieldTypeDefaultEffect
spec.cleanupPolicyManual | AutomaticAutomaticAutomatic removes test resources after retentionTime elapses. Manual keeps them until cleanupRequested: true is set or the CR is deleted.
spec.retentionTimeduration string2hHow long test resources are kept before automatic cleanup (e.g. 30m, 4h).
spec.timeoutduration string—Maximum wall-clock duration before the test is marked Failed. Leave unset to avoid spurious failures on large protection groups.
spec.verification.reachabilityarray—Optional per-VM TCP or HTTP reachability probes run after the automatic boot/agent/filesystem checks.
spec.schedulingCheckFail | WarnFailWhat to do when the DR scheduler cannot place a member VM: Fail marks the test failed; Warn continues and records the verdict.
spec.skipHooksbooleanfalseWhen 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.


Usage

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, or Unknown.
  • status.aggregated.connectionState — worst-case DRBD connection state across all VMs in the group.
  • status.aggregated.outOfSyncBytes — total unsynced bytes; should be 0 in 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

Examples

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.


Troubleshooting

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:

  1. The ClustersValidated condition on the ProtectionZone is not True.
  2. The VM does not exist in the namespace specified, or the namespace does not exist on the workload cluster.
  3. The VM's PVCs use a WaitForFirstConsumer StorageClass 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:

  1. DRBD port exhaustion — the zone already has 1,000 protected VMs (one port per VM, range 7000–8000).
  2. The specified metadata StorageClass does not exist or cannot provision immediately.
  3. 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 with tsrctl zone-credential rotate --refresh-rbac.
  • ClusterAlreadyClaimed — the workload cluster is already registered to another zone.
  • ClusterIdentityChanged — the credential now reaches a different cluster (different kube-system UID). 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.