Tech ONTAP Blogs
Tech ONTAP Blogs
NetApp® Trident™ protect revolutionizes application data management, enhancing the functionality and availability of stateful Kubernetes applications supported by NetApp ONTAP storage systems and the NetApp Trident Container Storage Interface (CSI) storage provisioner. Compatible with a wide range of fully managed and self-managed Kubernetes offerings, Trident protect is your ultimate solution for safeguarding Kubernetes services across diverse platforms and regions.
For backing up persistent data to object storage, Trident protect employs a data mover (Restic or, by default, Kopia) to seamlessly transfer data from CSI snapshots of the persistent volumes to object storage. Both data movers perform incremental backups from the initial backup, ensuring an "incremental forever" approach.
The "incremental forever" backup strategy - where the first backup is a full backup and all subsequent backups are incremental - provides storage efficiency and faster backup times. However, it comes with certain challenges:
This can become problematic in these cases:
In summary, while incremental forever is efficient for storage and backup speed, the trade-off is slower and more complex restores, which can be a critical limitation in recovery scenarios.
To help you find the optimal backup strategy for your environment, Trident protect 25.06 introduces the ability to combine "incremental forever" with occasional full backups. This balanced approach enhances storage efficiency while improving restore speeds.
In this blog post, I'll guide you through the steps to leverage this new feature for both on-demand and scheduled backups. Let's dive in and elevate your backup strategy!
To follow along with this guide, ensure you have the following:
First, we quickly go through the setup of the test environment that we used throughout the blog.
We use two simple sample applications – NGINX and Alpine – in different namespaces on our Azure Kubernetes Service (AKS) test cluster.
The NGINX application is deployed in the namespace web and has one persistent volume (PV) backed by NetApp Azure File (ANF) storage, to which we have added some random files:
$ kubectl get all -n web
NAME READY STATUS RESTARTS AGE
pod/web-64cdb84b99-pnvnm 1/1 Running 0 5d23h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 1/1 1 1 5d23h
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-64cdb84b99 1 1 1 5d23h
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/nginxdata Bound pvc-1ff159f4-a3ea-45e5-bdb7-b0b5f3878466 50Gi RWX azure-netapp-files-standard <unset> 5d23h
$ kubectl -n web exec -it pod/web-64cdb84b99-pnvnm -- df -h /data
Filesystem Size Used Avail Use% Mounted on
10.21.2.7:/pvc-1ff159f4-a3ea-45e5-bdb7-b0b5f3878466 50G 41M 50G 1% /data
~$ k -n web exec -it pod/web-64cdb84b99-pnvnm -- ls -l /data
total 41164
-rw-r--r-- 1 nobody nogroup 10240 Oct 10 09:41 file1
-rw-r--r-- 1 nobody nogroup 10485760 Oct 16 08:15 file2
-rw-r--r-- 1 nobody nogroup 10485760 Oct 16 08:15 file3
-rw-r--r-- 1 nobody nogroup 10485760 Oct 16 08:15 file4
-rw-r--r-- 1 nobody nogroup 10485760 Oct 16 08:16 file5
We add the web namespace as application web to Trident protect:
$ tridentctl-protect create app web --namespaces web -n web
Application "web" created.
The second sample application is an Alpine container in the namespace alpine with two PVs backed by ANF, each populated with one random file, which we add as application alpine to Trident protect:
$ kubectl get all -n alpine
NAME READY STATUS RESTARTS AGE
pod/alpine-6cd7c6fdf7-fm9dm 1/1 Running 0 7m9s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/alpine 1/1 1 1 7m10s
NAME DESIRED CURRENT READY AGE
replicaset.apps/alpine-6cd7c6fdf7 1 1 1 7m10s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/alpinedata Bound pvc-9601c314-f343-4894-927a-c5ad2ac59644 50Gi RWX azure-netapp-files-standard <unset> 7m10s
persistentvolumeclaim/alpinedata-2 Bound pvc-95bd408e-b0c7-4b68-83d0-9d85dadb3e46 50Gi RWX azure-netapp-files-standard <unset> 7m9s
$ kubectl -n alpine exec -it pod/alpine-6cd7c6fdf7-fm9dm -- ls -l /data /data2
/data:
total 102808
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:41 file1
/data2:
total 102808
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:46 file1
~$ k -n alpine exec -it pod/alpine-6cd7c6fdf7-fm9dm -- df -h | grep data
50.0G 101.0M 49.9G 0% /data
50.0G 101.0M 49.9G 0% /data2
$ tridentctl-protect create app alpine --namespaces alpine -n alpine
Application "alpine" created.
We have these two Trident protect applications defined now:
$ tridentctl-protect get application -A
+-----------+--------+------------+-------+-------+
| NAMESPACE | NAME | NAMESPACES | STATE | AGE |
+-----------+--------+------------+-------+-------+
| alpine | alpine | alpine | Ready | 1d3h |
| web | web | web | Ready | 1d4h |
+-----------+--------+------------+-------+-------+
As object storage to store the backup data, we use the Azure Storage Blob container demo in the Azure Storage account puneptunetest, which is represented by the Trident protect appVault custom resource (CR) demo:
$ tridentctl-protect get appvault
+------+----------+-----------+-------+---------+--------+
| NAME | PROVIDER | STATE | ERROR | MESSAGE | AGE |
+------+----------+-----------+-------+---------+--------+
| demo | Azure | Available | | | 19d23h |
+------+----------+-----------+-------+---------+--------+
See the Trident protect documentation for detailed steps and examples how to create an appVault CR and how to set a password for the data mover repository. Take note of the Kopia data mover password you configured here, we’ll need it later on.
Now we’re ready to investigate full backups with Trident protect.
Let’s start with on-demand backups and see how we can control the creation of full and incremental on-demand backups.
The type of backup is configured by the annotation metadata.annotations.protect.trident.netapp.io/full-backup in Trident protect. If this annotation is set to true, the backup becomes non-incremental. If not specified, the backup follows the default incremental backup setting.
Here’s a sample yaml manifest for a full on-demand backup:
apiVersion: protect.trident.netapp.io/v1
kind: Backup
metadata:
namespace: my-app-namespace
name: my-cr-name
annotations:
protect.trident.netapp.io/full-backup: "true"
spec:
applicationRef: my-application
appVaultRef: appvault-name
dataMover: Kopia
When creating a full on-demand backup with Trident protect’s powerful CLI, you add the --full-backup flag to the tridentctl-protect create backup command.
Next, we create a full on-demand backup of the alpine application now:
$ tridentctl-protect create backup alpine-full-1 --app alpine --appvault demo --full-backup -n alpine
Backup "alpine-full-1" created.
$ tridentctl-protect get backup -n alpine
+---------------+--------+----------------+-----------+-------+-------+
| NAME | APP | RECLAIM POLICY | STATE | ERROR | AGE |
+---------------+--------+----------------+-----------+-------+-------+
| alpine-full-1 | alpine | Retain | Completed | | 6m18s |
+---------------+--------+----------------+-----------+-------+-------+
Let’s have a closer look at the corresponding backup yaml manifest once the backup finishes.
$ kubectl -n alpine get backup alpine-full-1 -o yaml
apiVersion: protect.trident.netapp.io/v1
kind: Backup
metadata:
annotations:
protect.trident.netapp.io/correlationid: 1f15357e-1330-4478-a902-a826a26d4a67
protect.trident.netapp.io/full-backup: "true"
creationTimestamp: "2025-10-21T14:00:51Z"
finalizers:
.. ..
generation: 1
name: alpine-full-1
namespace: alpine
ownerReferences:
- apiVersion: protect.trident.netapp.io/v1
kind: Application
name: alpine
uid: 8ef17301-234c-4d48-b7c4-2809d592f125
resourceVersion: "11054926"
uid: 1736c690-98a2-4a04-977b-94ef1fec264d
spec:
appVaultRef: demo
applicationRef: alpine
cleanupSnapshot: false
dataMover: Kopia
reclaimPolicy: Retain
status:
appArchivePath: alpine_8ef17301-234c-4d48-b7c4-2809d592f125/backups/alpine-full-1_1736c690-98a2-4a04-977b-94ef1fec264d
appVaultRef: demo
completionTimestamp: "2025-10-21T14:05:05Z"
conditions:
.. ..
- lastTransitionTime: "2025-10-21T14:00:52Z"
message: Not yet reconciled
reason: Pending
status: Unknown
type: OnFailurePostBackupExecHooksRunCompleted
latestRepositoryTimestamp: "20251021_140113"
postBackupExecHooksRunResults: []
postSnapshotExecHooksRunResults: []
preBackupExecHooksRunResults: []
preSnapshotExecHooksRunResults: []
progress:
volumeBackups:
- completionTimestamp: "2025-10-21T14:05:04Z"
progress:
bytesCompleted: 104857600
bytesRemaining: 0
bytesTotal: 104857600
updatedAt: "2025-10-21T14:04:01Z"
pvcUid: 9601c314-f343-4894-927a-c5ad2ac59644
repositoryPath: alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata_9601c314-f343-4894-927a-c5ad2ac59644/
snapshotID: eb9bdc9f7f54ed1bd57fd63482575a21
sourceVolumeSnapshot:
name: snapshot-d9ec9c10-2963-4d4f-beb3-61434d26938f-pvc-9601c314-f343-4894-927a-c5ad2ac59644
namespace: alpine
volumeBackupCompleted: true
volumeBackupCreated: true
volumeSnapshotContentCopyName: backup-1736c690-98a2-4a04-977b-94ef1fec264d-vsc-e97828fc-99c7-4d3a-88c5-7559fdd3d3a4
volumeSnapshotCopied: true
volumeSnapshotCopyDeleted: true
volumeSnapshotCopyName: backup-1736c690-98a2-4a04-977b-94ef1fec264d-vs-6ada2115-8252-4e91-88c8-fbd227193453
volumeSnapshotCopyReadyToUse: true
- completionTimestamp: "2025-10-21T14:04:51Z"
progress:
bytesCompleted: 104857600
bytesRemaining: 0
bytesTotal: 104857600
updatedAt: "2025-10-21T14:03:50Z"
pvcUid: 95bd408e-b0c7-4b68-83d0-9d85dadb3e46
repositoryPath: alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata-2_95bd408e-b0c7-4b68-83d0-9d85dadb3e46/
snapshotID: 0d7c8b2c34c17d0a48d1529d399e0c52
sourceVolumeSnapshot:
name: snapshot-d9ec9c10-2963-4d4f-beb3-61434d26938f-pvc-95bd408e-b0c7-4b68-83d0-9d85dadb3e46
namespace: alpine
volumeBackupCompleted: true
volumeBackupCreated: true
volumeSnapshotContentCopyName: backup-1736c690-98a2-4a04-977b-94ef1fec264d-vsc-3589ff58-854a-4b64-8f1b-78af102193db
volumeSnapshotCopied: true
volumeSnapshotCopyDeleted: true
volumeSnapshotCopyName: backup-1736c690-98a2-4a04-977b-94ef1fec264d-vs-0d01ecc5-0b44-47d9-a4eb-40821d49eddc
volumeSnapshotCopyReadyToUse: true
sourceSnapshotName: backup-1736c690-98a2-4a04-977b-94ef1fec264d
state: Completed
In the metadata, we can see that the full backup annotation is set correctly to protect.trident.netapp.io/full-backup: "true":
$ kubectl -n alpine get backup alpine-full-1 -o yaml | yq '.metadata.annotations."protect.trident.netapp.io/full-backup"'
true
As we have two PVs to backup, we see two corresponding volumeBackups in the progress section of the yaml, each with its own repositoryPath value:
$ kubectl -n alpine get backup alpine-full-1 -o yaml | yq '.status.progress.volumeBackups[].repositoryPath'
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata_9601c314-f343-4894-927a-c5ad2ac59644/
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata-2_95bd408e-b0c7-4b68-83d0-9d85dadb3e46/
The two repositoryPath values allow us to connect to the Kopia repositories that were created during the backup in our Azure Storage container using the Kopia CLI. Let’s connect to the first Kopia repository:
$ kopia repository connect azure --storage-account=puneptunetest --storage-key="<REDACTED>" --container=demo --prefix=$(k -n alpine get backup alpine-full-1 -o yaml | yq '.status.progress.volumeBackups[0].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
Now we can list the Kopia snapshots in the first Kopia repository, where we have on snapshot corresponding to the first (full) backup, containing the backup of one file:
$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-21 16:01:41 CEST k8900e92b09583eec8c3932762fb57e17 104.9 MB drwxrwxrwx files:1 dirs:1 (latest-1)
We see that same situation in the second Kopia repository:
$ kopia repository connect azure --storage-account=puneptunetest --storage-key="<REDACTED>" --container=demo --prefix=$(k -n alpine get backup alpine-full-1 -o yaml | yq '.status.progress.volumeBackups[1].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
~$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-21 16:01:29 CEST k32e40ed699bc3e3d938c0ea65497f95f 104.9 MB drwxrwxrwx files:1 dirs:1 (latest-1)
Now, let’s see what happens when we add another file to the /data directory in the Alpine container and run an incremental backup afterwards.
$ kubectl -n alpine exec -it pod/alpine-6cd7c6fdf7-fm9dm -- ls -l /data /data2
/data:
total 205616
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:41 file1
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 14:13 file2
/data2:
total 102808
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:46 file1
We start another on-demand backup alpine-inc-1 now, not specifying the --full-backup flag will make it an incremental backup.
$ tridentctl-protect create backup alpine-inc-1 --app alpine --appvault demo -n alpine
Backup "alpine-inc-1" created.
$ tridentctl-protect get backup -n alpine
+---------------+--------+----------------+-----------+-------+-------+
| NAME | APP | RECLAIM POLICY | STATE | ERROR | AGE |
+---------------+--------+----------------+-----------+-------+-------+
| alpine-full-1 | alpine | Retain | Completed | | 2h45m |
| alpine-inc-1 | alpine | Retain | Completed | | 4m15s |
+---------------+--------+----------------+-----------+-------+-------+
Let’s list the full-backup annotation next to each backup in the alpine namespace:
$ for i in $(k -n alpine get backups | awk '/alpine/ {print $1}'); do type=$(k -n alpine get backup ${i} -o yaml | yq '.metadata.annotations."protect.trident.netapp.io/full-backup"'); printf "${i}\t ${type} \n";done
alpine-full-1 true
alpine-inc-1 null
Checking for the repositoryPath values in the newly created backup alpine-inc-1, we see that the values didn’t change (look at the timestamp ../kopia_<timestamp>/ in the middle of the repositoryPath name):
$ kubectl -n alpine get backup alpine-inc-1 -o yaml | yq '.status.progress.volumeBackups[].repositoryPath'
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata_9601c314-f343-4894-927a-c5ad2ac59644/
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251021_140113/alpine/alpinedata-2_95bd408e-b0c7-4b68-83d0-9d85dadb3e46/
The incremental backup data was written to the same Kopia repositories, which we can see when looking at the Kopia repositories again. The first Kopia repository has a second, new snapshot as we added another file to /data:
$ kopia repository connect azure --storage-account=puneptunetest --storage-key="AUdQ0hKBW1xw6e6GwqxHGXuiEdrrrmWUY1Ft9E26yDx7dylKW/CUsbGG2/ff36RJ2K2PnAoHAUtC+AStgmsdiA==" --container=demo --prefix=$(k -n alpine get backup alpine-inc-1 -o yaml | yq '.status.progress.volumeBackups[0].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
~$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-21 16:01:41 CEST k8900e92b09583eec8c3932762fb57e17 104.9 MB drwxrwxrwx files:1 dirs:1 (latest-2)
2025-10-21 18:42:47 CEST k00275f811aca280b34c4c9599c1a7278 209.7 MB drwxrwxrwx files:2 dirs:1 (latest-1)
While the second Kopia repository has an additional, identical snapshot as we didn’t change anything in the /data2 directory.
$ kopia repository connect azure --storage-account=puneptunetest --storage-key="AUdQ0hKBW1xw6e6GwqxHGXuiEdrrrmWUY1Ft9E26yDx7dylKW/CUsbGG2/ff36RJ2K2PnAoHAUtC+AStgmsdiA==" --container=demo --prefix=$(k -n alpine get backup alpine-inc-1 -o yaml | yq '.status.progress.volumeBackups[1].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
~$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-21 16:01:29 CEST k32e40ed699bc3e3d938c0ea65497f95f 104.9 MB drwxrwxrwx files:1 dirs:1 (latest-1..2)
+ 1 identical snapshots until 2025-10-21 18:42:48 CEST
In a last step, let’s add a new file to the /data2 directory and run another full backup afterwards.
$ kubectl -n alpine exec -it pod/alpine-6cd7c6fdf7-fm9dm -- ls -l /data /data2
/data:
total 205616
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:41 file1
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 14:13 file2
/data2:
total 205616
-rw-r--r-- 1 nobody nobody 104857600 Oct 21 13:46 file1
-rw-r--r-- 1 nobody nobody 104857600 Oct 22 07:56 file2
$ tridentctl-protect create backup alpine-full-2 --app alpine --appvault demo --full-backup -n alpine
Backup "alpine-full-2" created.
$ tridentctl-protect get backups -n alpine
+---------------+--------+----------------+-----------+-------+--------+
| NAME | APP | RECLAIM POLICY | STATE | ERROR | AGE |
+---------------+--------+----------------+-----------+-------+--------+
| alpine-full-1 | alpine | Retain | Completed | | 19h57m |
| alpine-full-2 | alpine | Retain | Completed | | 1h50m |
| alpine-inc-1 | alpine | Retain | Completed | | 17h16m |
+---------------+--------+----------------+-----------+-------+--------+
$ printf "BACKUP \t\t FULL?\n"; for i in $(k -n alpine get backups | awk '/alpine/ {print $1}'); do type=$(k -n alpine get backup ${i} -o yaml | yq '.metadata.annotations."protect.trident.netapp.io/full-backup"'); printf "${i}\t ${type} \n";done
BACKUP FULL?
alpine-full-1 true
alpine-full-2 true
alpine-inc-1 null
Checking the repositoryPath values of the second full backup alpine-full-2, we see that new Kopia repositories were created (watch the timestamp ../kopia_<timestamp>/ in the middle of the repositoryPath name).
$ kubectl -n alpine get backup alpine-full-2 -o yaml | yq '.status.progress.volumeBackups[].repositoryPath'
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251022_080816/alpine/alpinedata_9601c314-f343-4894-927a-c5ad2ac59644/
alpine_8ef17301-234c-4d48-b7c4-2809d592f125/kopia_20251022_080816/alpine/alpinedata-2_95bd408e-b0c7-4b68-83d0-9d85dadb3e46/
Each of the new Kopia repositories now contains one Kopia snapshot, protecting the two data files in each directory /data and /data2:
$ kopia repository connect azure --storage-account=puneptunetest --storage-key="AUdQ0hKBW1xw6e6GwqxHGXuiEdrrrmWUY1Ft9E26yDx7dylKW/CUsbGG2/ff36RJ2K2PnAoHAUtC+AStgmsdiA==" --container=demo --prefix=$(k -n alpine get backup alpine-full-2 -o yaml | yq '.status.progress.volumeBackups[0].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
~$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-22 10:08:28 CEST k74861c2a36fa0237c7e601638b2d3b9f 209.7 MB drwxrwxrwx files:2 dirs:1 (latest-1)$ kopia repository connect azure --storage-account=puneptunetest --storage-key="AUdQ0hKBW1xw6e6GwqxHGXuiEdrrrmWUY1Ft9E26yDx7dylKW/CUsbGG2/ff36RJ2K2PnAoHAUtC+AStgmsdiA==" --container=demo --prefix=$(k -n alpine get backup alpine-full-2 -o yaml | yq '.status.progress.volumeBackups[1].repositoryPath')
Enter password to open repository:
Connected to repository.
NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/Users/patricu/Library/Application Support/kopia/repository.config.update-info.json".
~$ kopia snapshot list --all
root@protect.trident.netapp.io:/sourceVolume
2025-10-22 10:08:28 CEST kc054b7ac1b577b6d627bc28fd87d5177 209.7 MB drwxrwxrwx files:2 dirs:1 (latest-1)
Now that we examined how full on-demand backups work in Trident protect, let’s see how can leverage this new capability for scheduled backups with Trident protect’s scheduler.
With the scheduler, you can schedule non-incremental full backups by using the metadata.annotations.protect.trident.netapp.io/full-backup-rule annotation. By default, all backups are incremental. You can set it to Always for constant full backups for all backup granularities, or for daily granularity, you can specify the weekdays (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday) on which a full backup should occur. When using the Trident protect CLI to create a schedule, you can use the --full-backup-rule flag to specify the weekdays for full backups with a daily granularity of the backup schedule or set it to Always for constant full backups.
Let’s give it a try by creating a schedule with tridentctl-protect for daily backups of our second sample application web. We want the daily backups to run at 12:12 UTC and have the scheduler create full backups on Monday, Wednesday, Friday, and Sunday, while retaining the last 10 backups.
$ tridentctl-protect create schedule --app web --appvault demo --backup-retention 10 --granularity Daily --hour 12 --minute 12 --full-backup-rule "Monday,Wednesday,Friday,Sunday" -n web
Schedule "web-qfr01u" created.
A kubectl describe of the newly created schedule shows us the correct setting of the full-backup-rule annotation:
$ kubectl -n web describe schedule web-qfr01u
Name: web-qfr01u
Namespace: web
Labels: <none>
Annotations: protect.trident.netapp.io/full-backup-rule: Monday,Wednesday,Friday,Sunday
API Version: protect.trident.netapp.io/v1
Kind: Schedule
Metadata:
Creation Timestamp: 2025-10-16T08:51:51Z
Generation: 1
Owner References:
API Version: protect.trident.netapp.io/v1
Kind: Application
Name: web
UID: 0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62
Resource Version: 12337070
UID: 97f37f06-9c3e-4af6-8db0-9ba99b13eaf4
Spec:
App Vault Ref: demo
Application Ref: web
Backup Retention: 10
Data Mover: Kopia
Day Of Month:
Day Of Week:
Enabled: true
Granularity: Daily
Hour: 12
Minute: 12
Recurrence Rule:
Replication Retention: 0
Snapshot Retention: 0
Events: <none>
We check back after the first backup run on Thursday, Oct 16, 2025:
$ date
Thu Oct 16 14:46:07 CEST 2025
$ tridentctl-protect get backup -n web
+----------------------------+-----+----------------+-----------+-------+--------+
| NAME | APP | RECLAIM POLICY | STATE | ERROR | AGE |
+----------------------------+-----+----------------+-----------+-------+--------+
| daily-90652-20251016121200 | web | Retain | Completed | | 34m10s |
+----------------------------+-----+----------------+-----------+-------+--------+
As this is the initial scheduled backup, it is implicitly a full backup, despite its full-backup annotation not being set to true:
$ kubectl -n web get backup daily-90652-20251016121200 -o yaml | yq '.metadata.annotations."protect.trident.netapp.io/full-backup"'
null
The application web has only one PV, so only one Kopia repository was created for the initial scheduled backup:
$ kubectl -n web get backup daily-90652-20251016121200 -o yaml | yq '.status.progress.volumeBackups[].repositoryPath'
web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
Checking back on Friday next week, we confirm that the scheduled backups on Friday, Sunday, Monday, Wednesday, and again Friday are full backups, and each full backup was written to a new Kopia repository (watch the timestamp ../kopia_<timestamp>/ in the middle of the repositoryPath name):
$ tridentctl-protect get backup -n web
+----------------------------+-----+----------------+-----------+-------+--------+
| NAME | APP | RECLAIM POLICY | STATE | ERROR | AGE |
+----------------------------+-----+----------------+-----------+-------+--------+
| daily-90652-20251016121200 | web | Retain | Completed | | 8d |
| daily-90652-20251017121200 | web | Retain | Completed | | 7d |
| daily-90652-20251018121200 | web | Retain | Completed | | 6d |
| daily-90652-20251019121200 | web | Retain | Completed | | 5d |
| daily-90652-20251020121200 | web | Retain | Completed | | 4d |
| daily-90652-20251021121200 | web | Retain | Completed | | 3d |
| daily-90652-20251022121200 | web | Retain | Completed | | 2d |
| daily-90652-20251023121200 | web | Retain | Completed | | 1d |
| daily-90652-20251024121200 | web | Retain | Completed | | 37m29s |
+----------------------------+-----+----------------+-----------+-------+--------+
$ printf "BACKUP \t\t\t\t FULL? \t REPO \n"; for i in $(k -n web get backups | awk '/daily/ {print $1}'); do type=$(k -n web get backup ${i} -o yaml | yq '.metadata.annotations."protect.trident.netapp.io/full-backup"'); repo=$(k -n web get backup ${i} -o yaml | yq '.status.progress.volumeBackups[0].repositoryPath'); printf "${i}\t ${type}\t ${repo}\n"; done
BACKUP FULL? REPO
daily-90652-20251016121200 null web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251017121200 true web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251017_121216/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251018121200 null web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251017_121216/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251019121200 true web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251019_121213/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251020121200 true web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251020_121215/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251021121200 null web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251020_121215/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251022121200 true web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251022_121215/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251023121200 null web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251022_121215/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
daily-90652-20251024121200 true web_0a87fec1-1c9a-4ef4-bee2-9b94a3df5c62/kopia_20251024_121214/web/nginxdata_1ff159f4-a3ea-45e5-bdb7-b0b5f3878466/
In conclusion, NetApp® Trident™ protect 25.06 significantly enhances your Kubernetes application data management by introducing the ability to schedule full backups alongside incremental ones. This new feature offers a compromise between storage efficiency and faster restore times, addressing the challenges posed by the "incremental forever" backup strategy. By leveraging both on-demand and scheduled full backups, you can ensure robust data protection and swift recovery for your critical systems.
Now that you're equipped with the knowledge to implement these capabilities, it's time to put them into action! Follow the steps outlined in this guide to set up and optimize your backup strategy with Trident protect. Ensure you have the prerequisites in place and start creating on-demand and scheduled backups to safeguard your Kubernetes applications effectively.
Don't wait until it's too late—enhance your data protection strategy today with Trident protect 25.06!
For more detailed instructions and examples, refer to the Trident protect documentation. If you have any questions or need further assistance, feel free to reach out to our support team or join the NetApp community forums.
Happy backing up!