Schedule icon
List icon
Commands icon
Docker icon
Log icon
If icon
Pause icon
Loop icon
Fail icon
SlackIncomingWebhook icon

Prune noncurrent object versions billed forever behind a versioned bucket

Find the noncurrent object versions a versioned bucket keeps forever, prune them by version id behind an approval, then re-read the version listing.

Categories
CloudInfrastructure

Turning on bucket versioning is one click and it is the right call. With no lifecycle expiry rule it is also a bill that only grows. Every overwrite and every delete keeps its predecessor forever. A key written daily for a year holds 365 noncurrent versions plus a delete marker, all billed, none of them visible in a normal object listing. Nothing warns you, because the bucket looks the same size in the console object count. This blueprint measures that hidden mass, selects the noncurrent versions that are genuinely safe to drop, deletes them by version id behind an approval, then re-reads the version listing and fails the run if any targeted version survived.

This blueprint was created by zkasuran.

Safety

A version delete is not recoverable. There is no undo and no recycle bin, so four rules are in the code rather than only in this text.

  • A current version is never a candidate. The selection only ever looks at entries whose IsLatest is false. The delete call always carries an explicit --version-id, so it cannot create a delete marker by accident.
  • keep_noncurrent_versions holds back the newest N noncurrent versions of every key whatever their age, so a rollback window always survives.
  • min_noncurrent_age_minutes holds back anything younger than the floor. A version written minutes ago is very likely the thing somebody is about to roll back to.
  • A delete marker is itself a version. Removing one resurrects the object it was hiding. delete_marker_policy defaults to keep, so none are touched. prune_noncurrent removes only markers that are not the latest version of their key, which means the key was written again after the delete and the marker is pure history. A latest delete marker is never removed under any setting.

On top of that the flow is read-only by default (dry_run: true, auto_remediate: false), the destructive step sits behind a Pause, the delete loop runs serially. max_versions_to_prune refuses a run that would delete more than you expect.

How it works

  1. list_current_objects (io.kestra.plugin.minio.List) lists the bucket the way a normal client sees it, current versions only. That is the number the console shows.
  2. list_version_entries (io.kestra.plugin.minio.List with includeVersions: true) lists the same bucket again with version listing on, so every stored version and every delete marker comes back as its own entry. The gap between the two listings is the mass you are paying for and cannot see.
  3. audit_versions (io.kestra.plugin.scripts.shell.Commands on the AWS CLI image) reads the full inventory with aws s3api list-object-versions, which carries the version ids a plugin listing does not expose. It groups noncurrent versions by key, sorts each group newest first, skips the keep window, drops anything inside the age floor, applies the delete marker policy, then writes the plan and reports the counts. An unreachable endpoint fails the task instead of reading as a clean bucket.
  4. log_bloat (io.kestra.plugin.core.log.Log) records both listings side by side, so the measurement exists whether or not anything is pruned.
  5. gate (io.kestra.plugin.core.flow.If) deletes only when auto_remediate is true, dry_run is false and the selection is not empty. ceiling_check refuses a selection larger than max_versions_to_prune.
  6. confirm (io.kestra.plugin.core.flow.Pause) holds for a person. On approval remediate_versions (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs aws s3api delete-object --version-id once per selected version.
  7. verify_pruned re-reads the version listing and compares it with the plan. No targeted version id may still be present. Every current version recorded before the prune must still be there with the same version id and the same ETag. assert_clean fails the run otherwise.
  8. report posts a summary to Slack. The errors block posts a separate alert when the audit fails, a delete is rejected or a targeted version survives.

What you get

  • The one number a versioned bucket does not show you: visible objects against stored version entries, measured through the MinIO plugin rather than estimated.
  • A prune that is precise to a single version id, so a key with one version is never touched and a current version is never a candidate.
  • A rollback window and an age floor that are inputs, not assumptions, so the policy matches what your team actually needs to restore.
  • An explicit, documented decision about delete markers, including the fact that removing one brings a deleted object back.
  • A re-read of the version listing that fails the run when the prune did not land, so a silently rejected delete cannot be reported as reclaimed space.

Who it's for

Platform and data engineers running MinIO or any S3-compatible object store where versioning was switched on for safety and no lifecycle rule was ever written. If your bucket size keeps climbing while the object count stays flat, this is why.

Why orchestrate this with Kestra

A bucket lifecycle rule is fire and forget. It cannot pause for a human, it cannot refuse a run that looks mis-scoped. It never tells you what it removed. Kestra gives the same job a dry run, an approval gate with the exact selection in front of the approver, a ceiling, a serial delete loop and a re-read that fails the run when the state did not change. The schedule keeps the measurement running daily in safe mode, so the bloat is tracked long before anybody decides to prune it.

Tested end to end

Run against MinIO with versioning enabled and the AWS CLI image on the same Docker network. Every path was exercised for real and confirmed with aws s3api list-object-versions against the bucket, not with the Kestra log.

  • A clean bucket, where every key sits inside the keep window, reports 0 prunable and writes nothing.
  • A bucket seeded with one key written five times (one current plus four noncurrent), one key carrying a latest delete marker over an older version, one key carrying a historical delete marker plus one key with a single version, reports exactly the aged noncurrent versions outside the keep window and leaves the single-version key alone.
  • dry_run: true reports the full selection and deletes nothing.
  • auto_remediate: true with dry_run: false pauses, then on approval deletes every selected version. The re-read confirms the version entries dropped while every current version kept its version id and its ETag.
  • Declining the pause leaves every version in place.
  • Raising keep_noncurrent_versions moves the boundary exactly as expected, version by version.
  • A credential without s3:DeleteObjectVersion makes the delete fail and the run route to the errors Slack alert.

Prerequisites

Secrets

  • MINIO_ACCESS_KEY_ID and MINIO_SECRET_KEY_ID: credentials that can list object versions and delete an object version. Reading alone is enough for the default safe mode.
  • SLACK_WEBHOOK_URL: incoming webhook for the run summary and the failure alert.

Inputs

  • bucket: the versioned bucket to audit.
  • prefix: restrict the audit to one key prefix. Empty audits the whole bucket.
  • keep_noncurrent_versions: how many noncurrent versions to keep per key, newest first. Default 3.
  • min_noncurrent_age_minutes: nothing younger than this is pruned. Default 43200, which is 30 days.
  • delete_marker_policy: keep (default) or prune_noncurrent.
  • max_versions_to_prune: ceiling per run. Default 200.
  • minio_endpoint, minio_access_key, minio_secret_key: connection details.
  • awscli_image: image carrying the AWS CLI. Neither the MinIO plugin nor the S3 plugin ships a task that deletes one specific object version, so the inventory and the delete run through this CLI.
  • docker_network: network the CLI container joins to reach the endpoint.
  • dry_run and auto_remediate: the safety gates. Both default to safe.

Quick start

  1. Add the three secrets, point minio_endpoint at your endpoint and set docker_network to the network that reaches it.
  2. Run it as it is, with bucket set. It only measures. Read log_bloat to see the gap between the visible object count and the stored version entries.
  3. Raise min_noncurrent_age_minutes and keep_noncurrent_versions until the selection matches what your team would accept losing.
  4. Set dry_run: false and auto_remediate: true, read the selection on the paused execution, then approve. The run fails if any targeted version survives.
  5. Enable the schedule once you are happy. It runs in safe mode, so it keeps measuring and never deletes on its own.

How to extend

  • Point minio_endpoint at AWS S3 to run the same audit there. The plugin and the CLI call are both plain S3.
  • Send the per-key selection to a ticket instead of Slack, so an owner approves their own prefix.
  • Write prunable_bytes to a time series each day and alert on the growth rate rather than the absolute number.
  • Loop the audit over several buckets and keep one ceiling per bucket.
  • Once the numbers are steady, use them to write the lifecycle rule you should have had. Keep this flow as the check that the rule is doing its job.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.