id: alibaba-oss-storage-class-cost-governance-sentinel
namespace: company.team
description: |
List objects in an Alibaba Cloud OSS bucket on a schedule, classify the
result HEALTHY or STORAGE_CLASS_DRIFT_DETECTED based on how many objects
are still sitting on the Standard storage class well past a configured
age threshold, log clean passes, and alert Slack with the exact keys and
ages before unmanaged storage-class drift becomes a cost problem.
triggers:
- id: periodic_storage_class_check
type: io.kestra.plugin.core.trigger.Schedule
description: "Checks once a day - object age and storage class change slowly, so
daily resolution is enough to catch lifecycle drift well before it
compounds into a meaningful cost difference. Shipped disabled; enable it
once alibaba_region and bucket_name point at a real OSS bucket and a
manual run has been verified."
cron: "0 9 * * *"
disabled: true
inputs:
alibaba_region: "cn-hangzhou"
bucket_name: "REPLACE-WITH-YOUR-BUCKET-NAME"
object_prefix: ""
max_object_age_days: 90
max_files_to_scan: 1000
inputs:
- id: alibaba_region
type: STRING
displayName: "Alibaba Cloud Region"
defaults: "cn-hangzhou"
description: "Passed to the List task's own region property; the OSS endpoint is
derived from it unless endpointOverride is set."
- id: bucket_name
type: STRING
displayName: "OSS Bucket Name"
defaults: "REPLACE-WITH-YOUR-BUCKET-NAME"
description: "Passed to the List task's own bucket property. Must be replaced
with a real bucket name - there is no safe universal default."
- id: object_prefix
type: STRING
displayName: "Object Key Prefix"
defaults: ""
description: "Passed to the List task's own prefix property. Leave empty to scan
the whole bucket, or scope the audit to one logical folder."
- id: max_object_age_days
type: INT
displayName: "Max Standard-Tier Object Age (days)"
defaults: 90
description: "A Standard-storage-class object older than this (by its
lastModified timestamp) is flagged as storage-class drift - it should
typically have transitioned to a cheaper tier (Infrequent Access, Archive,
or Cold Archive) under a lifecycle policy by now."
- id: max_files_to_scan
type: INT
displayName: "Max Files To Scan"
defaults: 1000
description: "Passed to the List task's own maxFiles property, capped at 1000
(the task's own documented hard limit). A warning is logged by the task
itself if more objects match than this cap allows."
tasks:
- id: probe_list_objects
type: io.kestra.plugin.alibaba.oss.List
description: "Lists up to max_files_to_scan objects (verified from the task's
own Javadoc: capped between 1 and 1000) under object_prefix, returning
each object's key, size, lastModified, and storageClass - a read-only
call, it cannot delete, copy, or modify any object."
accessKeyId: "{{ secret('ALIBABA_ACCESS_KEY_ID') }}"
accessKeySecret: "{{ secret('ALIBABA_ACCESS_KEY_SECRET') }}"
region: "{{ inputs.alibaba_region }}"
bucket: "{{ inputs.bucket_name }}"
prefix: "{{ inputs.object_prefix }}"
maxFiles: "{{ inputs.max_files_to_scan }}"
- id: evaluate_storage_class_drift
type: io.kestra.plugin.core.flow.If
description: "STORAGE_CLASS_DRIFT_DETECTED whenever at least one returned object
(verified output fields storageClass and lastModified from the List task's
own OssObject model) is still on the Standard class and older than
max_object_age_days. fromdateiso8601 and now are both real, documented jq
builtins used elsewhere in this repository's AWS CLI-based sentinels for
the same date-window pattern."
condition: "{{ (outputs.probe_list_objects.objects ?? []) |
jq('map(select(.storageClass == \"Standard\" and (now - (.lastModified |
fromdateiso8601)) > (' ~ inputs.max_object_age_days ~ ' * 86400))) |
length') > 0 }}"
then:
- id: alert_storage_class_drift
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Always alert on STORAGE_CLASS_DRIFT_DETECTED, listing each flagged
object's key, size in bytes, and age in whole days so the team can see
exactly what should be transitioned or whether a lifecycle rule is
missing or misconfigured."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{{ {
"text": "Alibaba OSS storage class sentinel: STORAGE_CLASS_DRIFT_DETECTED in bucket " ~ inputs.bucket_name ~ " (" ~ inputs.alibaba_region ~ "). Objects still on Standard tier past " ~ inputs.max_object_age_days ~ " days: " ~ ((outputs.probe_list_objects.objects ?? []) | jq('map(select(.storageClass == \"Standard\" and (now - (.lastModified | fromdateiso8601)) > (' ~ inputs.max_object_age_days ~ ' * 86400))) | map("\(.key) (\(.size) bytes, \(((now - (.lastModified | fromdateiso8601)) / 86400) | floor) days old)") | join("; ")')) ~ ". Execution " ~ execution.id ~ "."
} | toJson }}
else:
- id: log_storage_class_healthy
type: io.kestra.plugin.core.log.Log
message: "HEALTHY: no Standard-tier objects in oss://{{ inputs.bucket_name }}/{{
inputs.object_prefix }} exceed {{ inputs.max_object_age_days }} days
old, out of {{ (outputs.probe_list_objects.objects ?? []) | length }}
objects scanned."
- id: log_audit_complete
type: io.kestra.plugin.core.log.Log
description: "Final status line for every run, independent of which branch fired."
message: "Storage class audit complete for oss://{{ inputs.bucket_name }}/{{
inputs.object_prefix }}. objects_scanned={{
(outputs.probe_list_objects.objects ?? []) | length }}."
errors:
- id: alert_execution_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Alert when the flow itself fails outside the handled
HEALTHY/STORAGE_CLASS_DRIFT_DETECTED branches, for example a bucket that
does not exist, or invalid credentials."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{{ {
"text": "Alibaba OSS storage class sentinel FAILED in execution " ~ execution.id ~ " for bucket " ~ inputs.bucket_name ~ " (" ~ inputs.alibaba_region ~ "). List did not complete; investigate before trusting the last known HEALTHY/STORAGE_CLASS_DRIFT_DETECTED status."
} | toJson }}