Skip to content

feat(spark): add Iceberg + MinIO integration example - #501

Open
digvijay-y wants to merge 29 commits into
kubeflow:mainfrom
digvijay-y:examples/iceberg-minio
Open

digvijay-y wants to merge 29 commits into
kubeflow:mainfrom
digvijay-y:examples/iceberg-minio

Conversation

@digvijay-y

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:
Adds a runnable local development example demonstrating SparkClient with Apache Iceberg and MinIO as S3-compatible object storage.

Includes:

  • iceberg_minio.py — end-to-end: create namespace, table, write and read data
  • docker-compose-iceberg-minio.yml — one-command local setup
  • Updated README.md with setup instructions

Tested with:

  • iceberg-spark-runtime 1.9.1
  • AWS SDK v2 (software.amazon.awssdk:bundle:2.26.24)
  • PySpark 3.5.0
  • MinIO + tabulario/iceberg-rest

Which issue(s) this PR fixes (optional, in Fixes #<issue number>, #<issue number>, ... format, will close the issue(s) when PR gets merged):

Fixes #500

Checklist:

  • Docs included if any changes are user facing

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 24, 2026 08:34
@google-oss-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign electronic-waste for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a local-development example for using Spark with Apache Iceberg backed by MinIO (S3-compatible) and an Iceberg REST catalog.

Changes:

  • Added a runnable PySpark example that creates, writes, and reads an Iceberg table using MinIO + REST catalog.
  • Added a docker-compose stack to spin up MinIO, initialize a warehouse bucket, and run the Iceberg REST catalog.
  • Documented setup/run/config/teardown steps in the Spark examples README.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
examples/spark/iceberg_minio.py New end-to-end Iceberg + MinIO example script with Spark session configuration and basic table operations
examples/spark/docker-compose-iceberg-minio.yml New docker-compose stack for MinIO + bucket init + Iceberg REST catalog
examples/spark/README.md Documents how to run the Iceberg + MinIO example and its configuration

Comment thread examples/spark/iceberg_minio.py Outdated
Comment thread examples/spark/iceberg_minio.py Outdated
Comment thread examples/spark/iceberg_minio.py Outdated
Comment thread examples/spark/README.md Outdated
Comment thread examples/spark/docker-compose-iceberg-minio.yml Outdated
Comment thread examples/spark/docker-compose-iceberg-minio.yml Outdated
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>

@tariq-hasan tariq-hasan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @digvijay-y! Thanks for raising the PR. This is a great start. I left a few high-level comments for now. We can go deeper on the review once we align on the approach.

Comment thread examples/spark/iceberg_minio.py Outdated
Comment thread examples/spark/iceberg_minio.py Outdated
.config("spark.sql.catalog.lakehouse.s3.secret-access-key", MINIO_SECRET_KEY)
.getOrCreate()
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses pyspark directly. We need to run the test through the Kubeflow Spark SDK.

The value of this example for #470 is proving the Iceberg path works through the SDK's SparkConnect path end-to-end. A local SparkSession validates Iceberg+Spark but exercises no Kubeflow code, so it can't go in the example harness.

Comment thread examples/spark/README.md Outdated
Comment thread examples/spark/iceberg_minio.py Outdated


if __name__ == "__main__":
main()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example needs to be integrated with the test harness to verify that it works as part of the e2e.

Comment thread examples/spark/docker-compose-iceberg-minio.yml Outdated
Comment thread examples/spark/spark_connect_simple.py Outdated
digvijay-y and others added 4 commits June 24, 2026 21:45
Co-authored-by: Tariq Hasan <mmtariquehsn@gmail.com>
Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
@google-oss-prow google-oss-prow Bot added size/XL and removed size/L labels Jul 25, 2026
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
@google-oss-prow google-oss-prow Bot added size/L and removed size/XL labels Jul 25, 2026
Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
@digvijay-y
digvijay-y requested a review from tariq-hasan August 7, 2026 18:07
@tariq-hasan

Copy link
Copy Markdown
Member

/ok-to-test
/retest

@tariq-hasan

Copy link
Copy Markdown
Member

Hi @digvijay-y! Thanks for implementing the changes.

Please also rebase the PR to fix the merge conflicts.

Comment thread examples/spark/iceberg_minio.py Outdated
Comment on lines +110 to +112
"org.apache.iceberg:iceberg-spark-runtime-4.0_2.13:1.9.1,"
"software.amazon.awssdk:bundle:2.26.24,"
"software.amazon.awssdk:url-connection-client:2.26.24"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package sizes may be causing a timeout on the E2E tests. Better to replace them with lightweight minimal packages that showcase the end-to-end flow.

Comment thread examples/spark/iceberg_minio.py Outdated
"spark.sql.catalog.lakehouse.s3.path-style-access": "true",
"spark.sql.catalog.lakehouse.s3.region": "us-east-1",
"spark.sql.catalog.lakehouse.s3.access-key-id": MINIO_ACCESS_KEY,
"spark.sql.catalog.lakehouse.s3.secret-access-key": MINIO_SECRET_KEY,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access key and secret key credentials are best not exposed in spark configs.

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
…mpatibility

Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
…S SDK dependencies

Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
@digvijay-y
digvijay-y requested a review from tariq-hasan August 9, 2026 10:01
@digvijay-y
digvijay-y force-pushed the examples/iceberg-minio branch from c5271e1 to 06d43f5 Compare September 18, 2026 06:57
@github-project-automation github-project-automation Bot moved this to Ready to Merge in Kubeflow SDK Sep 21, 2026
@tariq-hasan tariq-hasan moved this from Ready to Merge to Changes Requested in Kubeflow SDK Sep 21, 2026
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
…mples/iceberg-minio

# Conflicts:
#	.github/workflows/test-spark-examples.yaml
#	examples/spark/README.md
#	hack/e2e-setup-cluster.sh
#	test/e2e/spark/test_spark_examples.py

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
@digvijay-y
digvijay-y force-pushed the examples/iceberg-minio branch 2 times, most recently from 32140bb to 93835f5 Compare October 17, 2026 21:20
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@google-oss-prow google-oss-prow Bot added size/XXL and removed size/L labels Oct 17, 2026
@digvijay-y
digvijay-y force-pushed the examples/iceberg-minio branch from 93835f5 to 32140bb Compare October 17, 2026 21:23
@google-oss-prow google-oss-prow Bot added size/L and removed size/XXL labels Oct 17, 2026
@digvijay-y
digvijay-y force-pushed the examples/iceberg-minio branch from 32140bb to 82fbe2b Compare October 17, 2026 21:25
@tariq-hasan

Copy link
Copy Markdown
Member

Thanks @digvijay-y! Let's convert the example into a notebook to match the other refactored Spark examples: https://github.com/kubeflow/sdk/tree/main/examples/spark.

… for E2E testing

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
…r bucket management

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>

@tariq-hasan tariq-hasan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @digvijay-y! Thanks for making the changes. I have added a few more comments.

Comment thread hack/e2e-setup-cluster.sh

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove all changes from hack/e2e-setup-cluster.sh. This file should not be modified.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this script to hack/setup-iceberg-minio.sh. We no longer maintain a test directory on main.

Comment thread Makefile Outdated
Comment on lines +143 to +149

.PHONY: test-e2e-setup-fixtures
test-e2e-setup-fixtures: ## Setup optional Spark E2E fixtures
@SPARK_TEST_NAMESPACE=$(SPARK_TEST_NAMESPACE) \
MINIO_ROOT_USER=$(MINIO_ROOT_USER) \
MINIO_ROOT_PASSWORD=$(MINIO_ROOT_PASSWORD) \
./test/e2e/spark/fixtures/setup-iceberg-minio.sh

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to configure user and password. Also let's reference the hack directory since we are moving the script.

Suggested change
.PHONY: test-e2e-setup-fixtures
test-e2e-setup-fixtures: ## Setup optional Spark E2E fixtures
@SPARK_TEST_NAMESPACE=$(SPARK_TEST_NAMESPACE) \
MINIO_ROOT_USER=$(MINIO_ROOT_USER) \
MINIO_ROOT_PASSWORD=$(MINIO_ROOT_PASSWORD) \
./test/e2e/spark/fixtures/setup-iceberg-minio.sh
.PHONY: test-e2e-setup-fixtures
test-e2e-setup-fixtures: ## Deploy optional Spark E2E fixtures (MinIO + Iceberg REST catalog) into an existing cluster
@echo "Setting up Spark E2E fixtures..."
@SPARK_TEST_NAMESPACE=$(SPARK_TEST_NAMESPACE) \
./hack/setup-iceberg-minio.sh

SPARK_TEST_CLUSTER: spark-test
SPARK_TEST_NAMESPACE: spark-test
SPARK_OPERATOR_IMAGE_TAG: local
SPARK_E2E_ENABLE_ICEBERG_MINIO: "1"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this.

Suggested change
SPARK_E2E_ENABLE_ICEBERG_MINIO: "1"

SPARK_OPERATOR_IMAGE_TAG: local
SPARK_E2E_ENABLE_ICEBERG_MINIO: "1"
timeout-minutes: 15

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead let's add an additional step here.

      - name: Setup Iceberg + MinIO fixtures
        run: |
          make test-e2e-setup-fixtures
        env:
          SPARK_TEST_NAMESPACE: spark-test
        timeout-minutes: 10

Comment on lines +31 to +54
"import os\n",
"import uuid\n",
"\n",
"from kubeflow.common.types import KubernetesBackendConfig\n",
"from kubeflow.spark import Name, SparkClient\n",
"\n",
"namespace = os.environ.get(\"SPARK_TEST_NAMESPACE\", \"default\")\n",
"minio_endpoint = os.environ.get(\"MINIO_ENDPOINT\", \"http://minio:9000\")\n",
"minio_access_key = os.environ.get(\"MINIO_ACCESS_KEY\", \"minioadmin\")\n",
"minio_secret_key = os.environ.get(\"MINIO_SECRET_KEY\", \"minioadmin\")\n",
"iceberg_rest_uri = os.environ.get(\"ICEBERG_REST_URI\", \"http://iceberg-rest:8181\")\n",
"warehouse = os.environ.get(\"ICEBERG_WAREHOUSE\", \"s3://warehouse/\")\n",
"\n",
"os.environ.setdefault(\"AWS_REGION\", \"us-east-1\")\n",
"os.environ.setdefault(\"AWS_ACCESS_KEY_ID\", minio_access_key)\n",
"os.environ.setdefault(\"AWS_SECRET_ACCESS_KEY\", minio_secret_key)\n",
"\n",
"backend_config = KubernetesBackendConfig(namespace=namespace)\n",
"client = SparkClient(backend_config=backend_config)\n",
"session_name = f\"kubeflow-iceberg-minio-{uuid.uuid4().hex[:8]}\"\n",
"\n",
"print(f\"Using namespace: {namespace}\")\n",
"print(f\"MinIO endpoint: {minio_endpoint}\")\n",
"print(f\"Iceberg REST catalog: {iceberg_rest_uri}\")"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set environment variables for the AWS settings. We still need to configure them as spark config though.

import os
import uuid

from kubeflow.common.types import KubernetesBackendConfig
from kubeflow.spark import Name, SparkClient

namespace = os.environ.get("SPARK_TEST_NAMESPACE", "default")

# Service names resolve from the Spark Connect server pod in the same namespace.
minio_endpoint = os.environ.get("MINIO_ENDPOINT", "http://minio:9000")
iceberg_rest_uri = os.environ.get("ICEBERG_REST_URI", "http://iceberg-rest:8181")
warehouse = os.environ.get("ICEBERG_WAREHOUSE", "s3://warehouse/")

# Local development defaults only. Never commit real credentials to a notebook.
minio_access_key = os.environ.get("MINIO_ACCESS_KEY", "minioadmin")
minio_secret_key = os.environ.get("MINIO_SECRET_KEY", "minioadmin")

client = SparkClient(backend_config=KubernetesBackendConfig(namespace=namespace))
session_name = f"iceberg-minio-{uuid.uuid4().hex[:8]}"

print(f"Namespace: {namespace}")
print(f"MinIO endpoint: {minio_endpoint}")
print(f"Iceberg REST catalog: {iceberg_rest_uri}")

Comment on lines +72 to +96
"spark = client.connect(\n",
" spark_conf={\n",
" \"spark.jars\": (\n",
" \"https://repo1.maven.org/maven2/org/apache/iceberg/iceberg-spark-runtime-4.0_2.13/1.11.0/iceberg-spark-runtime-4.0_2.13-1.11.0.jar,\"\n",
" \"https://repo1.maven.org/maven2/org/apache/iceberg/iceberg-aws-bundle/1.11.0/iceberg-aws-bundle-1.11.0.jar\"\n",
" ),\n",
" \"spark.sql.extensions\": (\n",
" \"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions\"\n",
" ),\n",
" \"spark.sql.catalog.lakehouse\": \"org.apache.iceberg.spark.SparkCatalog\",\n",
" \"spark.sql.catalog.lakehouse.type\": \"rest\",\n",
" \"spark.sql.catalog.lakehouse.uri\": iceberg_rest_uri,\n",
" \"spark.sql.catalog.lakehouse.warehouse\": warehouse,\n",
" \"spark.sql.catalog.lakehouse.io-impl\": \"org.apache.iceberg.aws.s3.S3FileIO\",\n",
" \"spark.sql.catalog.lakehouse.s3.endpoint\": minio_endpoint,\n",
" \"spark.sql.catalog.lakehouse.s3.path-style-access\": \"true\",\n",
" \"spark.sql.catalog.lakehouse.client.region\": \"us-east-1\",\n",
" \"spark.sql.catalog.lakehouse.s3.access-key-id\": minio_access_key,\n",
" \"spark.sql.catalog.lakehouse.s3.secret-access-key\": minio_secret_key,\n",
" },\n",
" options=[Name(session_name)],\n",
" timeout=180 if os.environ.get(\"SPARK_E2E_RUN_IN_CLUSTER\") == \"1\" else 300,\n",
" connect_timeout=60 if os.environ.get(\"SPARK_E2E_RUN_IN_CLUSTER\") == \"1\" else 120,\n",
")\n",
"print(f\"Connected Spark session: {session_name}\")"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop the timeouts and parameterize the fields that go in spark.jars.

iceberg_version = "1.11.0"
maven = "https://repo1.maven.org/maven2/org/apache/iceberg"

spark = client.connect(
    spark_conf={
        "spark.jars": ",".join(
            [
                f"{maven}/iceberg-spark-runtime-4.0_2.13/{iceberg_version}/"
                f"iceberg-spark-runtime-4.0_2.13-{iceberg_version}.jar",
                f"{maven}/iceberg-aws-bundle/{iceberg_version}/"
                f"iceberg-aws-bundle-{iceberg_version}.jar",
            ]
        ),
        "spark.sql.extensions": (
            "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions"
        ),
        "spark.sql.catalog.lakehouse": "org.apache.iceberg.spark.SparkCatalog",
        "spark.sql.catalog.lakehouse.type": "rest",
        "spark.sql.catalog.lakehouse.uri": iceberg_rest_uri,
        "spark.sql.catalog.lakehouse.warehouse": warehouse,
        "spark.sql.catalog.lakehouse.io-impl": "org.apache.iceberg.aws.s3.S3FileIO",
        "spark.sql.catalog.lakehouse.s3.endpoint": minio_endpoint,
        "spark.sql.catalog.lakehouse.s3.path-style-access": "true",
        "spark.sql.catalog.lakehouse.client.region": "us-east-1",
        "spark.sql.catalog.lakehouse.s3.access-key-id": minio_access_key,
        "spark.sql.catalog.lakehouse.s3.secret-access-key": minio_secret_key,
    },
    options=[Name(session_name)],
)
print(f"Connected Spark session: {session_name}")

Comment on lines +163 to +165
"spark.stop()\n",
"client.delete_session(session_name)\n",
"print(\"Iceberg + MinIO example complete.\")"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's cleanup before session deletion.

spark.sql("DROP TABLE IF EXISTS lakehouse.demo.users PURGE")
spark.stop()
client.delete_session(session_name)
print("Iceberg + MinIO example complete.")

Comment thread examples/spark/iceberg_minio.ipynb Outdated
"metadata": {},
"outputs": [],
"source": [
"spark.sql(\"DESCRIBE EXTENDED lakehouse.demo.users\").show(truncate=False)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be more thorough in the table metadata inspection and write some assertions since this runs in E2E tests.

snapshots = spark.sql("SELECT snapshot_id, operation FROM lakehouse.demo.users.snapshots").collect()
print(f"Snapshots: {[(s.snapshot_id, s.operation) for s in snapshots]}")
assert len(snapshots) == 1 and snapshots[0].operation == "append", snapshots

data_files = [
    row.file_path for row in spark.sql("SELECT file_path FROM lakehouse.demo.users.files").collect()
]
print("Data files:")
for path in data_files:
    print(f"  {path}")
assert data_files, "Expected at least one data file"
assert all(path.startswith(warehouse) for path in data_files), data_files

…s in Spark examples

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
@google-oss-prow google-oss-prow Bot added size/XL and removed size/L labels Oct 19, 2026
… script

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
…tegration

Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
@digvijay-y

Copy link
Copy Markdown
Contributor Author

@tariq-hasan it looks good now, can you check it whenever possible

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Changes Requested

Development

Successfully merging this pull request may close these issues.

examples/spark: Add Iceberg + MinIO integration example

3 participants