A small end-to-end demo showing operational MongoDB Atlas data continuously mirrored into an Apache Iceberg v2 table in Amazon S3 with Atlas Stream Processing.
MongoDB operational data → Atlas Stream Processing → Iceberg on S3 → AWS Glue → Athena
The demo deliberately focuses on four operations:
INSERT → UPDATE → DELETE → SCHEMA CHANGE
If those four changes are visible end-to-end, the architecture speaks for itself.
The source Atlas cluster can be on AWS, Azure, or GCP. The current $iceberg sink writes to Amazon S3; when the source/workspace is cross-cloud, account for network latency and egress.
mongodb-iceberg-demo/
├── README.md
├── config.py
├── requirements.txt
├── Makefile
├── scripts/
│ ├── common.py
│ ├── seed_orders.py
│ ├── insert_order.py
│ ├── update_order.py
│ ├── delete_order.py
│ ├── add_schema_field.py
│ ├── reset_demo.py
│ └── live_demo.py
├── setup/
│ ├── aws/
│ │ ├── README.md
│ │ └── bootstrap.sh
│ └── atlas/
│ └── README.md
├── stream-processing/
│ ├── create_processor.js
│ ├── status.js
│ ├── stop_processor.js
│ └── drop_processor.js
├── sql/
│ ├── 01_orders_by_region.sql
│ ├── 02_find_live_order.sql
│ ├── 03_validate_update.sql
│ ├── 04_validate_delete.sql
│ └── 05_validate_schema.sql
└── docs/
├── architecture.png
└── DEMO_FLOW.md
Python 3.10+ is recommended.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtEdit config.py and set the Atlas connection string.
MONGODB_URI = "mongodb+srv://<username>:<password>@<cluster>/"
DATABASE_NAME = "iceberg_demo"
COLLECTION_NAME = "orders"For a public repository, do not commit real credentials.
The helper script creates:
- an S3 bucket
- public-access blocking on the bucket
- an AWS Glue database named
mongodb_iceberg_demo
cd setup/aws
./bootstrap.shOptional overrides:
AWS_REGION=us-east-1 \
S3_BUCKET=my-mongodb-iceberg-demo \
GLUE_DATABASE=mongodb_iceberg_demo \
./bootstrap.shThe script prints the exact bucket, region, and Glue database values to copy into stream-processing/create_processor.js.
See setup/aws/README.md for the Atlas Unified AWS Access step.
Create or reuse:
- An Atlas cluster.
- An Atlas Stream Processing workspace.
- An Atlas source connection named
atlas-orderspointing at the cluster. - An AWS S3 connection named
s3-icebergusing Unified AWS Access.
For $iceberg, use an SP10, SP30, or SP50 processor tier.
Detailed setup notes are in setup/atlas/README.md.
python scripts/seed_orders.pyThis creates/replaces a small deterministic set of demo orders using IDs prefixed with DEMO-.
Edit the constants at the top of:
stream-processing/create_processor.js
At minimum, verify:
const SOURCE_CONNECTION = "atlas-orders";
const S3_CONNECTION = "s3-iceberg";
const S3_BUCKET = "mongodb-iceberg-demo-REPLACE_ME";
const AWS_REGION = "us-east-1";
const ICEBERG_DATABASE = "mongodb_iceberg_demo";The pipeline uses:
$sourcewithinitialSync- CDC change stream events
$replaceRootto emitfullDocumentfor inserts/updates/replaces anddocumentKeyfor deletes$icebergincdcmode- AWS Glue as the Iceberg catalog
The important pipeline is:
const isDeleteExpr = {
$eq: [{ $meta: "stream.source.operationType" }, "delete"]
};
const pipeline = [
{
$source: {
connectionName: SOURCE_CONNECTION,
db: SOURCE_DATABASE,
coll: SOURCE_COLLECTION,
initialSync: { enable: true },
config: { fullDocument: "required" }
}
},
{
$match: {
operationType: { $in: ["insert", "update", "delete", "replace"] }
}
},
{
$replaceRoot: {
newRoot: {
$cond: {
if: isDeleteExpr,
then: "$documentKey",
else: "$fullDocument"
}
}
}
},
{
$iceberg: {
connectionName: S3_CONNECTION,
bucket: S3_BUCKET,
databaseName: ICEBERG_DATABASE,
tableName: ICEBERG_TABLE,
path: ICEBERG_PATH,
region: AWS_REGION,
mode: "cdc",
idFieldName: "_id",
catalog: { type: "glue" }
}
}
];Connect mongosh to the Stream Processing workspace, then run:
load("stream-processing/create_processor.js")The script creates ordersToIceberg and starts it on SP10.
Once the first data has landed and Glue contains the table, run:
SELECT
region,
COUNT(*) AS orders,
SUM(amount) AS revenue
FROM mongodb_iceberg_demo.orders
GROUP BY region
ORDER BY revenue DESC;The same query is in sql/01_orders_by_region.sql.
python scripts/live_demo.pyThe script pauses between each operation so you can refresh Athena after every change.
Insert:
python scripts/insert_order.pyUpdate:
python scripts/update_order.pyDelete:
python scripts/delete_order.pySchema evolution:
python scripts/add_schema_field.pyReset the live demo documents:
python scripts/reset_demo.pyAfter insert:
SELECT *
FROM mongodb_iceberg_demo.orders
WHERE _id = 'ORD-LIVE-001';After update:
SELECT _id, status, amount
FROM mongodb_iceberg_demo.orders
WHERE _id = 'ORD-LIVE-001';After delete, the same lookup should return zero rows.
After schema evolution:
SELECT _id, product, fraudScore
FROM mongodb_iceberg_demo.orders
WHERE _id = 'ORD-LIVE-002';Depending on how Athena surfaces identifier casing, you may need to use the normalized column name it shows for fraudScore.
From mongosh connected to the Stream Processing workspace:
load("stream-processing/status.js")
load("stream-processing/stop_processor.js")
load("stream-processing/drop_processor.js")The entire demo should take only a few minutes:
- Show an operational order in Atlas.
- Show the same data aggregated in Athena.
- Insert
ORD-LIVE-001and query it in Athena. - Change
PROCESSING→SHIPPEDand225→199. - Delete it and show zero rows.
- Insert
ORD-LIVE-002with a brand-newfraudScorefield. - Show the evolved Iceberg schema / new column.
The point is not raw throughput. The point is that an operational MongoDB collection can be maintained as an open Iceberg table without building a separate Kafka + connector + Spark ingestion chain.
As of August 2026:
$icebergwrites Apache Iceberg tables to AWS S3.$icebergmust be the final pipeline stage.cdcandinsertmodes are supported.- CDC mode uses stream operation metadata to apply inserts, updates, replacements, and deletes.
- Iceberg schema evolves as new fields are observed.
- Output is at-least-once.
$icebergis supported on SP10, SP30, and SP50.- BSON objects and arrays are serialized as JSON strings; primitive fields map naturally to Iceberg primitive columns.
Official documentation:
