If you've ever looked at your AWS bill and wondered whether "Iceberg" was some expensive managed service you forgot to turn off - good news, it isn't. It's free, open-source, and probably already sitting quietly in your S3 bucket. In this post, I'll walk through what Apache Iceberg actually is, how it fits together with AWS Glue and EMR Serverless, and share a working proof-of-concept you can run yourself for less than the cost of a coffee.
The misconception worth clearing up first
A common assumption is that Iceberg is an AWS product - something you'd provision like RDS or DynamoDB. It isn't. Apache Iceberg is an open table format specification, not a service. There's no infrastructure to spin up and no separate line item on your bill.
Under the hood, an Iceberg table is just:
- Your data files (typically Parquet), sitting in S3
- A set of JSON/Avro metadata files next to them, tracking schema, partitioning, and snapshot history
Any engine that speaks the Iceberg spec — Spark, Athena, Trino, Flink — can read and write those files consistently. AWS Glue's role isn't to "provide" Iceberg; it's just the metastore, a pointer that tells query engines where a given table's metadata lives. Think of Glue as the address book, and Iceberg as the actual filing system.
Why this combination matters
Pairing Iceberg with Glue and EMR Serverless gives you a lakehouse architecture with real database-like guarantees, without paying for a database:
- ACID transactions on plain S3 files — no more partial writes corrupting downstream consumers
- Schema evolution without rewriting historical data
- Time travel — query a table as it existed at any previous snapshot
- Upserts via
MERGE INTO— the exact mechanism that powers Change Data Capture (CDC) pipelines - Compute that scales to zero — EMR Serverless bills only for the vCPU/memory-seconds your jobs actually consume, with no cluster to keep warm between runs
Proving it out: a hands-on POC
Rather than take this on faith, I built a small end-to-end proof of concept — the kind of thing worth running once yourself before trusting it in production. Here's the shape of it:
- Land raw data in S3 — a simple CSV of orders
- Register a Glue database — the catalog namespace for the "silver" layer
- Create an EMR Serverless application — Spark, no cluster to manage
- Run a PySpark job that reads the CSV and writes it out as a genuine Iceberg table via the Glue catalog
- Simulate an incoming CDC update and apply it with
MERGE INTO - Verify the table format directly in the Glue console
Setting up the environment — an EMR Studio gives you a console workspace to monitor and manage EMR Serverless applications and job runs.
The raw bucket, holding just two objects: the source CSV and the PySpark job script that EMR Serverless will execute directly from S3.
The core job logic
from pyspark.sql import SparkSession
from pyspark.sql.functions import to_date
spark = SparkSession.builder.appName("iceberg-poc-job").getOrCreate()
# Read raw CSV
df = spark.read.option("header", "true").option("inferSchema", "true").csv(RAW_PATH)
# Write it as a real Iceberg table
df.writeTo("glue_catalog.iceberg_poc_db.orders").using("iceberg").createOrReplace()
# Simulate a CDC update arriving for order_id=1
updates = spark.createDataFrame(
[(1, "Alice", 999.99, "2026-07-10")],
["order_id", "customer", "amount", "order_date"]
).withColumn("order_date", to_date("order_date", "yyyy-MM-dd"))
updates.createOrReplaceTempView("updates")
spark.sql("""
MERGE INTO glue_catalog.iceberg_poc_db.orders t
USING updates u
ON t.order_id = u.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
The spark-submit configuration that ties Iceberg to Glue is the part that actually matters and is easy to get wrong:
--conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
--conf spark.sql.catalog.glue_catalog=org.apache.iceberg.spark.SparkCatalog
--conf spark.sql.catalog.glue_catalog.catalog-impl=org.apache.iceberg.aws.glue.GlueCatalog
--conf spark.sql.catalog.glue_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO
--conf spark.sql.catalog.glue_catalog.warehouse=s3://your-bucket/warehouse/
Three things are doing distinct work here, and all three are required — dropping any one of them breaks the setup:
spark.sql.extensionsregisters Iceberg's SQL syntax (likeMERGE INTOand time-travel queries) with Spark- The
catalog-implline tells Spark that this particular catalog should behave as an Iceberg catalog backed by Glue, rather than a plain Hive catalog warehousesets where the actual table data physically lands in S3
On EMR (6.9+ / 7.x), the Iceberg runtime library itself ships pre-installed, so in practice this SQL config is the main piece you're responsible for wiring up.
Submitting the job is a single AWS CLI call — no cluster to launch first, just point at the application and the script:
aws emr-serverless start-job-run from CloudShell — the command returns immediately with a jobRunId you can poll for status.
What actually broke (and why it's worth sharing)
No POC survives first contact with reality untouched, and this one hit two very typical snags worth calling out — both because they're instructive, not just as debugging trivia.
The EMR Studio job run history doesn't lie — a string of failures before things clicked. Each one pointed at a specific, fixable cause rather than anything wrong with the underlying architecture.
Polling a job's status is just as simple as submitting it:
aws emr-serverless get-job-run returns the full job state — here caught mid-flight in SCHEDULED, still acquiring resources.
1. Bucket name placeholders left unreplaced. A NoSuchBucketException traced back to a stale copy of the job script sitting in S3 — the local file had been edited, but the uploaded copy hadn't. Worth remembering: EMR Serverless runs whatever's actually in S3, not whatever's on your laptop. Always re-verify the deployed version before re-running:
aws s3 cp s3://your-bucket/scripts/iceberg_poc_job.py - | grep RAW_PATH
2. A schema type mismatch during MERGE INTO. Spark's CSV reader, with inferSchema=true, correctly inferred order_date as a DATE column from ISO-formatted strings like 2026-07-01. But a manually constructed updates DataFrame built from a plain Python string kept order_date as STRING — and Spark refuses to silently cast types during a merge for safety. The fix was an explicit cast:
.withColumn("order_date", to_date("order_date", "yyyy-MM-dd"))
This is a good reminder that Iceberg's strictness here is a feature, not a bug — it's the same safety mechanism that prevents a bad CDC batch from silently corrupting a production table's schema.
The payoff: seeing it confirmed
After fixing both issues, the job ran clean:
Success, at last — sitting right above the string of earlier failures, a nice visual record of the debugging arc.
And the Glue console showed exactly what we were after:
Table format: Apache Iceberg
No ambiguity, no guesswork — Glue reports the table format directly. From there, the same table is queryable from Athena with zero extra setup, which is really the whole point of decoupling the table format from any single compute engine.
an Iceberg table is just a data/ folder (the actual Parquet files) sitting next to a metadata/ folder (the JSON/Avro snapshot and schema tracking files). No database server, no proprietary format — just organized objects in S3.
From there, the same table is queryable from Athena with zero extra setup, which is really the whole point of decoupling the table format from any single compute engine.
What this means for cost, not just architecture
Beyond the "does it work" question, this stack has real cost advantages worth calling out for anyone evaluating it:
- No idle compute. EMR Serverless bills per vCPU-second and GB-second actually consumed — nothing between job runs.
- No database server to provision. The "table" experience comes from the Iceberg format plus Glue's catalog layer — you're paying S3 storage rates (~$0.023/GB-month), not database instance pricing.
- Architecture flexibility. The same Spark jobs can run on x86_64 or arm64 (Graviton) EMR Serverless applications with no code changes — Graviton typically shaves 20–30% off compute cost for JVM-based workloads like this one.
Try it yourself
The best way to internalize how Iceberg, Glue, and EMR Serverless fit together is to build this exact pipeline once, end to end, in a sandbox account. Total cost: well under a dollar if you clean up afterward. The debugging you'll hit along the way — a stale S3 upload, a schema mismatch — is far more instructive than reading about it secondhand.
If you build this out yourself, the natural next experiment is running the same job on an arm64 EMR Serverless application and comparing the billed resource utilization directly — a five-minute change that turns an architecture diagram into a real cost data point.








0 Comments