Trilio Site Recovery for Kubernetes
Guide

Installation

Installing CRDs and deploying the site recovery operator


Overview

This page walks you through installing Trilio Site Recovery on OpenShift clusters using the DRBD Operator deployment model. By the end of these steps, you will have the quorum control plane running on your management cluster, the workload control plane running on your primary and DR clusters, and all required CRDs in place—ready for you to create your first ProtectionZone and begin protecting virtual machine workloads. Installing in the correct order matters: the ProtectionZone webhook must be present before the quorum control plane chart, and the workload control plane must be healthy on both workload clusters before you apply any protection CRDs.


Prerequisites

Before you begin, confirm the following are in place across all three clusters.

OpenShift clusters

  • OpenShift ≥ 4.14 on each cluster (quorum, primary, DR)
  • kubectl or oc CLI configured and able to reach all three cluster API servers
  • Helm ≥ 3.0 installed on your workstation
  • A valid kubeconfig for each cluster

Virtualization

  • OpenShift Virtualization (CNV) ≥ 1.0 installed on the primary and DR clusters
  • KubeVirt VMs you intend to protect must be running before protection CRDs are applied

Storage and networking

  • DRBD Operator installed on both the primary and DR clusters
  • DRBD kernel module ≥ 9.x loaded on every worker node of the primary and DR clusters
  • TCP ports 7000–7999 open between all primary and DR worker nodes (DRBD replication)
  • TCP port 6443 open from the quorum cluster to the primary and DR API servers
  • For synchronous replication (Protocol C, RPO=0): round-trip latency below 50 ms between primary and DR worker nodes

Credentials and access

  • Cluster-admin (or equivalent) privileges on all three clusters
  • Kubeconfig files for all clusters accessible on the workstation running Helm and kubectl

Environment variables used throughout this guide

export KUBECONFIG_PRIMARY=~/.kube/config-primary
export KUBECONFIG_DR=~/.kube/config-dr
export KUBECONFIG_QUORUM=~/.kube/config-quorum

Set these before running any command on this page.


Installation

Installation follows four ordered stages. Do not skip or reorder them.


Stage 1 — Install the workload control plane on the primary cluster

The site-recovery-workload-control-plane chart installs the DRBD node agent DaemonSet, admission webhooks, and supporting RBAC needed for VM protection and replication on a workload cluster. Install it on the primary cluster first.

helm install site-recovery ./helm/site-recovery-workload-control-plane \
  --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  --create-namespace

Wait until the DaemonSet is fully rolled out before continuing:

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  rollout status daemonset/drbd-node-agent

Stage 2 — Install the workload control plane on the DR cluster

Repeat the same Helm install against the DR cluster:

helm install site-recovery ./helm/site-recovery-workload-control-plane \
  --kubeconfig $KUBECONFIG_DR \
  -n trilio-site-recovery-system \
  --create-namespace

Verify the DaemonSet on the DR cluster:

kubectl --kubeconfig $KUBECONFIG_DR \
  -n trilio-site-recovery-system \
  rollout status daemonset/drbd-node-agent

Stage 3 — Install the ProtectionZone webhook on the quorum cluster

The site-recovery-protectionzone-controller chart installs the ProtectionZone admission webhook and its CRDs. It must be installed before the quorum control plane chart; the quorum control plane's admission webhook depends on the ProtectionZone CRD being registered.

Choose a DR deployment namespace. All quorum-side resources for one DR deployment live in a single namespace named dr-<name>. Replace <deployment-name> with your chosen name (for example, prod):

export DR_NAMESPACE=dr-prod

Install the ProtectionZone controller:

helm install site-recovery-pz ./helm/site-recovery-protectionzone-controller \
  --kubeconfig $KUBECONFIG_QUORUM \
  -n $DR_NAMESPACE \
  --create-namespace

Confirm the ProtectionZone CRD is registered:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  get crd protectionzones.siterecovery.trilio.io

Stage 4 — Install the quorum control plane on the quorum cluster

The site-recovery-quorum-control-plane chart deploys the unified controller manager into the same DR namespace. It packages the failover, protection, pg-sync, and replication-monitor reconcilers, plus admission webhooks, in a single Deployment.

helm install site-recovery-qcp ./helm/site-recovery-quorum-control-plane \
  --kubeconfig $KUBECONFIG_QUORUM \
  -n $DR_NAMESPACE

Verify the controller manager pod is running:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n $DR_NAMESPACE \
  get pods -l app.kubernetes.io/name=site-recovery-quorum-control-plane

Expected output:

NAME                                               READY   STATUS    RESTARTS   AGE
site-recovery-quorum-control-plane-<hash>          1/1     Running   0          60s

Stage 5 — Apply the ProtectionZone custom resource

A ProtectionZone CR tells the quorum cluster which primary and DR clusters belong to this DR deployment and which storage backend to use. Apply it in the DR namespace on the quorum cluster:

# protectionzone.yaml
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
  name: prod-zone
  namespace: dr-prod
spec:
  displayName: "Production DR Zone"          # required
  storageBackendMode: "drbd-operator"         # required; use drbd-operator for DRBD Operator deployments
  clusters:
    - name: primary
      kubeconfig: primary-cluster-secret      # reference to a Secret holding the kubeconfig
    - name: dr
      kubeconfig: dr-cluster-secret
  description: "Primary-to-DR replication for production VMs"
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f protectionzone.yaml

Check that the ProtectionZone is accepted:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get protectionzone prod-zone

Stage 6 — Verify all installed components

Run the following checks before proceeding to configure replication.

Quorum cluster — Helm releases

helm --kubeconfig $KUBECONFIG_QUORUM list -n dr-prod

Expected releases: site-recovery-pz and site-recovery-qcp.

Primary and DR clusters — DRBD node agent DaemonSet health

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system get daemonset drbd-node-agent

kubectl --kubeconfig $KUBECONFIG_DR \
  -n trilio-site-recovery-system get daemonset drbd-node-agent

Both DaemonSets must show DESIRED equal to READY before you proceed.

Quorum cluster — CRD registration

kubectl --kubeconfig $KUBECONFIG_QUORUM get crds | grep siterecovery.trilio.io

You should see entries for protectionzones, protectionrequests, failoverrequests, testfailovers, drbdreplicationpolicies, drbdresources, replicationgroupstatuses, and rpoevents.


Configuration

After installation, the primary configuration surface is the ProtectionZone CR and the DRBDReplicationPolicy CR. Helm values files allow you to tune controller behavior at deploy time.


ProtectionZone CR — key fields

The ProtectionZone is the root configuration object for a DR deployment. Its required fields control cluster membership and storage backend selection.

FieldRequiredValid valuesEffect
spec.displayNameYesAny stringHuman-readable name shown in the TSR web console
spec.storageBackendModeYesdrbd-operatorSelects the DRBD Operator storage management path
spec.clustersYesArray of cluster referencesRegisters primary and DR clusters with the quorum control plane
spec.descriptionNoAny stringOptional free-text description
spec.loggingNoObjectConfigures log verbosity for controllers in this zone

DRBDReplicationPolicy CR — key fields

The DRBDReplicationPolicy defines how volumes replicate between your primary and DR clusters. Create one per DR deployment on the quorum cluster. This CR is consumed by the protection controller when processing ProtectionRequest CRDs.

FieldRequiredValid values / defaultsEffect
spec.clustersYesArray of cluster endpoint objectsIdentifies the primary and DR cluster replication endpoints
spec.drbdProtocolNoC (default for RPO=0), AC = synchronous replication, requires <50 ms RTT; A = asynchronous, suits long-distance links
spec.replicationModeNoStringSets the overall replication strategy
spec.rpoNoObjectDefines RPO objectives that trigger RPOEvent CRs when violated
spec.diskConfigNoObjectStorage class mappings between primary and DR
spec.networkConfigNoObjectNetwork-level DRBD tuning (buffer sizes, timeout)
spec.resyncConfigNoObjectControls resync rate-limiting to protect production I/O
spec.isDefaultNotrue / falseMarks this policy as the default for the deployment namespace

Replication protocol choice is the most operationally significant setting:

  • Use drbdProtocol: C (Protocol C, synchronous) when your primary-to-DR round-trip latency is below 50 ms. Every write is acknowledged only after it commits on both sides, giving you RPO=0.
  • Use drbdProtocol: A (Protocol A, asynchronous) for geographically distant clusters where latency exceeds 50 ms. Writes complete after local commit; replication happens in the background, yielding an RPO of seconds.

Helm values — workload control plane

Pass values at install time with --set or a values file. The most commonly tuned settings for site-recovery-workload-control-plane are node selector and tolerations for the drbd-node-agent DaemonSet, so the agent runs only on nodes that have the DRBD kernel module loaded:

helm install site-recovery ./helm/site-recovery-workload-control-plane \
  --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  --create-namespace \
  --set drbdNodeAgent.nodeSelector."drbd-enabled"="true"

Usage

Once installation is complete and your ProtectionZone CR is accepted, you interact with Site Recovery exclusively through Kubernetes custom resources applied with kubectl. The following patterns cover the most common day-one operations.


Create a DRBDReplicationPolicy

Before protecting any VM, define how replication works between your clusters. Apply this on the quorum cluster in your DR namespace:

# drbd-replication-policy.yaml
apiVersion: siterecovery.trilio.io/v1alpha1
kind: DRBDReplicationPolicy
metadata:
  name: prod-replication-policy
  namespace: dr-prod
spec:
  drbdProtocol: C
  isDefault: true
  clusters:
    - name: primary
      replicationEndpoint: "10.0.1.10:7000"
    - name: dr
      replicationEndpoint: "10.0.2.10:7000"
  diskConfig:
    storageClassMappings:
      - primaryStorageClass: ocs-storagecluster-ceph-rbd
        drStorageClass: ocs-storagecluster-ceph-rbd
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f drbd-replication-policy.yaml

Protect a virtual machine

A ProtectionRequest CR asks the protection controller to validate a VM, provision a DRBDResource pair covering all of the VM's disks, and switch the VM to DRBD-backed frontend PVCs. Apply it on the quorum cluster:

# protection-request.yaml
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
  name: protect-vm-web
  namespace: dr-prod
spec:
  sourceCluster: primary           # required
  virtualMachine:                  # required
    name: vm-web-server
    namespace: production
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f protection-request.yaml

Watch the protection progress:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get protectionrequest protect-vm-web -w

The protection controller moves the request through phases: PendingValidatingProvisioningDRBDResourceSwitchingPVCsProtected.


Monitor replication health

Check the ReplicationGroupStatus CR to see aggregated health for a set of protected VMs:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get replicationgroupstatus

Inspect individual DRBDResource CRs on the primary cluster to see per-volume sync progress:

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  get drbdresource -o wide

List all Site Recovery CRDs in use

# Quorum cluster — failover and protection CRDs
kubectl --kubeconfig $KUBECONFIG_QUORUM \
  get protectionrequest,failoverrequest,testfailover,replicationgroupstatus,rpoevent -A

# Primary cluster — workload-side CRDs
kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  get drbdresource,drbdreplicationpolicy -A

Examples

Example 1 — Minimal DRBDReplicationPolicy with asynchronous replication

Use Protocol A when your DR site is geographically distant and latency exceeds 50 ms.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: DRBDReplicationPolicy
metadata:
  name: async-replication-policy
  namespace: dr-prod
spec:
  drbdProtocol: A
  isDefault: false
  clusters:
    - name: primary
      replicationEndpoint: "10.0.1.10:7000"
    - name: dr
      replicationEndpoint: "192.168.100.5:7000"
  diskConfig:
    storageClassMappings:
      - primaryStorageClass: ocs-storagecluster-ceph-rbd
        drStorageClass: ocs-storagecluster-ceph-rbd
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f async-replication-policy.yaml

Expected output:

drbdreplicationpolicy.siterecovery.trilio.io/async-replication-policy created

Example 2 — ProtectionRequest for a single VM

Protect a VM named vm-database running in the production namespace on the primary cluster.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
  name: protect-vm-database
  namespace: dr-prod
spec:
  sourceCluster: primary
  virtualMachine:
    name: vm-database
    namespace: production
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f protect-vm-database.yaml
kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get protectionrequest protect-vm-database -o wide

Expected output (after the controller finishes):

NAME                   STATUS      VM             NAMESPACE    AGE
protect-vm-database    Protected   vm-database    production   3m12s

Example 3 — Non-disruptive test failover

Validate DR readiness without affecting production workloads. The test-failover-controller creates snapshot-based copies of the VM's volumes on the DR cluster, starts test VMs in an isolated namespace, runs verification checks, and then cleans up.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
  name: test-prod-zone
  namespace: dr-prod
spec:
  protectionGroupRef:             # required
    name: prod-protection-group
    namespace: dr-prod
  cleanupPolicy: Manual
  retentionTime: 2h
  batchBootTimeoutSeconds: 300
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f test-failover.yaml
kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get testfailover test-prod-zone -w

Expected phase progression:

NAME             PHASE                AGE
test-prod-zone   CreatingSnapshots    10s
test-prod-zone   CreatingVolumes      45s
test-prod-zone   CreatingVMs          90s
test-prod-zone   VerifyingData        2m10s
test-prod-zone   Succeeded            3m05s

When you are finished inspecting the test environment, delete the CR to trigger cleanup:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  delete testfailover test-prod-zone

Example 4 — Planned failover

A FailoverRequest with failoverType: planned gracefully shuts down VMs on the primary cluster before promoting DRBD volumes on the DR cluster, ensuring zero data loss.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
  name: planned-failover-prod
  namespace: dr-prod
spec:
  protectionGroupRef:             # required
    name: prod-protection-group
  targetCluster: dr               # required
  failoverType: planned
  drainTimeoutSeconds: 120
  batchBootTimeoutSeconds: 300
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f planned-failover.yaml
kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get failoverrequest planned-failover-prod -w

Expected output:

NAME                    STATUS       AGE
planned-failover-prod   Pending      2s
planned-failover-prod   InProgress   8s
planned-failover-prod   Completed    4m33s

Example 5 — Collect a diagnostic support bundle

Run tsr-gather from the quorum cluster to collect logs and CRD state across all clusters in the DR estate. This tool is compatible with oc adm must-gather.

# Collect the full estate (default 48 h of logs)
oc adm must-gather \
  --image=$DOCKER_REGISTRY/site-recovery-must-gather:<tag>

# Limit to the last 2 hours of logs
oc adm must-gather \
  --image=$DOCKER_REGISTRY/site-recovery-must-gather:<tag> \
  -- /usr/bin/gather --since 2h

# Scope to a single protection zone
oc adm must-gather \
  --image=$DOCKER_REGISTRY/site-recovery-must-gather:<tag> \
  -- /usr/bin/gather --zone prod-zone

The bundle is written to a local directory. Open SUMMARY.md first to see flagged issues, then manifest.yaml for a full inventory of collected resources. The tool never captures secrets, kubeconfigs, cloud-init payloads, or DRBD shared secrets.


Troubleshooting

Use a consistent approach for each issue: confirm the symptom with the listed command, identify the likely cause, then apply the fix.


Issue 1 — Quorum control plane pod is not starting

Symptom: The site-recovery-quorum-control-plane pod stays in Pending or CrashLoopBackOff.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  describe pod -l app.kubernetes.io/name=site-recovery-quorum-control-plane

Likely cause — ProtectionZone webhook not installed: The quorum control plane admission webhook depends on the ProtectionZone CRD. If site-recovery-protectionzone-controller was not installed first, webhook calls fail and the pod crashes.

Fix: Install the ProtectionZone controller chart before the quorum control plane chart, then restart the quorum control plane pod.

helm install site-recovery-pz ./helm/site-recovery-protectionzone-controller \
  --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  rollout restart deployment site-recovery-quorum-control-plane

Likely cause — missing cluster credentials: The controller cannot reach the primary or DR API servers. Verify that the kubeconfig Secrets referenced in your ProtectionZone CR exist in the DR namespace.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get secrets

Issue 2 — DRBD node agent DaemonSet has pods not ready

Symptom: DESIRED and READY counts differ for the drbd-node-agent DaemonSet.

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  get daemonset drbd-node-agent

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  describe pod -l app=drbd-node-agent | grep -A 10 Events

Likely cause — DRBD kernel module not loaded: The agent requires the DRBD kernel module ≥ 9.x on each worker node. A missing or incompatible module causes the pod to fail its readiness probe.

Fix: On each affected worker node, verify and load the module:

# Run on each affected worker node
lsmod | grep drbd
modprobe drbd

Confirm the node's pod becomes ready after the module is loaded:

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n trilio-site-recovery-system \
  get pods -l app=drbd-node-agent -o wide

Issue 3 — ProtectionRequest stays in Pending

Symptom: A ProtectionRequest CR does not advance beyond Pending.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  describe protectionrequest <name>

Likely cause — no DRBDReplicationPolicy: The protection controller requires a DRBDReplicationPolicy in the same DR namespace. If none exists (or isDefault is not set and no policy is referenced), the request stalls.

Fix: Apply a DRBDReplicationPolicy in the same namespace and set isDefault: true, then verify:

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get drbdreplicationpolicy

Likely cause — VM not found on source cluster: The protection controller validates the VM on the source cluster before proceeding. Ensure the VM name and namespace in spec.virtualMachine are correct and that the VM exists and is running.

kubectl --kubeconfig $KUBECONFIG_PRIMARY \
  -n <vm-namespace> \
  get vm <vm-name>

Issue 4 — TestFailover stuck in CreatingSnapshots

Symptom: A TestFailover CR stays in the CreatingSnapshots phase for more than a few minutes.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  describe testfailover <name>

Likely cause — VolumeSnapshot CRD or snapshot class not present: The test-failover-controller creates VolumeSnapshot resources on the DR cluster. If the CSI snapshot CRDs or a compatible VolumeSnapshotClass are not installed, snapshot creation silently queues.

Fix: On the DR cluster, confirm snapshot infrastructure is present:

kubectl --kubeconfig $KUBECONFIG_DR \
  get volumesnapshotclass

kubectl --kubeconfig $KUBECONFIG_DR \
  get crd volumesnapshots.snapshot.storage.k8s.io

Install the CSI snapshot controller and a compatible VolumeSnapshotClass if they are missing, then delete and re-create the TestFailover CR.


Issue 5 — FailoverRequest stays in InProgress

Symptom: A FailoverRequest is stuck in InProgress for longer than 10 minutes.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  describe failoverrequest <name>

Likely cause — primary cluster unreachable (planned failover): For a planned failover, the failover controller waits for the primary cluster to confirm VM shutdown. If the primary API server is unreachable, the drain step times out.

Fix: If the primary cluster is truly unavailable and this is a disaster scenario, delete the current FailoverRequest and re-create it with failoverType: unplanned. This force-promotes DRBD volumes on the DR cluster without waiting for the primary.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  delete failoverrequest <name>
# unplanned-failover.yaml
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
  name: emergency-failover
  namespace: dr-prod
spec:
  protectionGroupRef:
    name: prod-protection-group
  targetCluster: dr
  failoverType: unplanned
kubectl --kubeconfig $KUBECONFIG_QUORUM apply -f unplanned-failover.yaml

Warning: An unplanned failover may result in minimal data loss if Protocol A (asynchronous) replication was in use at the time of the failure.


Issue 6 — ReplicationGroupStatus shows Degraded or Critical

Symptom: kubectl get replicationgroupstatus shows a status of Degraded or Critical for one or more protection groups.

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get replicationgroupstatus -o wide

kubectl --kubeconfig $KUBECONFIG_QUORUM \
  -n dr-prod \
  get rpoevent

Likely cause — DRBD replication link down: A network interruption on TCP ports 7000–7999 between primary and DR worker nodes breaks the DRBD connection.

Fix: Confirm port connectivity from a primary worker node to a DR worker node:

# Run on a primary worker node
nc -zv <dr-worker-node-ip> 7000

Restore network connectivity, then watch the ReplicationGroupStatus return to Healthy as DRBD reconnects and resyncs.

Likely cause — RPO threshold violated: An RPOEvent CR with severity critical indicates replication lag has exceeded the configured RPO objective. Check the event details and consider switching to Protocol C if latency permits.