Getting Started
Installing the operator and creating your first site recovery resource
This page walks you through installing Trilio Site Recovery on your OpenShift quorum cluster and creating your first protection zone — the foundational resource that wires together your primary and DR clusters and unlocks every DR workflow. By the end you will have a running operator, a ProtectionZone in the Available phase, and a protected virtual machine replicating synchronously (Protocol C, RPO=0) between two OpenShift clusters. Trilio Site Recovery is currently in Tech Preview; no component is separately GA.
Before you begin, confirm the following are in place:
Clusters
- Three OpenShift clusters: one quorum cluster (management plane) and two workload clusters (primary and DR). The quorum cluster is required — it is not optional.
- All three clusters must run OpenShift 4.17 or later. The OLM bundle annotations (
com.redhat.openshift.versions: v4.14-v4.19, fromdeploy/olm-catalog/site-recovery/metadata/annotations.yaml) describe the installable range; the validated floor perdocs/tech-preview-supported-configurations.mdis OpenShift 4.17. Versions 4.14–4.16 are not supported even if the bundle can be installed on them. - Single-Node OpenShift is supported for both workload and quorum roles.
Virtualization
- OpenShift Virtualization (CNV) installed on both workload clusters.
Storage
- DRBD kernel module ≥ 9.x available on every worker node of both workload clusters (the operator's DRBD module-loader init container handles this on RHEL CoreOS 9 nodes automatically).
- An LVM thin pool configured on each worker node of the primary and DR clusters, backed by a dedicated block device, for DRBD backing storage.
Network
- TCP ports 7000–8000 inclusive open between primary and DR worker nodes (one port per replicated VM volume; port 8000 is mandatory — a firewall that stops at 7999 will silently prevent replication on the last volume).
- TCP port 6443 open from the quorum cluster to both workload cluster API servers.
- Round-trip latency below 50 ms between primary and DR clusters (required for Protocol C synchronous replication).
CLI tools
ocCLI — required for all OpenShift-specific paths (oc adm must-gather, Routes, SCC binding).kubectl-only operation is not a supported mode.- Kubeconfig files for all three clusters.
Registry access
- Outbound HTTPS access to
quay.io/triliovaultfrom all three clusters.
Optional components (enable additional capabilities)
- Containerized Data Importer (CDI) — for DataVolume-based VMs.
- Multus CNI — for multi-network VMs.
The minimal path from zero to a protected virtual machine:
-
Install the operator on the quorum cluster via OperatorHub by creating a
CatalogSource,OperatorGroup, andSubscriptionin thetrilio-site-recovery-systemnamespace. -
Create kubeconfig Secrets for both workload clusters in the zone namespace on the quorum cluster.
-
Create a
ProtectionZoneCR in adr-<name>namespace on the quorum cluster — the controller automatically deploys the quorum and workload control planes. -
Wait for the zone to reach
Available(oc get pz -n dr-<name>). -
Protect a VM by creating a
ProtectionRequestCR on the quorum cluster and waiting forProtectedphase.
Detailed steps for each of these actions follow in the Steps section below.
Step 1 — Install the operator on the quorum cluster
All operator resources install into the trilio-site-recovery-system namespace. The OLM bundle deploys three components: the protection-zone-controller, the site-recovery-backend Flask API, and the site-recovery-frontend Next.js UI with its OpenShift oauth-proxy sidecar. There is no feature flag and no opt-out for the console.
Create a CatalogSource pointing at the Trilio catalog image:
apiVersion: operators.coreos.com/v1alpha1
kind: CatalogSource
metadata:
name: trilio-site-recovery-catalog
namespace: openshift-marketplace
spec:
sourceType: grpc
image: quay.io/triliovault/site-recovery-catalog:latest
displayName: Trilio Site Recovery
publisher: Trilio Data, Inc.
oc --kubeconfig $KUBECONFIG_QUORUM apply -f catalogsource.yaml
Create an OperatorGroup scoped to trilio-site-recovery-system:
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: trilio-site-recovery-og
namespace: trilio-site-recovery-system
spec:
targetNamespaces:
- trilio-site-recovery-system
oc --kubeconfig $KUBECONFIG_QUORUM apply -f operatorgroup.yaml
Create a Subscription for the site-recovery package on the tech-preview channel:
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: site-recovery
namespace: trilio-site-recovery-system
spec:
channel: tech-preview
name: site-recovery
source: trilio-site-recovery-catalog
sourceNamespace: openshift-marketplace
oc --kubeconfig $KUBECONFIG_QUORUM apply -f subscription.yaml
Verify the operator pod reaches Running:
oc --kubeconfig $KUBECONFIG_QUORUM get pods -n trilio-site-recovery-system \
-l app.kubernetes.io/part-of=site-recovery
Expected output: the protection-zone-controller, site-recovery-backend, and site-recovery-frontend pods are all Running.
Step 2 — Create kubeconfig Secrets for both workload clusters
The ProtectionZone controller reaches each workload cluster through a kubeconfig stored as a Kubernetes Secret in the zone namespace. Create the zone namespace first (it must follow the dr-<name> pattern — the DRBD admission webhook rejects CRs outside a dr--prefixed namespace):
oc --kubeconfig $KUBECONFIG_QUORUM create namespace dr-prod
Create a Secret for the primary cluster (cluster1):
oc --kubeconfig $KUBECONFIG_QUORUM create secret generic cluster1-kubeconfig \
--from-file=kubeconfig=$KUBECONFIG_CLUSTER1 \
-n dr-prod
Create a Secret for the DR cluster (cluster2):
oc --kubeconfig $KUBECONFIG_QUORUM create secret generic cluster2-kubeconfig \
--from-file=kubeconfig=$KUBECONFIG_CLUSTER2 \
-n dr-prod
Why these Secrets matter: the quorum controller projects them into the site-recovery-quorum-control-plane manager pod at /etc/kubeconfig/cluster1 and /etc/kubeconfig/cluster2. The manager exits at startup if either file is missing, so both Secrets must exist before the zone reaches Available.
Step 3 — Create the ProtectionZone CR
The ProtectionZone CR is the root configuration object for a DR deployment. Creating it triggers the protection-zone-controller to:
- Deploy the
site-recovery-quorum-control-planeHelm chart into thedr-prodnamespace on the quorum cluster. - Deploy the
site-recovery-workload-control-planeHelm chart (DRBD node agent DaemonSet, webhooks, RBAC) intotrilio-site-recovery-systemon both workload clusters. - Create a default
DRBDReplicationPolicynameddr-prod-defaultwithisDefault: trueand Protocol C (you do not author or modify this object).
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: dr-prod
namespace: dr-prod
spec:
displayName: "Production DR Zone"
description: "Primary to DR site synchronous replication."
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
oc --kubeconfig $KUBECONFIG_QUORUM apply -f protectionzone.yaml
Key fields explained:
storageBackendMode: DRBD— the only supported backend; the admission webhook rejectsLinstor.clusterID— must becluster1orcluster2; these stable slot identifiers are what all other CRs (ProtectionRequest, FailoverRequest) reference.distributionName— selects the DRBD kernel-module loader image. UseRHEL-CoreOS-9for OpenShift 4.17–4.19 on RHEL CoreOS 9 nodes. If your cluster has moved to a RHEL CoreOS 10 base, useRHEL-CoreOS-10(not validated in Tech Preview perdocs/tech-preview-supported-configurations.md).kubeconfigSecretRef— resolved in theProtectionZone's own namespace (dr-prod).
Step 4 — Verify the zone reaches Available
Watch zone status on the quorum cluster. Note that quorum-side components and CRs live in dr-prod; workload-side components live in trilio-site-recovery-system on each workload cluster — these two namespaces are always distinct.
# Watch zone phase (quorum cluster)
oc --kubeconfig $KUBECONFIG_QUORUM get pz -n dr-prod -w
# Inspect per-cluster reachability
oc --kubeconfig $KUBECONFIG_QUORUM get pz dr-prod -n dr-prod -o yaml
Successful output from get pz:
NAME DISPLAY NAME BACKEND CLUSTERS STATUS AGE
dr-prod Production DR Zone DRBD primary,dr Available 2m
Also verify the control planes came up on both workload clusters:
# DRBD node agent DaemonSet on the primary cluster
oc --kubeconfig $KUBECONFIG_CLUSTER1 get daemonset \
-n trilio-site-recovery-system \
-l app.kubernetes.io/component=drbd-node-agent
# Quorum manager on the quorum cluster
oc --kubeconfig $KUBECONFIG_QUORUM get deploy \
-n dr-prod \
-l app.kubernetes.io/component=quorum-control-plane
The quorum manager pod runs a single container named manager and shows 1/1 Running. The node-agent DaemonSet should show one pod per worker node on each workload cluster.
If the zone stays in Provisioning or moves to Degraded, check the status conditions:
oc --kubeconfig $KUBECONFIG_QUORUM get pz dr-prod -n dr-prod \
-o jsonpath='{.status.conditions}' | jq .
A Unreachable cluster entry means the controller cannot reach the workload API server — verify the kubeconfig Secret content and TCP port 6443 connectivity from the quorum cluster.
Step 5 — Create a webhook serving certificate
The admission webhooks on both the quorum and workload control planes require a TLS serving certificate. The charts do not provision certificates automatically — you must create the Secret out of band before the managers can serve webhooks. The default secret name on the quorum side is webhook-server-cert (in the dr-prod namespace); on the workload side it is webhook (in trilio-site-recovery-system).
Generate a self-signed certificate and create the Secrets (adjust SANs for your cluster's DNS):
# Quorum webhook cert (dr-prod namespace)
oc --kubeconfig $KUBECONFIG_QUORUM create secret tls webhook-server-cert \
--cert=tls.crt --key=tls.key \
-n dr-prod
# Workload webhook cert — primary cluster
oc --kubeconfig $KUBECONFIG_CLUSTER1 create secret tls webhook \
--cert=tls.crt --key=tls.key \
-n trilio-site-recovery-system
# Workload webhook cert — DR cluster
oc --kubeconfig $KUBECONFIG_CLUSTER2 create secret tls webhook \
--cert=tls.crt --key=tls.key \
-n trilio-site-recovery-system
The cert SANs must cover the webhook Service's in-cluster DNS names, for example:
<release>-site-recovery-quorum-control-plane-webhook.dr-prod.svc<release>-site-recovery-quorum-control-plane-webhook.dr-prod.svc.cluster.local
Step 6 — Protect a virtual machine
With the zone Available, you can protect your first VM. A ProtectionRequest tells the protection controller to:
- Validate the VM and its PVCs on the source cluster.
- Provision a
DRBDResourcepair (one CR per cluster side, nameddrbd-<namespace>-<vm>-<hash8>, in the VM's namespace on each workload cluster). - Switch the VM to DRBD-backed frontend PVCs.
Create the ProtectionRequest on the quorum cluster in the zone namespace:
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-my-vm
namespace: dr-prod
spec:
sourceCluster: cluster1
virtualMachine:
name: my-vm
namespace: default
oc --kubeconfig $KUBECONFIG_QUORUM apply -f protectionrequest.yaml
Watch the protection phases:
oc --kubeconfig $KUBECONFIG_QUORUM get pr protect-my-vm -n dr-prod -w
The phase column progresses through: Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected.
Once Protected, the VM on the primary cluster carries the label siterecovery.trilio.io/protected-vm=true. Verify it:
oc --kubeconfig $KUBECONFIG_CLUSTER1 get vm -A \
-l siterecovery.trilio.io/protected-vm=true
Important operational constraints after protection:
- The VM is pinned to one node per cluster;
spec.nodeNameis immutable for the lifetime of the protection. - Live migration and cold migration of protected VMs are denied by admission webhook.
oc adm drainon a node hosting a running protected VM will not complete.- Re-protecting a VM always requires a full unprotect followed by a full initial resync.
Example 1 — Minimal ProtectionZone for a production DR deployment
This is the smallest valid ProtectionZone for a two-cluster DRBD deployment. The controller creates the default replication policy (dr-prod-default) automatically.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: dr-prod
namespace: dr-prod
spec:
displayName: "Production DR Zone"
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
Expected oc get pz output after the controller reconciles:
NAME DISPLAY NAME BACKEND CLUSTERS STATUS AGE
dr-prod Production DR Zone DRBD primary,dr Available 3m
Example 2 — ProtectionRequest for a single VM
Protect a VM named web-server running in the production namespace on cluster1.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-web-server
namespace: dr-prod
spec:
sourceCluster: cluster1
virtualMachine:
name: web-server
namespace: production
oc --kubeconfig $KUBECONFIG_QUORUM apply -f protect-web-server.yaml
Watch until Protected:
oc --kubeconfig $KUBECONFIG_QUORUM get pr protect-web-server -n dr-prod -w
Expected output once complete:
NAME VM NAMESPACE PHASE SYNC AGE
protect-web-server web-server production Protected 100 4m
Check the DRBD resource on the primary workload cluster (the CR lives in the VM's namespace, not the zone namespace):
oc --kubeconfig $KUBECONFIG_CLUSTER1 get dres -n production
Expected output:
NAME SIDE ROLE NODE STATE PHASE AGE
drbd-production-web-server-a1b2c3d4 Source Primary worker-0 Connected Syncing 2m
Example 3 — ProtectionGroup for coordinated failover of multiple VMs
A ProtectionGroup groups VMs that must fail over together. Create it on the quorum cluster after all member VMs have been individually protected via ProtectionRequest.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: web-tier
namespace: dr-prod
spec:
sourceCluster: cluster1
virtualMachines:
- name: web-server
namespace: production
- name: db-server
namespace: production
oc --kubeconfig $KUBECONFIG_QUORUM apply -f protectiongroup.yaml
Check group replication health:
oc --kubeconfig $KUBECONFIG_QUORUM get pg web-tier -n dr-prod
oc --kubeconfig $KUBECONFIG_QUORUM get rgs web-tier-replication -n dr-prod
Expected rgs output when healthy:
NAME PROTECTION-GROUP RPO-STATUS OUT-OF-SYNC SYNCED CONNECTION LAST-SYNC
web-tier-replication web-tier Met 0 2 Connected 30s
Example 4 — Accessing the TSR web console (OpenShift only)
The ProtectionZone controller creates an OpenShift Route and console link for the TSR web console automatically when the zone reaches Available. Browser access is supported on OpenShift only — no ingress is created on upstream Kubernetes.
Find the console Route on the quorum cluster:
oc --kubeconfig $KUBECONFIG_QUORUM get route -n trilio-site-recovery-system
Navigate to the URL shown in the HOST/PORT column. Authentication is handled by the OpenShift oauth-proxy sidecar; your OpenShift credentials are used directly. Zone-scoped pages derive the dr-<zoneId> namespace — ensure your zone name matches the namespace (a zone in a differently named namespace will be listed but its detail pages will not function).
Example 5 — Collecting a diagnostic bundle
Run tsr-gather (an oc adm must-gather-compatible tool) against the quorum cluster. It collects logs, CRD state, and configuration from all clusters in the DR estate into a single bundle.
oc --kubeconfig $KUBECONFIG_QUORUM adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:latest
To limit the log window or scope to a single zone:
# Last 2 hours of logs only
oc --kubeconfig $KUBECONFIG_QUORUM adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:latest \
-- /usr/bin/gather --since 2h
# Single zone only
oc --kubeconfig $KUBECONFIG_QUORUM adm must-gather \
--image=quay.io/triliovault/site-recovery-must-gather:latest \
-- /usr/bin/gather --zone dr-prod
Read SUMMARY.md in the bundle first for a summary of detected issues, then manifest.yaml for the full collection manifest. Secret data, kubeconfigs, and DRBD shared secrets are never written to the bundle.
Use a consistent pattern for each issue: Symptom → Likely cause → Fix.
ProtectionZone stuck in Provisioning or moves to Degraded
Symptom: oc get pz -n dr-prod shows STATUS: Provisioning or Degraded after several minutes.
Likely cause: The controller cannot reach one or both workload clusters, the kubeconfig Secret is missing or malformed, or TCP port 6443 is blocked from the quorum cluster to the workload API servers.
Fix:
# Check per-cluster reachability in the zone status
oc --kubeconfig $KUBECONFIG_QUORUM get pz dr-prod -n dr-prod \
-o jsonpath='{.status.clusters}' | jq .
# Check protection-zone-controller logs
oc --kubeconfig $KUBECONFIG_QUORUM logs -n trilio-site-recovery-system \
-l app.kubernetes.io/part-of=site-recovery,app.kubernetes.io/component=protectionzone-controller \
--tail=100
If status.clusters[].status is Unreachable, verify the Secret key name matches kubeconfigSecretRef.key, confirm the embedded kubeconfig points to the correct API server URL, and test connectivity: oc --kubeconfig $KUBECONFIG_CLUSTER1 get namespace trilio-site-recovery-system.
DRBD node agent pods stuck in Init phase
Symptom: On a workload cluster, agent pods show Init:CrashLoopBackOff or Init:Error.
Likely cause: The DRBD module-loader init container (drbd-module-loader) cannot find a precompiled kernel module matching the running kernel. This happens when the node's kernel version is not covered by the mirrored LINBIT release.
Fix:
# Find the agent DaemonSet name (release-prefixed)
oc --kubeconfig $KUBECONFIG_CLUSTER1 get daemonset -n trilio-site-recovery-system \
-l app.kubernetes.io/component=drbd-node-agent
# Get logs from the init container on a failing node
AGENT_POD=$(oc --kubeconfig $KUBECONFIG_CLUSTER1 get pods \
--field-selector spec.nodeName=<node-name> \
-n trilio-site-recovery-system -o jsonpath='{.items[0].metadata.name}')
oc --kubeconfig $KUBECONFIG_CLUSTER1 logs $AGENT_POD \
-n trilio-site-recovery-system \
-c drbd-module-loader
If the kmod loader reports no matching kernel, either update the node to a supported RHCOS kernel version or pre-load the DRBD module out of band and set agent.moduleLoader.enabled=false in the workload chart values (the chart values are managed by the ProtectionZone controller — contact Trilio support to override this setting).
ProtectionRequest stuck in Syncing with Sync percentage not advancing
Symptom: oc get pr -n dr-prod shows PHASE: Syncing and SYNC is not increasing over time.
Likely cause: DRBD replication ports (7000–8000) are not fully open between primary and DR worker nodes, the round-trip latency exceeds 50 ms, or the LVM thin pool on a worker node is exhausted.
Fix:
# Check the DRBDResource on the primary workload cluster
oc --kubeconfig $KUBECONFIG_CLUSTER1 get dres -n <vm-namespace>
oc --kubeconfig $KUBECONFIG_CLUSTER1 get dres <drbd-resource-name> -n <vm-namespace> -o yaml
Look at status.connectionState and status.volumes[].replicationState. A Disconnected or StandAlone connection state indicates a network problem. Verify that all ports 7000–8000 inclusive are open — port 8000 is mandatory. Check RTT: ping -c 10 <dr-worker-ip> from a primary worker node and confirm it is below 50 ms.
ProtectionRequest in Failed phase with RoleConflict reason
Symptom: oc get pr -n dr-prod shows PHASE: Failed. Inspecting conditions shows Ready=False with reason: RoleConflict.
Likely cause: Both DRBD copies have promoted to Primary simultaneously (split-brain scenario), or a previous failover left the DR-side copy in Primary role.
Fix:
# Check DRBD resource role on both clusters
oc --kubeconfig $KUBECONFIG_CLUSTER1 get dres -n <vm-namespace> -o wide
oc --kubeconfig $KUBECONFIG_CLUSTER2 get dres -n <vm-namespace> -o wide
The ROLE column shows Primary or Secondary for each side. Only one side may be Primary at any time. To recover: unprotect the VM (oc delete pr <name> -n dr-prod) and re-protect it. Re-protect always requires a full initial resync.
oc get dres returns server doesn't have a resource type
Symptom: Running oc --kubeconfig $KUBECONFIG_QUORUM get dres returns an error about an unknown resource type.
Likely cause: DRBDResource CRDs are installed only on workload clusters, not on the quorum cluster. The command is being run against the wrong cluster context.
Fix: Always query dres against a workload cluster:
oc --kubeconfig $KUBECONFIG_CLUSTER1 get dres -A
oc --kubeconfig $KUBECONFIG_CLUSTER2 get dres -A
TSR web console pages for a zone are blank or return errors
Symptom: The console loads but zone-scoped pages show errors or empty data.
Likely cause: Zone-scoped console pages derive the dr-<zoneId> namespace. If the ProtectionZone metadata name does not match its namespace name (for example, the zone is named prod but lives in namespace dr-production), the page cannot resolve the correct namespace and all data calls fail.
Fix: Ensure the ProtectionZone metadata.name matches the dr- prefixed namespace it lives in — for example, a zone named prod must live in namespace dr-prod. If the zone was created in the wrong namespace, delete it and recreate it in the correctly named namespace.