Schedule backup snapshots
This tutorial shows how to use a Kubernetes CronJob to automate backup storage rotation, creating timestamped snapshots at regular intervals.
Challenge
Section titled “Challenge”When backing up topics, the storage preserves all historical data including old record values and tombstones. Retention policies on the Backup do not handle compacted topics, as they work on time-based boundaries rather than key-based compaction.
Scheduled snapshots help you:
- Create fresh snapshots with current data
- Achieve faster restore times from smaller, bounded snapshots
- Maintain a clear audit trail with timestamped backup boundaries
- Simplify retention policy enforcement
Solution
Section titled “Solution”A Kubernetes CronJob automates storage rotation by creating new timestamped Storage resources at regular intervals. Each snapshot represents the complete state of your data at the time it was created.
Prerequisites
Section titled “Prerequisites”- A Kannika Armory instance available, running on a Kubernetes environment.
- Local installation of the
kubectlbinary.
Refer to the Setup section to set up the lab environment.
Scenario
Section titled “Scenario”In this tutorial, you will simulate a financial services company that needs to meet regulatory compliance requirements:
- Data: Financial transactions that must be backed up daily
- Requirement: Daily snapshots with verifiable timestamps for auditors
- Retention: Clear boundaries enabling 7-year retention policies
- Recovery: Ability to restore data as it existed on any specific date
The goal is to configure automated daily snapshots that create new timestamped Storage resources, enabling auditors to request data from any specific date and supporting point-in-time recovery.
Run the setup script:
curl -fsSL https://raw.githubusercontent.com/kannika-io/armory-examples/main/install.sh | bash -s -- schedule-snapshotsOr clone the armory-examples repository:
git clone https://github.com/kannika-io/armory-examples.gitcd armory-examples./setup schedule-snapshotsThis sets up:
Kubernetes cluster: kannika-kind├── Namespace: kannika-system│ └── Kannika Armory└── Namespace: kannika-data ├── EventHub: prod-kafka → kafka-source:9092 ├── Storage: prod-storage └── Backup: prod-backup
Kafka: kafka-source:9092 (localhost:9092)└── Topic: transactionsStep 1: Create the RBAC resources
Section titled “Step 1: Create the RBAC resources”The CronJob needs permission to read and update Backups, and to create new Storage resources. Create a ServiceAccount with the required permissions.
Apply the RBAC configuration:
kubectl apply -f tutorials/schedule-snapshots/k8s/backup-storage-rotate.rbac.yaml# https://github.com/kannika-io/armory-examples/blob/main/tutorials/schedule-snapshots/k8s/backup-storage-rotate.rbac.yamlapiVersion: v1kind: ServiceAccountmetadata: name: backup-storage-rotate-sa namespace: kannika-data---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: backup-storage-rotate-role namespace: kannika-datarules:- apiGroups: ["kannika.io"] resources: ["backups"] verbs: ["get", "list", "watch", "patch", "update"]- apiGroups: ["kannika.io"] resources: ["storages"] verbs: ["get", "list", "watch", "create", "update"]- apiGroups: ["apps"] resources: ["statefulsets"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: backup-storage-rotate-rb namespace: kannika-datasubjects:- kind: ServiceAccount name: backup-storage-rotate-sa namespace: kannika-dataroleRef: kind: Role name: backup-storage-rotate-role apiGroup: rbac.authorization.k8s.ioStep 2: Deploy the CronJob
Section titled “Step 2: Deploy the CronJob”The CronJob performs the storage rotation by:
- Disabling the Backup to flush any pending segments
- Reading the current Storage configuration
- Creating a new Storage with a timestamped name
- Updating the Backup to point to the new Storage
- Re-enabling the Backup
Apply the CronJob:
kubectl apply -f tutorials/schedule-snapshots/k8s/rotate-backup-storage.cronjob.yaml# https://github.com/kannika-io/armory-examples/blob/main/tutorials/schedule-snapshots/k8s/rotate-backup-storage.cronjob.yamlapiVersion: batch/v1kind: CronJobmetadata: name: rotate-backup-storage namespace: kannika-dataspec: schedule: "0 0 * * *" # Run at midnight daily jobTemplate: spec: template: spec: serviceAccountName: backup-storage-rotate-sa restartPolicy: OnFailure containers: - name: rotate image: alpine:3 command: - /bin/sh - -c - | set -e
# Install kubectl wget -qO /usr/local/bin/kubectl \ "https://dl.k8s.io/release/$(wget -qO- https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" chmod +x /usr/local/bin/kubectl
BACKUP_NAME="prod-backup" NAMESPACE="kannika-data" TIMESTAMP=$(date +%Y%m%d%H%M%S)
echo "Starting storage rotation at ${TIMESTAMP}"
# Step 1: Disable the backup to flush segments echo "Disabling backup..." kubectl patch backup ${BACKUP_NAME} -n ${NAMESPACE} \ --type merge \ -p '{"spec":{"enabled":false}}'
# Wait for backup StatefulSet to scale down echo "Waiting for backup to stop..." kubectl wait statefulset -l io.kannika/backup=${BACKUP_NAME} -n ${NAMESPACE} \ --for=jsonpath='{.status.replicas}'=0 \ --timeout=60s
# Step 2: Get current storage configuration CURRENT_STORAGE=$(kubectl get backup ${BACKUP_NAME} -n ${NAMESPACE} \ -o jsonpath='{.spec.sink}') echo "Current storage: ${CURRENT_STORAGE}"
# Extract base name (remove any existing timestamp suffix) BASE_NAME=$(echo ${CURRENT_STORAGE} | sed 's/-[0-9]\{14\}$//') NEW_STORAGE_NAME="${BASE_NAME}-${TIMESTAMP}"
echo "New storage name: ${NEW_STORAGE_NAME}"
# Step 3: Create new storage with timestamp # Extract the spec from current storage VOLUME_CAPACITY=$(kubectl get storage ${CURRENT_STORAGE} -n ${NAMESPACE} \ -o jsonpath='{.spec.volume.capacity}')
echo "Creating new storage..." cat <<EOF | kubectl apply -f - apiVersion: kannika.io/v1alpha kind: Storage metadata: name: ${NEW_STORAGE_NAME} namespace: ${NAMESPACE} spec: volume: capacity: ${VOLUME_CAPACITY} EOF
# Step 4: Update backup to use new storage echo "Updating backup to use new storage..." kubectl patch backup ${BACKUP_NAME} -n ${NAMESPACE} \ --type merge \ -p "{\"spec\":{\"sink\":\"${NEW_STORAGE_NAME}\"}}"
# Step 5: Re-enable the backup echo "Re-enabling backup..." kubectl patch backup ${BACKUP_NAME} -n ${NAMESPACE} \ --type merge \ -p '{"spec":{"enabled":true}}'
echo "Storage rotation complete" echo "New storage: ${NEW_STORAGE_NAME}"Verify the CronJob was created:
kubectl get cronjob rotate-backup-storage -n kannika-dataNAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGErotate-backup-storage 0 0 * * * False 0 <none> 10sStep 3: Test the rotation manually
Section titled “Step 3: Test the rotation manually”Instead of waiting until midnight, trigger the CronJob manually to verify it works:
kubectl create job --from=cronjob/rotate-backup-storage test-rotation -n kannika-dataWatch the job progress:
kubectl logs -f job/test-rotation -n kannika-dataYou should see output similar to:
Starting storage rotation at 20260202120000Disabling backup...backup.kannika.io/prod-backup patchedWaiting for backup to stop...statefulset.apps/prod-backup condition metCurrent storage: prod-storageNew storage name: prod-storage-20260202120000Creating new storage...storage.kannika.io/prod-storage-20260202120000 createdUpdating backup to use new storage...backup.kannika.io/prod-backup patchedRe-enabling backup...backup.kannika.io/prod-backup patchedStorage rotation completeNew storage: prod-storage-20260202120000Step 4: Verify the results
Section titled “Step 4: Verify the results”Check that the new Storage was created:
kubectl get storage -n kannika-dataNAME AGEprod-storage 10mprod-storage-20260202120000 30sVerify the Backup now points to the new Storage:
kubectl get backup prod-backup -n kannika-data -o jsonpath='{.spec.sink}'prod-storage-20260202120000Verify the Backup is running again:
kubectl get backup prod-backup -n kannika-dataNAME STATUSprod-backup StreamingExamine the new Storage configuration:
kubectl get storage prod-storage-20260202120000 -n kannika-data -o yamlapiVersion: kannika.io/v1alphakind: Storagemetadata: name: prod-storage-20260202120000 namespace: kannika-dataspec: volume: capacity: 1GiEach new Storage resource represents a distinct backup snapshot.
Step 5: Simulate an audit request
Section titled “Step 5: Simulate an audit request”To demonstrate the compliance benefit, imagine an auditor requests data from a specific date.
List all available snapshots:
kubectl get storage -n kannika-dataNAME AGEprod-storage 10mprod-storage-20260202120000 1mEach timestamped Storage represents data as it existed at that point in time. To restore data from a specific date, you can create a Restore pointing to the corresponding Storage.
Cleanup
Section titled “Cleanup”To clean up the test job:
kubectl delete job test-rotation -n kannika-dataTo remove all tutorial resources:
./teardownSummary
Section titled “Summary”In this tutorial, you learned how to:
- Set up RBAC permissions for automated storage rotation
- Deploy a CronJob that creates timestamped backup snapshots
- Verify that each snapshot creates a separate Storage resource
- Understand how timestamped snapshots enable compliance auditing
This pattern ensures your organization can:
- Meet retention requirements: Each snapshot has clear boundaries for retention policies
- Support audits: Provide verifiable point-in-time data on request
- Enable recovery: Restore data as it existed on any specific date
- Automate compliance: No manual intervention required for daily snapshots
For production use, consider adjusting the CronJob schedule to match your compliance requirements (hourly, daily, weekly) and implementing alerting for failed rotations.