New to Kestra?
Use blueprints to kickstart your first workflows.
Archive expired DynamoDB records to Amazon S3 cold storage and safely purge primary table entries with dry-run controls, concurrency queueing, and Slack alerts.
Amazon DynamoDB is built for low-latency operational workloads, but storing historical, soft-deleted, or expired records in primary tables indefinitely leads to unbounded storage expenses. While DynamoDB table storage costs $0.25 per GB-month, Amazon S3 Standard costs $0.023 per GB-month (over 10x cheaper) and archival tiers like S3 Glacier Deep Archive cost less than $0.001 per GB-month (over 250x cheaper). Furthermore, DynamoDB's native Time-to-Live (TTL) feature guarantees deletion only within 48 hours of expiration, which can cause downstream queries to return stale data or violate strict compliance windows.
This blueprint implements an automated, production-grade retention tiering and garbage collection pipeline for DynamoDB. It scans records matching an ISO-8601 retention cutoff, writes them into internal storage manifests, safely archives them into an Amazon S3 cold bucket under date-partitioned keys, and removes the purged records from the primary DynamoDB table only after S3 write durability is confirmed. The workflow features safe-by-default dry-run controls, serialized execution concurrency to prevent race conditions, and immediate Slack alerting on task failures.
This blueprint assumes items in your DynamoDB table contain an id partition key and an ISO-8601 UTC timestamp string attribute representing record expiration or soft deletion:
{
"id": "evt_01HZX87Q9E1K4B9A2M4C5D6E7F",
"event_type": "ORDER_COMPLETED",
"customer_id": "cust_90214",
"expires_at": "2026-07-14T03:00:00Z",
"payload": {
"order_id": "ord_88201",
"total_amount": 128.50
}
}
concurrency: { behavior: QUEUE, limit: 1 } prevents concurrent execution overlap between scheduled cron sweeps and manual on-demand webhook invocations.retentionDays input enforces min: 0 to prevent negative offsets from advancing the cutoff into future timestamps, while filterAttribute validates against ^[a-zA-Z_][a-zA-Z0-9_]*$ to reject malformed expressions or invalid attribute names.scan_expired_records (io.kestra.plugin.aws.dynamodb.Scan) evaluates the table with a filter expression comparing the configured expiration attribute against :cutoff. The cutoff timestamp is computed dynamically in Kestra using {{ now() | dateAdd(-inputs.retentionDays, 'DAYS') | date("yyyy-MM-dd'T'HH:mm:ss'Z'", timeZone="UTC") }}.evaluate_archival_batch (io.kestra.plugin.core.flow.If) evaluates outputs.scan_expired_records.size > 0. If no records exceed the retention threshold, the workflow logs a clean status and completes without executing redundant S3 uploads or downstream loops.write_archive_manifest (io.kestra.plugin.core.storage.Write) converts the matched records into an internal JSON storage file.upload_to_s3_archive (io.kestra.plugin.aws.s3.Upload) pushes the manifest into the target Amazon S3 bucket under the key {{ inputs.s3Prefix }}/{{ inputs.tableName }}/{{ execution.startDate | date('yyyy/MM/dd') }}/{{ execution.id }}.json.check_dry_run_and_purge (io.kestra.plugin.core.flow.If) verifies dryRun: false. It executes purge_items_from_dynamodb (io.kestra.plugin.core.flow.Loop) over the archived records and sequentially invokes delete_item (io.kestra.plugin.aws.dynamodb.DeleteItem) by primary key (id). Because deletions occur strictly downstream of upload_to_s3_archive, records are never deleted unless S3 persistence has succeeded.log_purge_summary and log_dry_run_summary (io.kestra.plugin.core.log.Log) output structured confirmation logs with item counts and destination S3 URIs.notify_archival_incident in the flow-level errors block dispatches a Slack alert with execution and task failure context if any step fails.dryRun: true, disabled: true on the schedule trigger, and min: 0 retention bounds.QUEUE concurrency to prevent race conditions.Building custom retention tiering scripts with AWS Lambda or EC2 cron jobs introduces operational burdens: Lambda 15-minute execution timeouts, fragile IAM permission configurations, lack of native queueing/serialization, unhandled batch retries, and absent execution lineage. Kestra delivers:
Schedule) and authenticated Webhook trigger in a single YAML specification.QUEUE concurrency to eliminate overlapping workers without Redis locks or distributed mutexes.dynamodb:Scan, dynamodb:DeleteItem) and Amazon S3 (s3:PutObject).id primary key and an ISO-8601 timestamp attribute.Configure the following secrets in your Kestra namespace:
AWS_ACCESS_KEY_ID: IAM access key with DynamoDB and S3 permissions.AWS_SECRET_KEY_ID: Corresponding IAM secret key.AWS_DEFAULT_REGION: Target AWS region (e.g. us-east-1, eu-west-1).DYNAMODB_PURGE_WEBHOOK_KEY: Secret authentication key for the manual webhook trigger.SLACK_WEBHOOK_URL: Slack Incoming Webhook URL for incident notifications.AWS_ACCESS_KEY_ID, AWS_SECRET_KEY_ID, AWS_DEFAULT_REGION, DYNAMODB_PURGE_WEBHOOK_KEY, SLACK_WEBHOOK_URL) in your Kestra namespace.tableName, s3Bucket, and retentionDays inputs to match your AWS environment.dryRun: true in the Kestra UI to verify that expired records are detected and uploaded to S3 without removing them from DynamoDB.dryRun: false and toggle disabled: false on daily_retention_schedule to automate ongoing archival sweeps.io.kestra.plugin.jdbc.duckdb.Query task between the scan and upload to compress JSON manifests into columnar Parquet format before writing to S3.io.kestra.plugin.core.flow.Parallel task or subflow to sweep multiple application tables concurrently.