Problem
During a disaster‑recovery (DR) run‑book, the automated backup job that persists GPT‑3.5 checkpoint files to the configured backup bucket fails with the following error:
2026-11-05T14:32:07Z ERROR backup_job: storage class not found: "fast-ssd-backup"
2026-11-05T14:32:07Z INFO backup_job: aborting checkpoint upload for model "gpt-3.5"
The failure occurs only in the DR region; the same backup pipeline works normally in the primary region.
Root Cause
The backup job runs inside a Kubernetes Job that references a StorageClass by name. In the primary region the StorageClass fast-ssd-backup exists and points to a provisioner that creates gs:// buckets with the required performance tier. In the DR region the StorageClass object was never created because the infrastructure‑as‑code (IaC) manifest that defines it was scoped to the primary cluster only. When the backup job starts, the CSI driver queries the API server for the class, receives a NotFound response, and aborts the upload.
Additional contributing factors:
- The backup job uses the
dynamicProvisioning=trueflag, which forces the driver to create a new PersistentVolumeClaim (PVC) at runtime. Without a matchingStorageClass, the PVC remains inPendingstate. - Alerting rules only fire on PVC creation failures, not on the higher‑level backup job error, so the issue went unnoticed until the DR drill.
- The DR cluster runs a different version of the CSI driver (v1.4 vs v1.2) that logs the error as “storage class not found” rather than “PVC pending”. This change masked the symptom.
Debug
Step‑by‑step investigation that reproduced the failure:
- Inspect the backup job pod logs (already shown above).
- Check the PVC created by the job:
$ kubectl -n backup get pvc -l job-name=gpt35-backup-20231105 -o yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gpt35-checkpoint-pvc
namespace: backup
labels:
job-name: gpt35-backup-20231105
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
storageClassName: fast-ssd-backup
status:
phase: Pending
The PVC remains Pending because the storage class cannot be resolved.
3. Verify the existence of the storage class in the DR cluster:
$ kubectl get sc fast-ssd-backup
Error from server (NotFound): storageclass.storage.k8s.io "fast-ssd-backup" not found
4. Review the IaC repository for the storage class definition:
# storage-classes/fast-ssd-backup.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd-backup
provisioner: com.google.cloud.storage.csi
parameters:
type: pd-ssd
replication-type: regional-pd
reclaimPolicy: Delete
volumeBindingMode: Immediate
The manifest exists but is only applied to the primary cluster via a kustomize overlay.
Solution
Make the storage class definition part of the DR cluster’s provisioning pipeline and add a fallback to a known‑good class.
Step 1 – Add the storage class to the DR overlay
# kustomization.yaml (DR overlay)
resources:
- ../../base
- ../storage-classes/fast-ssd-backup.yaml # ← new line
Step 2 – Deploy the updated manifest
$ kubectl apply -k overlays/dr
storageclass.storage.k8s.io/fast-ssd-backup configured
Step 3 – Update the backup job to use a fallback class
Modify the job spec to reference a variable that resolves to the primary class if present, otherwise to standard-backup (a class guaranteed to exist in all regions).
# backup-job.yaml (excerpt)
apiVersion: batch/v1
kind: Job
metadata:
name: gpt35-backup
spec:
template:
spec:
containers:
- name: backup
image: myorg/gpt-backup:1.2.0
env:
- name: STORAGE_CLASS
valueFrom:
configMapKeyRef:
name: backup-config
key: storageClass # now populated by a ConfigMap
volumeMounts:
- name: checkpoint
mountPath: /data/checkpoint
volumes:
- name: checkpoint
persistentVolumeClaim:
claimName: gpt35-checkpoint-pvc
storageClassName: $(STORAGE_CLASS)
Update the ConfigMap to include the fallback logic (rendered by Helm or Kustomize):
# backup-config.yaml (template)
apiVersion: v1
kind: ConfigMap
metadata:
name: backup-config
data:
storageClass: {{ if .Values.storageClass }}{{ .Values.storageClass }}{{ else }}standard-backup{{ end }}
Step 4 – Redeploy the backup job
$ helm upgrade --install gpt-backup ./chart \
--set storageClass=fast-ssd-backup \
--namespace backup
Verify
Confirm that the PVC binds and the backup completes:
$ kubectl -n backup get pvc gpt35-checkpoint-pvc -o jsonpath='{.status.phase}'
Bound
Check the job logs for successful upload:
2026-11-05T14:38:12Z INFO backup_job: checkpoint uploaded to gs://dr-backup-bucket/gpt-3.5/20231105.ckpt
2026-11-05T14:38:12Z INFO backup_job: backup job completed successfully
Validate the object exists in the DR bucket:
$ gsutil ls gs://dr-backup-bucket/gpt-3.5/20231105.ckpt
gs://dr-backup-bucket/gpt-3.5/20231105.ckpt
Prevent
- IaC consistency checks: Add a CI pipeline step that runs
kubectl get scagainst all target clusters and fails if any expected class is missing. - Cross‑region ConfigMap defaults: Store the default storage class name in a ConfigMap that is replicated via
kubectl apply --server-sideto all clusters. - Alert on PVC Pending: Extend Prometheus rules to fire when any PVC in the
backupnamespace stays inPendingfor more than 5 minutes. - Version‑agnostic logging: Normalize backup job error messages to a common schema (e.g.,
{error_code:"STORAGE_CLASS_NOT_FOUND"}) so that alerting does not depend on driver version. - Disaster‑recovery test checklist: Include a step that verifies the presence of all storage classes before initiating DR backups.
Related Topic Hub: LLM Systems Troubleshooting Hub
FAQ
- Why does the backup succeed in the primary region but fail in DR? The primary cluster has the
fast-ssd-backupStorageClass defined; the DR cluster never received that manifest, so the PVC cannot be provisioned. - Can I use a static bucket instead of dynamic provisioning? Yes. Replace the PVC with a direct
gsutil cpcall and remove thestorageClassNamereference, but you lose the benefits of CSI‑managed lifecycle and automatic cleanup. - What if the CSI driver version changes again? Keep error handling generic (e.g., check for PVC
Pendingstatus) and rely on alerting rather than parsing driver‑specific log strings. - Is there a way to automatically copy missing StorageClasses to new clusters? Use a GitOps operator (Argo CD, Flux) that watches a central
storage-classesdirectory and syncs it to every cluster in the fleet. - How do I test the backup job without affecting production data? Deploy the job in a dedicated namespace with a mock bucket (e.g.,
gs://test-backup-bucket) and setdryRun=truein the container’s environment variables.