Repository navigation
feat(spark): add Iceberg + MinIO integration example - #501
digvijay-y wants to merge 29 commits into
Conversation
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
There was a problem hiding this comment.
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 |
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
tariq-hasan
left a comment
There was a problem hiding this comment.
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.
| .config("spark.sql.catalog.lakehouse.s3.secret-access-key", MINIO_SECRET_KEY) | ||
| .getOrCreate() | ||
| ) | ||
|
|
There was a problem hiding this comment.
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.
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
The example needs to be integrated with the test harness to verify that it works as part of the e2e.
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>
Signed-off-by: digvijay-y <144053736+digvijay-y@users.noreply.github.com>
Signed-off-by: DIGVIJAY <144053736+digvijay-y@users.noreply.github.com>
|
/ok-to-test |
|
Hi @digvijay-y! Thanks for implementing the changes. Please also rebase the PR to fix the merge conflicts. |
| "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" |
There was a problem hiding this comment.
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.
| "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, |
There was a problem hiding this comment.
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>
c5271e1 to
06d43f5
Compare
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>
32140bb to
93835f5
Compare
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
93835f5 to
32140bb
Compare
32140bb to
82fbe2b
Compare
|
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
left a comment
There was a problem hiding this comment.
Hi @digvijay-y! Thanks for making the changes. I have added a few more comments.
There was a problem hiding this comment.
Please remove all changes from hack/e2e-setup-cluster.sh. This file should not be modified.
There was a problem hiding this comment.
Please move this script to hack/setup-iceberg-minio.sh. We no longer maintain a test directory on main.
|
|
||
| .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 |
There was a problem hiding this comment.
No need to configure user and password. Also let's reference the hack directory since we are moving the script.
| .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" |
There was a problem hiding this comment.
Let's remove this.
| SPARK_E2E_ENABLE_ICEBERG_MINIO: "1" |
| SPARK_OPERATOR_IMAGE_TAG: local | ||
| SPARK_E2E_ENABLE_ICEBERG_MINIO: "1" | ||
| timeout-minutes: 15 | ||
|
|
There was a problem hiding this comment.
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
| "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}\")" |
There was a problem hiding this comment.
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}")| "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}\")" |
There was a problem hiding this comment.
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}")| "spark.stop()\n", | ||
| "client.delete_session(session_name)\n", | ||
| "print(\"Iceberg + MinIO example complete.\")" |
There was a problem hiding this comment.
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.")| "metadata": {}, | ||
| "outputs": [], | ||
| "source": [ | ||
| "spark.sql(\"DESCRIBE EXTENDED lakehouse.demo.users\").show(truncate=False)" |
There was a problem hiding this comment.
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>
… 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>
|
@tariq-hasan it looks good now, can you check it whenever possible |
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 datadocker-compose-iceberg-minio.yml— one-command local setupREADME.mdwith setup instructionsTested with:
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: