RealtimeTrigger icon
Loop icon
If icon
Log icon
Set icon
Commands icon
Docker icon
Delete icon
SlackIncomingWebhook icon

S3 Parquet Spark Iceberg Compaction Trigger

Automate Apache Iceberg table compaction using S3 SQS events, Kestra KV tracking, and containerized Spark rewrite_data_files with Slack alerts.

Categories
Data

Automate Apache Iceberg table file compaction using an event-driven architecture powered by Amazon S3, SQS, Kestra KV store, and Apache Spark. In analytical lakehouses with continuous streaming or micro-batch writes, frequent ingestion generates hundreds or thousands of small Parquet files. This small-file accumulation severely degrades query planning and read performance across analytical query engines like Trino, DuckDB, Presto, and Spark.

Rather than running blind, resource-intensive compaction jobs on a fixed schedule, this blueprint acts as an event listener that intercepts Amazon S3 ObjectCreated events from an SQS queue, filters landed Parquet files against configurable size thresholds, accumulates pending small-file metrics in Kestra's persistent KV store, and automatically triggers an isolated Apache Spark container running Iceberg's native rewrite_data_files procedure the moment predefined thresholds are exceeded.

How it works

  1. sqs_s3_event_listener uses io.kestra.plugin.aws.sqs.RealtimeTrigger to listen to an Amazon SQS queue receiving S3 ObjectCreated event notifications in real time.
  2. process_s3_records iterates over each event record in the notification payload using io.kestra.plugin.core.flow.Loop.
  3. check_eligible_object (io.kestra.plugin.core.flow.If) validates that the event is an ObjectCreated event, targets the designated s3_bucket, matches the configured s3_prefix, and ends with .parquet. Non-matching events, non-Parquet files, and deletes are ignored and logged.
  4. check_small_file_threshold identifies files whose size is below small_file_mb (default 64 MB), classifying them as candidate small files requiring compaction.
  5. update_pending_state (io.kestra.plugin.core.kv.Set) increments the table's pending small file count and total byte volume in Kestra's namespace KV store under key pending_compaction_<table_identifier>. To eliminate read-modify-write race conditions when multiple S3 events arrive simultaneously, the flow enforces concurrency: { limit: 1, behavior: QUEUE }, guaranteeing strictly serialized execution slots.
  6. evaluate_compaction_threshold (io.kestra.plugin.core.flow.If) evaluates accumulated metrics against configured thresholds: min_files (e.g., 10 files) or min_total_mb (e.g., 256 MB).
  7. run_spark_compaction (io.kestra.plugin.scripts.shell.Commands with io.kestra.plugin.scripts.runner.docker.Docker) launches an Apache Spark container (apache/spark:3.5.1) with Iceberg runtime packages and executes CALL <catalog>.system.rewrite_data_files(table => ..., strategy => 'binpack', options => map(...)) to bin-pack small files into target-sized data files (e.g., 512 MB). AWS credentials reach the container securely via environment variables from secrets.
  8. reset_pending_state (io.kestra.plugin.core.kv.Delete) deletes the pending counter entry from the KV store upon successful Spark job completion.
  9. notify_slack_success (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) dispatches a structured Slack summary with the table identifier, execution ID, catalog details, and compaction confirmation.
  10. notify_slack_failure in the flow-level errors: handler catches any container or compaction failures, posts an incident alert to Slack, and intentionally preserves the KV state so pending small files are not forgotten and will be compacted on the next execution cycle.

What you get

  • Autonomous event-driven lakehouse maintenance triggered by real S3 landing telemetry.
  • Safe small-file bin-packing using Apache Iceberg's native rewrite_data_files Spark procedure.
  • Atomic, race-free state tracking via Kestra's namespace KV store and execution concurrency queueing.
  • Zero persistent Spark cluster infrastructure overhead: Spark runs inside an ephemeral Docker container only when compaction thresholds are met.
  • Resilient failure recovery: failed compaction jobs preserve accumulated KV counts, alert on-call engineers via Slack, and fail cleanly without data loss.

Who it's for

  • Lakehouse Engineers and Platform Engineers managing Apache Iceberg tables on Amazon S3.
  • Data Engineers looking to eliminate small-file query degradation from streaming ingestion.
  • Analytics Platform teams automating Day-2 table maintenance with declarative infrastructure.

Why orchestrate this with Kestra

Running compaction on fixed cron schedules is inefficient: jobs fire during quiet periods when no files arrived, or fail to keep up during heavy ingestion bursts. Pure script-based consumers lack durable execution state, retry logic, concurrency isolation, and container management.

Orchestrating this workflow with Kestra combines event-driven triggers, persistent KV state storage, strict execution serialization (limit: 1, behavior: QUEUE), containerized Spark execution via Docker task runners, and failure routing in a single declarative YAML definition.

Prerequisites

  • An Amazon SQS queue configured to receive s3:ObjectCreated:* notifications from your target S3 bucket.
  • An Apache Iceberg catalog (REST Catalog, AWS Glue, or Hadoop Catalog) reachable from the Kestra worker environment.
  • A Kestra worker running with Docker task runner enabled to execute the Apache Spark container.
  • A Slack Incoming Webhook URL for compaction reports and incident alerts.

Secrets

  • AWS_ACCESS_KEY_ID: AWS access key with permissions to read from the SQS queue and read/write to the target S3 bucket and warehouse prefix.
  • AWS_SECRET_ACCESS_KEY: AWS secret access key corresponding to the access key ID.
  • SLACK_WEBHOOK_URL: Slack Incoming Webhook URL used to send compaction summaries and failure notifications.

Quick start

  1. Configure the three required secrets in Kestra: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and SLACK_WEBHOOK_URL.
  2. Configure your S3 bucket event notifications to publish All object create events for prefix warehouse/analytics/customer_orders/ to your Amazon SQS queue.
  3. Update the flow inputs: set queue_url, s3_bucket, s3_prefix, iceberg_catalog_uri, and iceberg_table to point to your lakehouse environment.
  4. Save the flow and ensure the sqs_s3_event_listener trigger is active. Land Parquet files in S3 and observe automated small file accumulation and compaction in the Kestra execution UI.

How to extend

  • Partition filtering: Add a where clause option to the rewrite_data_files Spark procedure (e.g., where => 'order_date >= current_date() - interval 7 days') to constrain compaction to recently landed partitions.
  • Snapshot expiration: Chain a follow-up task executing CALL <catalog>.system.expire_snapshots(table => ...) to remove obsolete snapshots and prune unreferenced manifests after compaction.
  • Multi-table routing: Expand the record filtering logic to handle multiple table prefixes by mapping prefix paths to distinct table identifiers and isolated KV storage keys.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.