From e96806ddd2448daf32971c8f0a05737b3c1b2bda Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:29:21 -0400 Subject: [PATCH 01/45] Start with rpk examples --- .../pages/topic-iceberg-integration.adoc | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 modules/manage/pages/topic-iceberg-integration.adoc diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc new file mode 100644 index 0000000000..79814559c9 --- /dev/null +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -0,0 +1,50 @@ += Topics as Iceberg Tables +:description: Learn how to integrate Redpanda topics with Iceberg. +:page-context-links: [{"name": "Linux", "to": "manage:topic-recovery.adoc" } ] +:page-categories: Management, High Availability + + +== Prerequisites + +. Install `rpk`. + +== Enable Iceberg integration + +. Create a new topic. ++ +[,bash,] +---- +rpk topic create test_topic --partitions 1 --replicas 1 +---- ++ +[,bash,.no-copy] +---- +TOPIC STATUS +test_topic OK +---- + +// Enable data lake support +. Enable the integration for the cluster. ++ +[,bash] +---- +rpk cluster config set enable_datalake true +---- ++ +[,bash,.no-copy] +---- +Successfully updated configuration. New configuration version is 2. +---- + +. Enable the integration for the topic. ++ +[,bash] +---- +rpk topic alter-config test_topic --set iceberg.topic=true +---- ++ +[,bash,.no-copy] +---- +TOPIC STATUS +test_topic OK +---- \ No newline at end of file From cffe8e1cf58969c8a8147281abc3570086198a66 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:31:06 -0400 Subject: [PATCH 02/45] First outline --- .../pages/topic-iceberg-integration.adoc | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 79814559c9..6fea40d672 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -4,47 +4,89 @@ :page-categories: Management, High Availability +The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without having to set up and configure ETL pipelines. + +// Apache Iceberg is an open source format for defining structured tables in the Data Lake and enables multiple applications to work together on the same data in a consistent fashion and more effectively track dataset states with transactional consistency as changes are made. + +// Reduced cost of data storage, no ETL required, immediate access to late arriving data. + + == Prerequisites +include::shared:partial$enterprise-license.adoc[] + +. To check if you already have a license key applied to your cluster: ++ +[,bash] +---- +rpk cluster license info +---- + . Install `rpk`. +== Limitations + +* You can only enable the Iceberg integration when creating a new Redpanda topic. +* You can only use one schema per topic. Schema versioning as well as upcasting (where a message is cast into its more generic data type) are not supported. + +== Architecture + + == Enable Iceberg integration +// Enable data lake support +. Enable the Iceberg integration for the cluster by setting the cluster configuration `enable_datalake` to `true`. ++ +[,bash] +---- +rpk cluster config set enable_datalake true +---- ++ +[,bash,.no-copy] +---- +Successfully updated configuration. New configuration version is 2. +---- + . Create a new topic. + [,bash,] ---- -rpk topic create test_topic --partitions 1 --replicas 1 +rpk topic create --partitions 1 --replicas 1 ---- + [,bash,.no-copy] ---- TOPIC STATUS -test_topic OK +new-topic OK ---- -// Enable data lake support -. Enable the integration for the cluster. +. Enable the integration for the topic. + [,bash] ---- -rpk cluster config set enable_datalake true +rpk topic alter-config --set iceberg_topic=true ---- + [,bash,.no-copy] ---- -Successfully updated configuration. New configuration version is 2. +TOPIC STATUS +new-topic OK ---- -. Enable the integration for the topic. +. Optional: Register a schema for the topic. Redpanda uses the schema to derive the Iceberg table format and write data files to object storage in this format. ++ +If you don't configure a schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original message timestamp. If you don't have a need for a strict schema and can use the data in a more semi structured format, consider the schemaless approach. + [,bash] ---- -rpk topic alter-config test_topic --set iceberg.topic=true +rpk registry schema create --schema --type ---- + [,bash,.no-copy] ---- -TOPIC STATUS -test_topic OK ----- \ No newline at end of file +SUBJECT VERSION ID TYPE +new-topic 1 1 PROTOBUF +---- ++ + + From 0beeaef2dd68ba8f61b466eb1effd9458dd2cfca Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:51:29 -0400 Subject: [PATCH 03/45] Add architecture --- .../pages/topic-iceberg-integration.adoc | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 6fea40d672..b6016f2d98 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -1,15 +1,10 @@ = Topics as Iceberg Tables :description: Learn how to integrate Redpanda topics with Iceberg. -:page-context-links: [{"name": "Linux", "to": "manage:topic-recovery.adoc" } ] +:page-context-links: [{"name": "Linux", "to": "manage:topic-iceberg-integration.adoc" } ] :page-categories: Management, High Availability -The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without having to set up and configure ETL pipelines. - -// Apache Iceberg is an open source format for defining structured tables in the Data Lake and enables multiple applications to work together on the same data in a consistent fashion and more effectively track dataset states with transactional consistency as changes are made. - -// Reduced cost of data storage, no ETL required, immediate access to late arriving data. - +The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. == Prerequisites @@ -27,14 +22,35 @@ rpk cluster license info == Limitations * You can only enable the Iceberg integration when creating a new Redpanda topic. +* It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. * You can only use one schema per topic. Schema versioning as well as upcasting (where a message is cast into its more generic data type) are not supported. == Architecture +Apache Iceberg is an open source format specification for defining structured tables in a glossterm:data lake[]. The table format enables you to easily and quickly manage, query, and process huge amounts of structured and unstructured data, like you would manage and run SQL queries against relational data in a database or data warehouse. The open format allows many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is also referred to as a _data lakehouse_. + +In the Iceberg specification, tables comprise of: + +* Data layer +** Data files: Store the actual data. The Redpanda integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale, and come with compression capabilities that optimize files for object storage. +* Metadata layer ++ +-- +** Manifest files: Track data files and contain metadata about those files, such as record count, partition membership, and file paths. +** Manifest list: Tracks all the manifest files belonging to a table, including file paths and upper and lower bounds for partition fields. +** Metadata file: Stores metadata about the table, including its schema, partition information, and snapshots. Whenever a change is made to the table, a new metadata file is created and becomes the latest version of the metadata in the catalog. +-- ++ +In the Redpanda Iceberg integration, the manifest files are in JSON format. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. Redpanda creates the catalog in JSON format. + +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to writing Kafka messages stored in segment files on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through business intelligence and machine learning tools that work with Iceberg. + == Enable Iceberg integration -// Enable data lake support +To use the Iceberg integration for a topic, you must set Redpanda cluster configuration `enable_datalake` to true, and also explicitly set the topic configuration to `true`. + . Enable the Iceberg integration for the cluster by setting the cluster configuration `enable_datalake` to `true`. + [,bash] @@ -60,7 +76,7 @@ TOPIC STATUS new-topic OK ---- -. Enable the integration for the topic. +. Enable the integration for the topic. + [,bash] ---- @@ -75,7 +91,7 @@ new-topic OK . Optional: Register a schema for the topic. Redpanda uses the schema to derive the Iceberg table format and write data files to object storage in this format. + -If you don't configure a schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original message timestamp. If you don't have a need for a strict schema and can use the data in a more semi structured format, consider the schemaless approach. +If you don't configure a schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original message timestamp. If you don't have a need for a strict schema and can use the data in a more semi-structured format, consider the schemaless approach. + [,bash] ---- @@ -87,6 +103,14 @@ rpk registry schema create --schema --type Date: Thu, 10 Oct 2024 15:25:28 -0400 Subject: [PATCH 04/45] Schema format support --- modules/manage/pages/topic-iceberg-integration.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index b6016f2d98..efd5136a3a 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -23,13 +23,14 @@ rpk cluster license info * You can only enable the Iceberg integration when creating a new Redpanda topic. * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. +* JSON schemas are not currently supported. * You can only use one schema per topic. Schema versioning as well as upcasting (where a message is cast into its more generic data type) are not supported. == Architecture Apache Iceberg is an open source format specification for defining structured tables in a glossterm:data lake[]. The table format enables you to easily and quickly manage, query, and process huge amounts of structured and unstructured data, like you would manage and run SQL queries against relational data in a database or data warehouse. The open format allows many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is also referred to as a _data lakehouse_. -In the Iceberg specification, tables comprise of: +In the Iceberg specification, tables comprise the following: * Data layer ** Data files: Store the actual data. The Redpanda integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale, and come with compression capabilities that optimize files for object storage. @@ -89,7 +90,7 @@ TOPIC STATUS new-topic OK ---- -. Optional: Register a schema for the topic. Redpanda uses the schema to derive the Iceberg table format and write data files to object storage in this format. +. Optional: Register a schema for the topic. Redpanda uses the schema to derive the Iceberg table format and write data files to object storage in this format. Protobuf schemas are supported. + If you don't configure a schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original message timestamp. If you don't have a need for a strict schema and can use the data in a more semi-structured format, consider the schemaless approach. + From cc78efe5cc10fcd9103bdf1c520ee36ee12c3c0c Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:11:39 -0400 Subject: [PATCH 05/45] Add page categories --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index efd5136a3a..3e3caa8fb4 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -1,7 +1,7 @@ = Topics as Iceberg Tables :description: Learn how to integrate Redpanda topics with Iceberg. :page-context-links: [{"name": "Linux", "to": "manage:topic-iceberg-integration.adoc" } ] -:page-categories: Management, High Availability +:page-categories: Management, High Availability, Data Replication, Integration The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. From 2038794234cbb0b72bf8959c72022117d9d0154b Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:31:27 -0400 Subject: [PATCH 06/45] Edits per review --- modules/manage/pages/topic-iceberg-integration.adoc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 3e3caa8fb4..f0d55d9cd2 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -6,6 +6,8 @@ The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. +The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic is enabled with Tiered Storage, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. + == Prerequisites include::shared:partial$enterprise-license.adoc[] @@ -17,13 +19,14 @@ include::shared:partial$enterprise-license.adoc[] rpk cluster license info ---- +. Enable xref:manage:tiered-storage.adoc#set-up-tiered-storage[Tiered Storage] for the topics for which you want to generate Iceberg tables. . Install `rpk`. == Limitations -* You can only enable the Iceberg integration when creating a new Redpanda topic. +* You can only enable the topic integration for new Redpanda topics. * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. -* JSON schemas are not currently supported. +* JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128KB. * You can only use one schema per topic. Schema versioning as well as upcasting (where a message is cast into its more generic data type) are not supported. == Architecture @@ -43,7 +46,9 @@ In the Iceberg specification, tables comprise the following: -- + In the Redpanda Iceberg integration, the manifest files are in JSON format. -* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. Redpanda creates the catalog in JSON format. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You'll configure your Iceberg catalog to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda uses the https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. + +image::shared:iceberg-integration.png[] When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to writing Kafka messages stored in segment files on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through business intelligence and machine learning tools that work with Iceberg. @@ -105,7 +110,7 @@ SUBJECT VERSION ID TYPE new-topic 1 1 PROTOBUF ---- -As you produce messages to the topic, the data also becomes available to consume from object storage by Iceberg-compliant clients. +As you produce messages to the topic, the data also becomes available to consume from object storage by Iceberg-compatible clients. == Consume data in Iceberg tables From 0c826ede294213b9f8f300b2a327c7ddd5cee5e8 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:31:48 -0400 Subject: [PATCH 07/45] Add architecture diagram --- modules/shared/images/iceberg-integration.png | Bin 0 -> 151491 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/shared/images/iceberg-integration.png diff --git a/modules/shared/images/iceberg-integration.png b/modules/shared/images/iceberg-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..a11db5230db656833e7c54bed0c574f3e3330177 GIT binary patch literal 151491 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 s8cH{lCgKKL@ Date: Sun, 13 Oct 2024 20:34:07 -0400 Subject: [PATCH 08/45] Add to nav tree --- modules/ROOT/nav.adoc | 1 + modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 975de24c5a..23a1abfb78 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -166,6 +166,7 @@ *** xref:manage:mountable-topics.adoc[] *** xref:manage:remote-read-replicas.adoc[Remote Read Replicas] *** xref:manage:topic-recovery.adoc[Topic Recovery] +*** xref:manage:topic-iceberg-integration.adoc[Apache Iceberg Integration] *** xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore] ** xref:manage:schema-reg/index.adoc[Schema Registry] *** xref:manage:schema-reg/schema-reg-overview.adoc[Overview] diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index f0d55d9cd2..1f7dacb2dc 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -1,5 +1,5 @@ = Topics as Iceberg Tables -:description: Learn how to integrate Redpanda topics with Iceberg. +:description: Learn how to integrate Redpanda topics with Apache Iceberg. :page-context-links: [{"name": "Linux", "to": "manage:topic-iceberg-integration.adoc" } ] :page-categories: Management, High Availability, Data Replication, Integration From 7813f7873c1b298137ebe9d1789ea3d054284869 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:23:21 -0400 Subject: [PATCH 09/45] Flesh out example section --- .../pages/topic-iceberg-integration.adoc | 115 +++++++++++++++--- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 1f7dacb2dc..7c8e687d79 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -27,7 +27,7 @@ rpk cluster license info * You can only enable the topic integration for new Redpanda topics. * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. * JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128KB. -* You can only use one schema per topic. Schema versioning as well as upcasting (where a message is cast into its more generic data type) are not supported. +* You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. == Architecture @@ -50,7 +50,7 @@ In the Redpanda Iceberg integration, the manifest files are in JSON format. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to writing Kafka messages stored in segment files on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through business intelligence and machine learning tools that work with Iceberg. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to writing events stored in segment files on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through business intelligence and machine learning tools that work with Iceberg. == Enable Iceberg integration @@ -64,7 +64,7 @@ To use the Iceberg integration for a topic, you must set Redpanda cluster config rpk cluster config set enable_datalake true ---- + -[,bash,.no-copy] +[,bash,role=no-copy] ---- Successfully updated configuration. New configuration version is 2. ---- @@ -73,50 +73,127 @@ Successfully updated configuration. New configuration version is 2. + [,bash,] ---- -rpk topic create --partitions 1 --replicas 1 +rpk topic create --partitions 1 --replicas 1 ---- + -[,bash,.no-copy] +[,bash,role=no-copy] ---- -TOPIC STATUS -new-topic OK +TOPIC STATUS +new-topic-name OK ---- . Enable the integration for the topic. + [,bash] ---- -rpk topic alter-config --set iceberg_topic=true +rpk topic alter-config --set iceberg_topic=true ---- + -[,bash,.no-copy] +[,bash,role=no-copy] ---- -TOPIC STATUS -new-topic OK +TOPIC STATUS +new-topic-name OK ---- -. Optional: Register a schema for the topic. Redpanda uses the schema to derive the Iceberg table format and write data files to object storage in this format. Protobuf schemas are supported. +. Optional: Register a schema for the topic. Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. + -If you don't configure a schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original message timestamp. If you don't have a need for a strict schema and can use the data in a more semi-structured format, consider the schemaless approach. +If there is no registered schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original event timestamp. If you don't have a need for a strict schema and can use the data in a more semi-structured format, consider the schemaless approach. + [,bash] ---- -rpk registry schema create --schema --type +rpk registry schema create --schema --type ---- + -[,bash,.no-copy] +[,bash,role=no-copy] ---- -SUBJECT VERSION ID TYPE -new-topic 1 1 PROTOBUF +SUBJECT VERSION ID TYPE +new-topic-name-value 1 1 PROTOBUF ---- -As you produce messages to the topic, the data also becomes available to consume from object storage by Iceberg-compatible clients. +As you produce records to the topic, the data also becomes available to consume from object storage by Iceberg-compatible clients. == Consume data in Iceberg tables -You can use the same analytical tools to access and manage data just like you would in a relational database. +You can use the same analytical tools to access table data just like you would in a relational database or data lake. +For example, suppose you are producing the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A record looks like the following: +[,bash,role=no-copy] +---- + +---- + +When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on whether you've registered a schema on which to base the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. + +=== Structured approach with schema + +If you register the following schema for `test_topic` under the `test_topic-value` subject: + +[,bash,role=no-copy] +---- +syntax = "proto3"; + +message twitter_record { + string Topic = 1; + string Sentiment = 2; + int64 TweetId = 3; + string TweetText = 4; + string TweetDate = 5; +} +---- + +Many platforms such as Clickhouse provide Iceberg integrations that allow you to easily access existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: + +[,bash] +---- +CREATE TABLE test_topic ENGINE=IcebergS3('', ', '', 'Parquet') +---- + +The table structure is derived from the schema, and in this case, the `test-topic` table contains columns based on the values: + +[,bash] +---- +SELECT + Topic, + Sentiment, + TweetId, + left(TweetText, 30) +FROM test_topic +---- +[,bash,role=no-copy] +---- + +---- + +=== Schemaless approach + +You can also forgo using a schema, which means the data in Iceberg will be semi-structured. + +This example reads the semi-structured data in `test_topic_schemaless` into Clickhouse: + +[,bash] +---- +CREATE TABLE test_topic_schemaless ENGINE=IcebergS3('', ', '', 'Parquet') +---- + +The table consists of columns containing the record key, value, and the original timestamp. + +[,bash] +---- +SELECT + Timestamp, + Key, + left(Value, 30) +FROM test_topic_schemaless +---- + +[,bash,role=no-copy] +---- + +---- +== Suggested reading +* https://www.redpanda.com/blog/apache-iceberg-topics-streaming-data[Apache Iceberg Topics: Stream directly into your data lake +^] \ No newline at end of file From 2a2471f5903b830827661ac463b5178c2d0ed873 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:24:03 -0400 Subject: [PATCH 10/45] Move doc up nav tree section --- modules/ROOT/nav.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 23a1abfb78..bd9b956a3d 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -162,11 +162,11 @@ *** xref:manage:security/iam-roles.adoc[] ** xref:manage:tiered-storage-linux/index.adoc[Tiered Storage] *** xref:manage:tiered-storage.adoc[] +*** xref:manage:topic-iceberg-integration.adoc[Apache Iceberg Integration] *** xref:manage:fast-commission-decommission.adoc[] *** xref:manage:mountable-topics.adoc[] *** xref:manage:remote-read-replicas.adoc[Remote Read Replicas] *** xref:manage:topic-recovery.adoc[Topic Recovery] -*** xref:manage:topic-iceberg-integration.adoc[Apache Iceberg Integration] *** xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore] ** xref:manage:schema-reg/index.adoc[Schema Registry] *** xref:manage:schema-reg/schema-reg-overview.adoc[Overview] From bfba1aa3780d6d74c45479b7056a2494c0183daa Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:36:48 -0400 Subject: [PATCH 11/45] Indicate that SQL is also used --- modules/manage/pages/topic-iceberg-integration.adoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 7c8e687d79..7f958bda0a 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -129,7 +129,7 @@ When you point your Iceberg-compatible tool or framework to the object storage l If you register the following schema for `test_topic` under the `test_topic-value` subject: -[,bash,role=no-copy] +[,protobuf] ---- syntax = "proto3"; @@ -144,14 +144,14 @@ message twitter_record { Many platforms such as Clickhouse provide Iceberg integrations that allow you to easily access existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: -[,bash] +[,sql] ---- CREATE TABLE test_topic ENGINE=IcebergS3('', ', '', 'Parquet') ---- -The table structure is derived from the schema, and in this case, the `test-topic` table contains columns based on the values: +The Iceberg data is available to query in Clickhouse. The table structure is derived from the schema, and in this case, the `test-topic` table contains columns based on the values: -[,bash] +[,sql] ---- SELECT Topic, @@ -172,14 +172,14 @@ You can also forgo using a schema, which means the data in Iceberg will be semi- This example reads the semi-structured data in `test_topic_schemaless` into Clickhouse: -[,bash] +[,sql] ---- CREATE TABLE test_topic_schemaless ENGINE=IcebergS3('', ', '', 'Parquet') ---- The table consists of columns containing the record key, value, and the original timestamp. -[,bash] +[,sql] ---- SELECT Timestamp, From 7f386d30b0b4b161855d3b6ed134aa92a7376f92 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:05:32 -0400 Subject: [PATCH 12/45] Apply suggestions from review --- .../pages/topic-iceberg-integration.adoc | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 7f958bda0a..17ef7ca555 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -4,23 +4,25 @@ :page-categories: Management, High Availability, Data Replication, Integration -The Redpanda integration with Apache Iceberg allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. +The Apache Iceberg integration for Redpanda allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic is enabled with Tiered Storage, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. == Prerequisites -include::shared:partial$enterprise-license.adoc[] +* Install `rpk`. -. To check if you already have a license key applied to your cluster: +* {empty} +include::shared:partial$enterprise-license.adoc[] ++ +To check if you already have a license key applied to your cluster: + [,bash] ---- rpk cluster license info ---- -. Enable xref:manage:tiered-storage.adoc#set-up-tiered-storage[Tiered Storage] for the topics for which you want to generate Iceberg tables. -. Install `rpk`. +* Enable xref:manage:tiered-storage.adoc#set-up-tiered-storage[Tiered Storage] for the topics for which you want to generate Iceberg tables. == Limitations @@ -36,7 +38,7 @@ Apache Iceberg is an open source format specification for defining structured ta In the Iceberg specification, tables comprise the following: * Data layer -** Data files: Store the actual data. The Redpanda integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale, and come with compression capabilities that optimize files for object storage. +** Data files: Store the actual data. The Iceberg integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale, and come with compression capabilities that optimize files for object storage. * Metadata layer + -- @@ -55,7 +57,7 @@ When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers s == Enable Iceberg integration -To use the Iceberg integration for a topic, you must set Redpanda cluster configuration `enable_datalake` to true, and also explicitly set the topic configuration to `true`. +To use the Iceberg integration for a topic, you must set Redpanda cluster configuration `enable_datalake` to true, and also explicitly set the topic configuration to `true`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. . Enable the Iceberg integration for the cluster by setting the cluster configuration `enable_datalake` to `true`. + @@ -118,6 +120,7 @@ You can use the same analytical tools to access table data just like you would i For example, suppose you are producing the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A record looks like the following: +// add sample records here [,bash,role=no-copy] ---- @@ -127,7 +130,7 @@ When you point your Iceberg-compatible tool or framework to the object storage l === Structured approach with schema -If you register the following schema for `test_topic` under the `test_topic-value` subject: +Register the following schema for `test_topic` under the `test_topic-value` subject: [,protobuf] ---- @@ -142,6 +145,11 @@ message twitter_record { } ---- +[,bash] +---- +rpk registry schema create test_topic-value --schema path/to/schema.proto --type protobuf +---- + Many platforms such as Clickhouse provide Iceberg integrations that allow you to easily access existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: [,sql] @@ -149,7 +157,7 @@ Many platforms such as Clickhouse provide Iceberg integrations that allow you to CREATE TABLE test_topic ENGINE=IcebergS3('', ', '', 'Parquet') ---- -The Iceberg data is available to query in Clickhouse. The table structure is derived from the schema, and in this case, the `test-topic` table contains columns based on the values: +The Iceberg data is available to query in Clickhouse. The table structure is derived from the schema. In this case, the `test-topic` table contains columns based on the schema fields: [,sql] ---- @@ -161,6 +169,7 @@ SELECT FROM test_topic ---- +// add SQL output here to show structured table data [,bash,role=no-copy] ---- @@ -188,6 +197,7 @@ SELECT FROM test_topic_schemaless ---- +// add SQL output here to show semi-structured table data [,bash,role=no-copy] ---- From 2d50e5a111f404249ebe708a41d471c59eb1949f Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:14:24 -0400 Subject: [PATCH 13/45] Apply suggestions from review --- modules/manage/pages/topic-iceberg-integration.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 17ef7ca555..8ab909a8bb 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -38,7 +38,7 @@ Apache Iceberg is an open source format specification for defining structured ta In the Iceberg specification, tables comprise the following: * Data layer -** Data files: Store the actual data. The Iceberg integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale, and come with compression capabilities that optimize files for object storage. +** Data files: Store the data. The Iceberg integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale. They come with compression capabilities that optimize files for object storage. * Metadata layer + -- @@ -57,7 +57,7 @@ When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers s == Enable Iceberg integration -To use the Iceberg integration for a topic, you must set Redpanda cluster configuration `enable_datalake` to true, and also explicitly set the topic configuration to `true`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. +To use the Iceberg integration for a topic, you must set the cluster configuration property `enable_datalake` to `true`, and also explicitly set the topic configuration to `true`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. . Enable the Iceberg integration for the cluster by setting the cluster configuration `enable_datalake` to `true`. + From e491f14380af3af1bcce3798aa70006c11b0d06e Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:09:21 -0400 Subject: [PATCH 14/45] Apply suggestions from code review Co-authored-by: Angela Simms <102690377+asimms41@users.noreply.github.com> --- .../pages/topic-iceberg-integration.adoc | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 8ab909a8bb..9644fd13ad 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -6,7 +6,7 @@ The Apache Iceberg integration for Redpanda allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. -The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic is enabled with Tiered Storage, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. +The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic has Tiered Storage enabled, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. == Prerequisites @@ -26,14 +26,14 @@ rpk cluster license info == Limitations -* You can only enable the topic integration for new Redpanda topics. +* You can only enable Iceberg topic integration for new Redpanda topics. * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. -* JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128KB. +* JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128 KB. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. == Architecture -Apache Iceberg is an open source format specification for defining structured tables in a glossterm:data lake[]. The table format enables you to easily and quickly manage, query, and process huge amounts of structured and unstructured data, like you would manage and run SQL queries against relational data in a database or data warehouse. The open format allows many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is also referred to as a _data lakehouse_. +Apache Iceberg is an open source format specification for defining structured tables in a glossterm:data lake[]. The table format lets you quickly and easily manage, query, and process huge amounts of structured and unstructured data. This is similar to the way in which you would manage and run SQL queries against relational data in a database or data warehouse. The open format lets you use many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is referred to as a _data lakehouse_. In the Iceberg specification, tables comprise the following: @@ -42,7 +42,7 @@ In the Iceberg specification, tables comprise the following: * Metadata layer + -- -** Manifest files: Track data files and contain metadata about those files, such as record count, partition membership, and file paths. +** Manifest files: Track data files and contain metadata about these files, such as record count, partition membership, and file paths. ** Manifest list: Tracks all the manifest files belonging to a table, including file paths and upper and lower bounds for partition fields. ** Metadata file: Stores metadata about the table, including its schema, partition information, and snapshots. Whenever a change is made to the table, a new metadata file is created and becomes the latest version of the metadata in the catalog. -- @@ -52,14 +52,14 @@ In the Redpanda Iceberg integration, the manifest files are in JSON format. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to writing events stored in segment files on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through business intelligence and machine learning tools that work with Iceberg. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage. The brokers also write events to segment files stored on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through compatible business intelligence and machine learning tools. == Enable Iceberg integration To use the Iceberg integration for a topic, you must set the cluster configuration property `enable_datalake` to `true`, and also explicitly set the topic configuration to `true`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. -. Enable the Iceberg integration for the cluster by setting the cluster configuration `enable_datalake` to `true`. +. Set the `enable_datalake` configuration option on your cluster to `true`. + [,bash] ---- @@ -97,9 +97,9 @@ TOPIC STATUS new-topic-name OK ---- -. Optional: Register a schema for the topic. Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. +. Register a schema for the topic (optional). Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. + -If there is no registered schema for the topic, Redpanda by default uses a simple schema consisting of the record’s key, value, and the original event timestamp. If you don't have a need for a strict schema and can use the data in a more semi-structured format, consider the schemaless approach. +If there is no registered schema for the topic, Redpanda uses a simple schema consisting of the record’s key, value, and the original event timestamp. Consider the schemaless approach if you can use the data in a more semi-structured format. + [,bash] ---- @@ -112,13 +112,13 @@ SUBJECT VERSION ID TYPE new-topic-name-value 1 1 PROTOBUF ---- -As you produce records to the topic, the data also becomes available to consume from object storage by Iceberg-compatible clients. +As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. == Consume data in Iceberg tables -You can use the same analytical tools to access table data just like you would in a relational database or data lake. +You can use the same analytical tools to access table data in a data lake as you would for a relational database. -For example, suppose you are producing the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A record looks like the following: +For example, suppose you produce the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A record looks like the following: // add sample records here [,bash,role=no-copy] @@ -150,7 +150,7 @@ message twitter_record { rpk registry schema create test_topic-value --schema path/to/schema.proto --type protobuf ---- -Many platforms such as Clickhouse provide Iceberg integrations that allow you to easily access existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: +Platforms such as Clickhouse provide Iceberg integrations to give easy access to existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: [,sql] ---- @@ -177,7 +177,7 @@ FROM test_topic === Schemaless approach -You can also forgo using a schema, which means the data in Iceberg will be semi-structured. +You can also forgo using a schema, which means using semi-structured data in Iceberg. This example reads the semi-structured data in `test_topic_schemaless` into Clickhouse: From 82adf47ccee3440bd29cf807aa5edf2b7553bcfc Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:44:16 -0400 Subject: [PATCH 15/45] Remove glossterm for now --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 9644fd13ad..c6a674effb 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -33,7 +33,7 @@ rpk cluster license info == Architecture -Apache Iceberg is an open source format specification for defining structured tables in a glossterm:data lake[]. The table format lets you quickly and easily manage, query, and process huge amounts of structured and unstructured data. This is similar to the way in which you would manage and run SQL queries against relational data in a database or data warehouse. The open format lets you use many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is referred to as a _data lakehouse_. +Apache Iceberg is an open source format specification for defining structured tables in a data lake. The table format lets you quickly and easily manage, query, and process huge amounts of structured and unstructured data. This is similar to the way in which you would manage and run SQL queries against relational data in a database or data warehouse. The open format lets you use many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is referred to as a _data lakehouse_. In the Iceberg specification, tables comprise the following: From 14024609e5fb98dd171af328736ad8631e5701d3 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:06:48 -0400 Subject: [PATCH 16/45] Additional edit from review --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index c6a674effb..4888a7baf3 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -4,7 +4,7 @@ :page-categories: Management, High Availability, Data Replication, Integration -The Apache Iceberg integration for Redpanda allows your topic data to be stored in the cloud in Iceberg's table format. This opens up immediate access to your streaming data for analytical systems such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. +The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in Iceberg's table format. This makes your streaming data immediately available for analytical systems, such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms, such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic has Tiered Storage enabled, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. From dbfab484632124cbd4db474b7599924ef51ae073 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:07:20 -0400 Subject: [PATCH 17/45] Add sample records --- .../pages/topic-iceberg-integration.adoc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 4888a7baf3..8338f26633 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -118,12 +118,11 @@ As you produce records to the topic, the data also becomes available in object s You can use the same analytical tools to access table data in a data lake as you would for a relational database. -For example, suppose you produce the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A record looks like the following: +For example, suppose you produce the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A sample record contains the following data: -// add sample records here [,bash,role=no-copy] ---- - +{'Topic': 'apple', 'Sentiment': 'positive', 'TweetId': '126377656416612353', 'TweetDate': 'Tue Oct 18 19:22:35 +0000 2011', 'TweetText': 'Great up close & personal event @Apple tonight in Regent St store!'} ---- When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on whether you've registered a schema on which to base the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. @@ -169,10 +168,13 @@ SELECT FROM test_topic ---- -// add SQL output here to show structured table data [,bash,role=no-copy] ---- - ++-------+-----------+--------------------+--------------------------------+ +| Topic | Sentiment | TweetId | left(TweetText, 30) | ++-------+-----------+--------------------+--------------------------------+ +| apple | positive | 126377656416612353 | Great up close & personal even | ++-------+-----------+--------------------+--------------------------------+ ---- === Schemaless approach @@ -197,10 +199,13 @@ SELECT FROM test_topic_schemaless ---- -// add SQL output here to show semi-structured table data [,bash,role=no-copy] ---- - ++---------------+-------+--------------------------------+ +| Timestamp | Key | left(Value, 30) | ++---------------+-------+--------------------------------+ +| 1728593400000 | apple | {"Topic": "apple", "Sentiment" | ++---------------+-------+--------------------------------+ ---- == Suggested reading From 00786d01de3c3292cb1ddad688ade7d78c12e380 Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:52:39 -0400 Subject: [PATCH 18/45] Update modules/manage/pages/topic-iceberg-integration.adoc Co-authored-by: Angela Simms <102690377+asimms41@users.noreply.github.com> --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 8338f26633..aea5b3d125 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -99,7 +99,7 @@ new-topic-name OK . Register a schema for the topic (optional). Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. + -If there is no registered schema for the topic, Redpanda uses a simple schema consisting of the record’s key, value, and the original event timestamp. Consider the schemaless approach if you can use the data in a more semi-structured format. +If there is no registered schema for the topic, Redpanda uses a simple schema consisting of the record’s key, value, and the original event timestamp. Consider this schemaless approach if you can use the data in a more semi-structured format. + [,bash] ---- From 5822c3f0541179cce0667ad427a125cd1629ea82 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:16:50 -0500 Subject: [PATCH 19/45] Add beta changes to config properties --- .../pages/topic-iceberg-integration.adoc | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index aea5b3d125..24ab3ffd73 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -2,6 +2,7 @@ :description: Learn how to integrate Redpanda topics with Apache Iceberg. :page-context-links: [{"name": "Linux", "to": "manage:topic-iceberg-integration.adoc" } ] :page-categories: Management, High Availability, Data Replication, Integration +:page-beta: true The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in Iceberg's table format. This makes your streaming data immediately available for analytical systems, such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms, such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. @@ -54,12 +55,11 @@ image::shared:iceberg-integration.png[] When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage. The brokers also write events to segment files stored on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through compatible business intelligence and machine learning tools. - == Enable Iceberg integration -To use the Iceberg integration for a topic, you must set the cluster configuration property `enable_datalake` to `true`, and also explicitly set the topic configuration to `true`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. +To use the Iceberg integration for a topic, you must set the cluster configuration property `iceberg_enabled` to `true`, and also configure the topic property `redpanda.iceberg.mode`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. -. Set the `enable_datalake` configuration option on your cluster to `true`. +. Set the `iceberg_enabled` configuration option on your cluster to `true`. + [,bash] ---- @@ -84,11 +84,15 @@ TOPIC STATUS new-topic-name OK ---- -. Enable the integration for the topic. +. Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: ++ +. `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. +. `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +. `DISABLED` (default): Disables the creation of an Iceberg table for this topic. + [,bash] ---- -rpk topic alter-config --set iceberg_topic=true +rpk topic alter-config --set redpanda.iceberg.mode= ---- + [,bash,role=no-copy] @@ -97,9 +101,7 @@ TOPIC STATUS new-topic-name OK ---- -. Register a schema for the topic (optional). Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. -+ -If there is no registered schema for the topic, Redpanda uses a simple schema consisting of the record’s key, value, and the original event timestamp. Consider this schemaless approach if you can use the data in a more semi-structured format. +. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `VALUE_SCHEMA_ID_PREFIX`. Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. + [,bash] ---- @@ -111,6 +113,10 @@ rpk registry schema create --schema --type Date: Tue, 19 Nov 2024 17:27:38 -0500 Subject: [PATCH 20/45] Missed config name --- modules/manage/pages/topic-iceberg-integration.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 24ab3ffd73..3e5cb604c6 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -49,7 +49,7 @@ In the Iceberg specification, tables comprise the following: -- + In the Redpanda Iceberg integration, the manifest files are in JSON format. -* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You'll configure your Iceberg catalog to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda uses the https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You'll <> to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda uses the https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. image::shared:iceberg-integration.png[] @@ -63,7 +63,7 @@ To use the Iceberg integration for a topic, you must set the cluster configurati + [,bash] ---- -rpk cluster config set enable_datalake true +rpk cluster config set iceberg_enabled true ---- + [,bash,role=no-copy] @@ -218,6 +218,7 @@ FROM test_topic_schemaless == Integrate into Iceberg catalog + //// Comment out due to renamed properties and other changes to beta From fe37d63f39a5099eb707803b05fbab3c5dbe989a Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:53:49 -0500 Subject: [PATCH 21/45] Address some SME feedback --- .../pages/topic-iceberg-integration.adoc | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 3e5cb604c6..2dd2f77f0f 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -5,7 +5,7 @@ :page-beta: true -The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in Iceberg's table format. This makes your streaming data immediately available for analytical systems, such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms, such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. +The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in the Iceberg open table format. This makes your streaming data immediately available for analytical systems, such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms, such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic has Tiered Storage enabled, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. @@ -27,7 +27,6 @@ rpk cluster license info == Limitations -* You can only enable Iceberg topic integration for new Redpanda topics. * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. * JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128 KB. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. @@ -36,7 +35,7 @@ rpk cluster license info Apache Iceberg is an open source format specification for defining structured tables in a data lake. The table format lets you quickly and easily manage, query, and process huge amounts of structured and unstructured data. This is similar to the way in which you would manage and run SQL queries against relational data in a database or data warehouse. The open format lets you use many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is referred to as a _data lakehouse_. -In the Iceberg specification, tables comprise the following: +In the Iceberg specification, tables consist of the following layers: * Data layer ** Data files: Store the data. The Iceberg integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale. They come with compression capabilities that optimize files for object storage. @@ -49,11 +48,11 @@ In the Iceberg specification, tables comprise the following: -- + In the Redpanda Iceberg integration, the manifest files are in JSON format. -* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You'll <> to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda uses the https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You can <> to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda can also use an https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage. The brokers also write events to segment files stored on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through compatible business intelligence and machine learning tools. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage. The brokers also write events to segment files stored on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible https://iceberg.apache.org/vendors/[data lakehouse, data engineering, and business intelligence tools]. == Enable Iceberg integration @@ -88,7 +87,7 @@ new-topic-name OK + . `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. . `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. -. `DISABLED` (default): Disables the creation of an Iceberg table for this topic. +. `DISABLED` (default): Disables writing to an Iceberg table for this topic. + [,bash] ---- @@ -120,6 +119,20 @@ NOTE: Redpanda automatically adds three Kafka-related fields to the table it cre As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. +== Integrate Iceberg catalog + +You can configure the Iceberg integration to either create a file in the object storage bucket or container to serve as the catalog, or connect to a REST-based catalog. + +Set the cluster configuration property `iceberg_catalog_type`: +* `OBJECT_STORAGE`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda tables. +* `REST`: Connect to and update an Iceberg catalog hosted on a REST server. + +For an Iceberg REST catalog, set the following additional cluster configuration properties: + +* `iceberg_rest_catalog_endpoint` +* `iceberg_rest_catalog_client_id` +* `iceberg_rest_catalog_client_secret` + == Consume data in Iceberg tables You can use the same analytical tools to access table data in a data lake as you would for a relational database. @@ -215,9 +228,6 @@ FROM test_topic_schemaless +---------------+-------+--------------------------------+ ---- -== Integrate into Iceberg catalog - - //// Comment out due to renamed properties and other changes to beta From 5bab0f28c671f1c0c1fd2cc64520141c81837d7f Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:44:27 -0500 Subject: [PATCH 22/45] Replace example --- .../pages/topic-iceberg-integration.adoc | 177 +++++++++++------- 1 file changed, 109 insertions(+), 68 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 2dd2f77f0f..bc4d60f9f4 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -85,9 +85,11 @@ new-topic-name OK . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: + -. `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. -. `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. -. `DISABLED` (default): Disables writing to an Iceberg table for this topic. +-- +* `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. +* `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +* `DISABLED` (default): Disables writing to an Iceberg table for this topic. +-- + [,bash] ---- @@ -100,7 +102,7 @@ TOPIC STATUS new-topic-name OK ---- -. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `VALUE_SCHEMA_ID_PREFIX`. Redpanda uses xref:manage:schema-reg/schema-id-validation.adoc#set-subject-name-strategy-per-topic[`TopicNameStrategy`] (`-value` or `-key`) to find the matching schema based on the subject name. Redpanda derives the Iceberg table format from the schema and writes data files to object storage in this format. Protobuf and Avro schemas are supported. +. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `VALUE_SCHEMA_ID_PREFIX`. + [,bash] ---- @@ -113,12 +115,77 @@ SUBJECT VERSION ID TYPE new-topic-name-value 1 1 PROTOBUF ---- + -If you select `KEY_VALUE` for the topic Iceberg mode, Redpanda uses a simple schema consisting of the record’s key, value, and the original record timestamp. Consider this schemaless approach if you can use the data in a more semi-structured format. -+ -NOTE: Redpanda automatically adds three Kafka-related fields to the table it creates: the record timestamp, the partition it belongs to, and its offset. +If you select `KEY_VALUE` for the topic Iceberg mode, Redpanda uses a simple schema consisting of a column that stores the record’s metadata, key, and value. As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. +== Schema mapping and support + +The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. + +For both `KEY_VALUE` and `VALUE_SCHEMA_ID_PREFIX` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record timestamp, the partition it belongs to, and its offset. In the `KEY_VALUE` mode, the `redpanda` metadata structs also contain both the record key and value. If you are associating a schema with the topic using the `VALUE_SCHEMA_ID_PREFIX` mode, the `redpanda` structs contain the record key only. Redpanda uses the matching schema to define table columns based on schema fields and then maps the record values to the corresponding columns. + +For example, if you produce to a topic according to the following Avro schema: + +``` +{ + "type": "record", + "name": "ClickEvent", + "fields": [ + { + "name": "user_id", + "type": "int" + }, + { + "name": "event_type", + "type": "string" + }, + { + "name": "ts", + "type": "string" + } + ] +} + +``` + +The `KEY_VALUE` mode writes to the table format: + +``` +CREATE TABLE ClickEvent ( + redpanda OBJECT( + partition INT, + timestamp TIMESTAMP, + offset LONG, + headers ARRAY(OBJECT(KEY BINARY, VALUE BINARY NULLABLE)), + key BINARY NULLABLE, + data BINARY NULLABLE + ) +) +``` + +Consider this schemaless approach if you are using the JSON Schema format, or if you can use the data in a semi-structured format. + +The `VALUE_SCHEMA_ID_PREFIX` translates to the following table format: + +``` +CREATE TABLE ClickEvent ( + redpanda OBJECT( + partition INT, + timestamp TIMESTAMP, + offset LONG, + headers ARRAY(OBJECT(KEY BINARY, VALUE BINARY NULLABLE)), + key BINARY NULLABLE + ), + user_id INT, + event_type STRING NULLABLE, + ts STRING NULLABLE +); + +``` + +With schema integration, Redpanda uses the schema ID prefix embedded in each record to find the matching schema in the Schema Registry. Producers to the topic must use the schema ID prefix in the serialization process so Redpanda can determine the schema used for each record, parse the record, and use that schema for the Iceberg table as well. + == Integrate Iceberg catalog You can configure the Iceberg integration to either create a file in the object storage bucket or container to serve as the catalog, or connect to a REST-based catalog. @@ -137,103 +204,77 @@ For an Iceberg REST catalog, set the following additional cluster configuration You can use the same analytical tools to access table data in a data lake as you would for a relational database. -For example, suppose you produce the same stream of events to a topic `test_topic`, and another topic `test_topic_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A sample record contains the following data: +For example, suppose you produce the same stream of events to a topic `ClickEvent`, and another topic `ClickEvent_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A sample record contains the following data: [,bash,role=no-copy] ---- -{'Topic': 'apple', 'Sentiment': 'positive', 'TweetId': '126377656416612353', 'TweetDate': 'Tue Oct 18 19:22:35 +0000 2011', 'TweetText': 'Great up close & personal event @Apple tonight in Regent St store!'} +{"user_id": 2324, "event_type": "BUTTON_CLICK", "ts": "2024-11-25T20:23:59.380Z"} ---- -When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on whether you've registered a schema on which to base the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. +When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on whether you've registered a schema on which to derive the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. === Structured approach with schema -Register the following schema for `test_topic` under the `test_topic-value` subject: +Register the following schema for `ClickEvent` under the `ClickEvent-value` subject: -[,protobuf] +[,avro] ---- -syntax = "proto3"; - -message twitter_record { - string Topic = 1; - string Sentiment = 2; - int64 TweetId = 3; - string TweetText = 4; - string TweetDate = 5; -} +{ + "type" : "record", + "namespace" : "com.redpanda.examples.avro", + "name" : "ClickEvent", + "fields" : [ + { "name": "user_id", "type" : "int" }, + { "name": "event_type", "type" : "string" }, + { "name": "ts", "type": "string" } + ] + } ---- [,bash] ---- -rpk registry schema create test_topic-value --schema path/to/schema.proto --type protobuf +rpk registry schema create ClickEvent-value --schema path/to/schema.avsc --type avro ---- -// Change to different example -Platforms such as Clickhouse provide Iceberg integrations to give easy access to existing Iceberg tables in object storage. This example reads the data from `test_topic` into Clickhouse: +Query engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, the `ClickEvent` table contains columns based on the schema fields: [,sql] ---- -CREATE TABLE test_topic ENGINE=IcebergS3('', ', '', 'Parquet') ----- - -The Iceberg data is available to query in Clickhouse. The table structure is derived from the schema. In this case, the `test-topic` table contains columns based on the schema fields: - -[,sql] ----- -SELECT - Topic, - Sentiment, - TweetId, - left(TweetText, 30) -FROM test_topic +SELECT user_id, + event_type, + ts +FROM ClickEvent; ---- [,bash,role=no-copy] ---- -+-------+-----------+--------------------+--------------------------------+ -| Topic | Sentiment | TweetId | left(TweetText, 30) | -+-------+-----------+--------------------+--------------------------------+ -| apple | positive | 126377656416612353 | Great up close & personal even | -+-------+-----------+--------------------+--------------------------------+ ++---------+--------------+--------------------------+ +| user_id | event_type | ts | ++---------+--------------+--------------------------+ +| 2324 | BUTTON_CLICK | 2024-11-25T20:23:59.380Z | ++---------+--------------+--------------------------+ ---- === Schemaless approach You can also forgo using a schema, which means using semi-structured data in Iceberg. -This example reads the semi-structured data in `test_topic_schemaless` into Clickhouse: - -[,sql] ----- -CREATE TABLE test_topic_schemaless ENGINE=IcebergS3('', ', '', 'Parquet') ----- - -The table consists of columns containing the record key, value, and the original timestamp. +This example reads the semi-structured data in the `ClickEvent_schemaless` table, which consists of a column `redpanda` containing the record key, value, and metadata: [,sql] ---- -SELECT - Timestamp, - Key, - left(Value, 30) -FROM test_topic_schemaless +SELECT + redpanda.data +FROM ClickEvent_schemaless; ---- [,bash,role=no-copy] ---- -+---------------+-------+--------------------------------+ -| Timestamp | Key | left(Value, 30) | -+---------------+-------+--------------------------------+ -| 1728593400000 | apple | {"Topic": "apple", "Sentiment" | -+---------------+-------+--------------------------------+ ++------------------------------------------------------------------------------+ +| redpanda.data | ++------------------------------------------------------------------------------+ +| {"user_id":2324,"event_type":"BUTTON_CLICK","ts":"2024-11-25T20:23:59.380Z"} | ++------------------------------------------------------------------------------+ ---- - -//// -Comment out due to renamed properties and other changes to beta - == Suggested reading - -* https://www.redpanda.com/blog/apache-iceberg-topics-streaming-data[Apache Iceberg Topics: Stream directly into your data lake -^] -//// \ No newline at end of file From 70eaccab18e3b80f6a7145890a49ed1c11c550d2 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:44:46 -0500 Subject: [PATCH 23/45] Start to add topic properties --- .../pages/properties/topic-properties.adoc | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/modules/reference/pages/properties/topic-properties.adoc b/modules/reference/pages/properties/topic-properties.adoc index 7276488f24..c50fc9a7cc 100644 --- a/modules/reference/pages/properties/topic-properties.adoc +++ b/modules/reference/pages/properties/topic-properties.adoc @@ -504,6 +504,36 @@ CAUTION: Setting `redpanda.remote.readreplica` together with either `redpanda.re --- +=== Apache Iceberg integration properties + +Integrate Redpanda topics as Iceberg tables. + +==== redpanda.iceberg.mode + +Enable the Iceberg integration for the topic. You can choose one of three modes. + +**Default**: `DISABLED` + +**Values**: + +- `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. +- `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for the topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +- `DISABLED`: Disables writing to an Iceberg table for the topic. + +**Related topics**: + +- xref:manage:topic-iceberg-integration.adoc[] + +==== redpanda.iceberg.delete + +Whether the corresponding Iceberg table is deleted upon deleting the topic. + +**Default**: `true` + +**Related topics**: + +- xref:manage:topic-iceberg-integration.adoc[] + === Redpanda topic properties Configure Redpanda-specific topic properties. From 7ac675c3f685e4db1a3b9d9f0bdc9be16c041ab3 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:07:40 -0500 Subject: [PATCH 24/45] Add type translation details --- .../pages/topic-iceberg-integration.adoc | 102 ++++++++++++++---- 1 file changed, 84 insertions(+), 18 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index bc4d60f9f4..274522bf41 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -29,7 +29,8 @@ rpk cluster license info * It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. * JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128 KB. -* You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. +* You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. See <> for more details. +* xref:manage:remote-read-replicas.adoc[Remote read replicas] and xref:manage:topic-recovery.adoc[topic recovery] are not supported for Iceberg-enabled topics. == Architecture @@ -48,7 +49,7 @@ In the Iceberg specification, tables consist of the following layers: -- + In the Redpanda Iceberg integration, the manifest files are in JSON format. -* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. You can <> to point to your object storage bucket or container where the Redpanda data in Iceberg format is located. Redpanda can also use an https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update your catalog when there are changes to the Iceberg data and metadata. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. The Iceberg integration supports two <> types. You can configure Redpanda to catalog files stored in the same object storage bucket or container where the Iceberg data files are located, or you can configure Redpanda to use an https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update an externally-managed catalog when there are changes to the Iceberg data and metadata. image::shared:iceberg-integration.png[] @@ -56,7 +57,7 @@ When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers s == Enable Iceberg integration -To use the Iceberg integration for a topic, you must set the cluster configuration property `iceberg_enabled` to `true`, and also configure the topic property `redpanda.iceberg.mode`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. +To create an Iceberg table for a Redpanda topic, you must set the cluster configuration property `iceberg_enabled` to `true`, and also configure the topic property `redpanda.iceberg.mode`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. . Set the `iceberg_enabled` configuration option on your cluster to `true`. + @@ -86,7 +87,7 @@ new-topic-name OK . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: + -- -* `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. +* `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. We also refer to this as the "schemaless" mode. * `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. * `DISABLED` (default): Disables writing to an Iceberg table for this topic. -- @@ -115,15 +116,19 @@ SUBJECT VERSION ID TYPE new-topic-name-value 1 1 PROTOBUF ---- + -If you select `KEY_VALUE` for the topic Iceberg mode, Redpanda uses a simple schema consisting of a column that stores the record’s metadata, key, and value. +If you don't use a schema for the topic, select `KEY_VALUE` for the topic Iceberg mode. Redpanda uses a simple schema for the Iceberg table, consisting of a column that stores the record’s metadata, key, and value. As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. -== Schema mapping and support +== Schema support and mapping -The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. +The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the schemaless mode where Redpanda stores the record values as-is in the table. The JSON Schema format is not supported in this beta release. If your topic data is in JSON, it is recommended to use the `KEY_VALUE` (schemaless) mode. -For both `KEY_VALUE` and `VALUE_SCHEMA_ID_PREFIX` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record timestamp, the partition it belongs to, and its offset. In the `KEY_VALUE` mode, the `redpanda` metadata structs also contain both the record key and value. If you are associating a schema with the topic using the `VALUE_SCHEMA_ID_PREFIX` mode, the `redpanda` structs contain the record key only. Redpanda uses the matching schema to define table columns based on schema fields and then maps the record values to the corresponding columns. +=== Iceberg table modes + +For both `KEY_VALUE` and `VALUE_SCHEMA_ID_PREFIX` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record timestamp, the partition it belongs to, and its offset. + +In the `KEY_VALUE` ("schemaless") mode, the `redpanda` metadata structs also contain both the record key and value. If you are associating a schema with the topic using the `VALUE_SCHEMA_ID_PREFIX` mode, the `redpanda` structs contain the record key only. Redpanda uses the matching schema to define table columns based on schema fields and then maps the record values to the corresponding columns. For example, if you produce to a topic according to the following Avro schema: @@ -149,7 +154,7 @@ For example, if you produce to a topic according to the following Avro schema: ``` -The `KEY_VALUE` mode writes to the table format: +The `KEY_VALUE` mode writes to the following table format: ``` CREATE TABLE ClickEvent ( @@ -164,7 +169,7 @@ CREATE TABLE ClickEvent ( ) ``` -Consider this schemaless approach if you are using the JSON Schema format, or if you can use the data in a semi-structured format. +Consider this schemaless approach if the topic data is in JSON, or if you can use the Iceberg data in its semi-structured format. The `VALUE_SCHEMA_ID_PREFIX` translates to the following table format: @@ -184,21 +189,82 @@ CREATE TABLE ClickEvent ( ``` -With schema integration, Redpanda uses the schema ID prefix embedded in each record to find the matching schema in the Schema Registry. Producers to the topic must use the schema ID prefix in the serialization process so Redpanda can determine the schema used for each record, parse the record, and use that schema for the Iceberg table as well. +With schema integration, Redpanda uses the schema ID prefix embedded in each record to find the matching schema in the Schema Registry. Producers to the topic must use the schema ID prefix in the serialization process so Redpanda can determine the schema used for each record, parse the record according to that schema, and use the schema for the Iceberg table as well. + +=== Schema types translation + +[tabs] +====== +Avro:: ++ +-- +* Redpanda supports direct translations of the following Avro types to Iceberg value domains: +** boolean +** int +** long +** float +** double +** bytes +** string +** record +** array +** maps +** fixed +** decimal +** uuid +** date +** time +** timestamp +* Different flavors of timestamp and time types are all represented by the same Iceberg type, conversion will be done. +* Avro unions are flattened to structs with optional fields, out of which only one is present, or none in case there is a field with null type: +** For example, the union `["int", "long", "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 LONG NULLABLE, 2 FLOAT NULLABLE)`. +** The union `["int", null, "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 FLOAT NULLABLE)`. +* All fields are required by default (Avro always sets a default in binary representation). +* Duration logical type is ignored. +* Null type is ignored and not represented in Iceberg schema. +* Recursive types are not supported. +-- + +Protobuf:: ++ +-- +* Redpanda supports direct translations of the following Avro types to Iceberg value domains: +** BOOL +** DOUBLE +** FLOAT +** INT32 +** SINT32 +** INT64 +** SINT64 +** SFIXED32 +** SFIXED64 +** STRING +** BYTES +** MAP +* Repeated values are translated into Iceberg ARRAY types. +* Enums are translated into Iceberg INT types based on the integer value of the enumerated type. +* UINT32 and FIXED32 are translated into Iceberg LONG types as that is the existing semantic for unsigned 32bit values in Iceberg. +* UINT64 and FIXED64 values are translated into their base 10 string representation. +* Timestamp well known protos are translated into TIMESTAMP Iceberg types. +* Messages are converted into STRUCT types. +* Recursive types are not supported. +-- +====== + +== Set up catalog integration -== Integrate Iceberg catalog +You can configure the Iceberg integration to either create a file in the same object storage bucket or container to serve as the catalog, or connect to a REST-based catalog. -You can configure the Iceberg integration to either create a file in the object storage bucket or container to serve as the catalog, or connect to a REST-based catalog. +Set the cluster configuration property `iceberg_catalog_type` with one of the following values: -Set the cluster configuration property `iceberg_catalog_type`: -* `OBJECT_STORAGE`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda tables. +* `OBJECT_STORAGE`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. * `REST`: Connect to and update an Iceberg catalog hosted on a REST server. For an Iceberg REST catalog, set the following additional cluster configuration properties: -* `iceberg_rest_catalog_endpoint` -* `iceberg_rest_catalog_client_id` -* `iceberg_rest_catalog_client_secret` +* `iceberg_rest_catalog_endpoint`: The endpoint URL for your Iceberg catalog, which you are either managing directly, or is managed by an external catalog service. +* `iceberg_rest_catalog_client_id`: The ID to connect to the REST server. +* `iceberg_rest_catalog_client_secret`: The secret data to connect to the REST server. == Consume data in Iceberg tables From ed3eb85593ebff6d9984b747818e7b31fe09d14d Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:26:05 -0500 Subject: [PATCH 25/45] Prefix topic property --- modules/manage/pages/topic-iceberg-integration.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 274522bf41..1bdfa08a0a 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -263,10 +263,14 @@ Set the cluster configuration property `iceberg_catalog_type` with one of the fo For an Iceberg REST catalog, set the following additional cluster configuration properties: * `iceberg_rest_catalog_endpoint`: The endpoint URL for your Iceberg catalog, which you are either managing directly, or is managed by an external catalog service. +* `iceberg_rest_catalog_prefix`: Append a prefix to the catalog path, e.g. `/v1/{prefix}/namespaces`. * `iceberg_rest_catalog_client_id`: The ID to connect to the REST server. * `iceberg_rest_catalog_client_secret`: The secret data to connect to the REST server. -== Consume data in Iceberg tables +// update xref when PR for extracted properties is ready +See xref:reference:properties/cluster-properties.adoc[Cluster Configuration Properties] for the full list of cluster properties to configure for a catalog integration. + +== Access data in Iceberg tables You can use the same analytical tools to access table data in a data lake as you would for a relational database. From 293d28fb70889c7c06127072cb1028c835a30b41 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:39:04 -0500 Subject: [PATCH 26/45] Simple explanation for rest catalog usage --- .../pages/topic-iceberg-integration.adoc | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 1bdfa08a0a..f6d4f072b8 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -27,7 +27,8 @@ rpk cluster license info == Limitations -* It is not possible to append or backfill data from Redpanda topics to an existing Iceberg table. +* It is not possible to append data from Redpanda topics to an existing Iceberg table. +* If you enable the Iceberg integration on an existing Redpanda topic, Redpanda does not backfill the generated Iceberg table with topic data. * JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128 KB. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. See <> for more details. * xref:manage:remote-read-replicas.adoc[Remote read replicas] and xref:manage:topic-recovery.adoc[topic recovery] are not supported for Iceberg-enabled topics. @@ -87,9 +88,9 @@ new-topic-name OK . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: + -- -* `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. We also refer to this as the "schemaless" mode. -* `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. -* `DISABLED` (default): Disables writing to an Iceberg table for this topic. +* `key_value`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. We also refer to this as the "schemaless" mode. +* `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +* `disabled` (default): Disables writing to an Iceberg table for this topic. -- + [,bash] @@ -103,7 +104,7 @@ TOPIC STATUS new-topic-name OK ---- -. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `VALUE_SCHEMA_ID_PREFIX`. +. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `value_schema_id_prefix`. + [,bash] ---- @@ -116,19 +117,19 @@ SUBJECT VERSION ID TYPE new-topic-name-value 1 1 PROTOBUF ---- + -If you don't use a schema for the topic, select `KEY_VALUE` for the topic Iceberg mode. Redpanda uses a simple schema for the Iceberg table, consisting of a column that stores the record’s metadata, key, and value. +If you don't use a schema for the topic, select `key_value` for the topic Iceberg mode. Redpanda uses a simple schema for the Iceberg table, consisting of a column that stores the record’s metadata, key, and value. -As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. +The Iceberg table name is the same as the Redpanda topic name. As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. == Schema support and mapping -The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the schemaless mode where Redpanda stores the record values as-is in the table. The JSON Schema format is not supported in this beta release. If your topic data is in JSON, it is recommended to use the `KEY_VALUE` (schemaless) mode. +The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the schemaless mode where Redpanda stores the record values as-is in the table. The JSON Schema format is not supported in this beta release. If your topic data is in JSON, it is recommended to use the `key_value` (schemaless) mode. === Iceberg table modes -For both `KEY_VALUE` and `VALUE_SCHEMA_ID_PREFIX` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record timestamp, the partition it belongs to, and its offset. +For both `key_value` and `value_schema_id_prefix` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record timestamp, the partition it belongs to, and its offset. -In the `KEY_VALUE` ("schemaless") mode, the `redpanda` metadata structs also contain both the record key and value. If you are associating a schema with the topic using the `VALUE_SCHEMA_ID_PREFIX` mode, the `redpanda` structs contain the record key only. Redpanda uses the matching schema to define table columns based on schema fields and then maps the record values to the corresponding columns. +In the `key_value` ("schemaless") mode, the `redpanda` metadata structs also contain both the record key and value. If you are associating a schema with the topic using the `value_schema_id_prefix` mode, the `redpanda` structs contain the record key only. Redpanda uses the matching schema to define table columns based on schema fields and then maps the record values to the corresponding columns. For example, if you produce to a topic according to the following Avro schema: @@ -154,7 +155,7 @@ For example, if you produce to a topic according to the following Avro schema: ``` -The `KEY_VALUE` mode writes to the following table format: +The `key_value` mode writes to the following table format: ``` CREATE TABLE ClickEvent ( @@ -171,7 +172,7 @@ CREATE TABLE ClickEvent ( Consider this schemaless approach if the topic data is in JSON, or if you can use the Iceberg data in its semi-structured format. -The `VALUE_SCHEMA_ID_PREFIX` translates to the following table format: +The `value_schema_id_prefix` mode translates to the following table format: ``` CREATE TABLE ClickEvent ( @@ -257,8 +258,8 @@ You can configure the Iceberg integration to either create a file in the same ob Set the cluster configuration property `iceberg_catalog_type` with one of the following values: -* `OBJECT_STORAGE`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. -* `REST`: Connect to and update an Iceberg catalog hosted on a REST server. +* `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. +* `rest`: Connect to and update an Iceberg catalog hosted on a REST server. For an Iceberg REST catalog, set the following additional cluster configuration properties: @@ -270,6 +271,32 @@ For an Iceberg REST catalog, set the following additional cluster configuration // update xref when PR for extracted properties is ready See xref:reference:properties/cluster-properties.adoc[Cluster Configuration Properties] for the full list of cluster properties to configure for a catalog integration. +=== Example catalog configuration + +You'll be able to use to the catalog to load, query, or refresh the Iceberg data as you produce to the Redpanda topic. Refer to the official documentation of your query engine or Iceberg-compatible tool for guidance on integrating the REST-based catalog. + +For example, if you have Redpanda cluster configuration properties set to connect to a REST catalog named `demo`: + +iceberg_rest_catalog_type: rest +iceberg_rest_catalog_endpoint: http://catalog-service:8181 +iceberg_rest_catalog_client_id: +iceberg_rest_catalog_client_secret: + +And you have a Spark configured to use the `demo` catalog: + +``` +spark.sql.catalog.demo = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.demo.type = rest +spark.sql.catalog.demo.uri = http://catalog-service:8181 +spark.sql.catalog.demo.warehouse = s3://redpanda/ +``` + +Using Spark SQL, you can query the Iceberg table directly by specifying the catalog name: + +``` +SELECT * FROM demo.redpanda.ClickEvent; +``` + == Access data in Iceberg tables You can use the same analytical tools to access table data in a data lake as you would for a relational database. @@ -306,7 +333,7 @@ Register the following schema for `ClickEvent` under the `ClickEvent-value` subj rpk registry schema create ClickEvent-value --schema path/to/schema.avsc --type avro ---- -Query engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, the `ClickEvent` table contains columns based on the schema fields: +Query engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, the query returns values from columns in the `ClickEvent` table, with the column names matching the schema fields: [,sql] ---- @@ -329,7 +356,7 @@ FROM ClickEvent; You can also forgo using a schema, which means using semi-structured data in Iceberg. -This example reads the semi-structured data in the `ClickEvent_schemaless` table, which consists of a column `redpanda` containing the record key, value, and metadata: +This example queries the semi-structured data in the `ClickEvent_schemaless` table, which consists of a column `redpanda` containing the record key, value, and metadata: [,sql] ---- From 8826f7cb07e2251c1fc2319f0d2d3f7286f0f066 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:44:12 -0500 Subject: [PATCH 27/45] lowercase config values --- modules/reference/pages/properties/topic-properties.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/reference/pages/properties/topic-properties.adoc b/modules/reference/pages/properties/topic-properties.adoc index c50fc9a7cc..dadf9abd59 100644 --- a/modules/reference/pages/properties/topic-properties.adoc +++ b/modules/reference/pages/properties/topic-properties.adoc @@ -512,13 +512,13 @@ Integrate Redpanda topics as Iceberg tables. Enable the Iceberg integration for the topic. You can choose one of three modes. -**Default**: `DISABLED` +**Default**: `disabled` **Values**: -- `KEY_VALUE`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. -- `VALUE_SCHEMA_ID_PREFIX`: Creates an Iceberg table whose structure matches the Redpanda schema for the topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. -- `DISABLED`: Disables writing to an Iceberg table for the topic. +- `key_value`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. +- `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for the topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +- `disabled`: Disables writing to an Iceberg table for the topic. **Related topics**: From d5b7896d459d764903bb7fa9f01b237e3c0b1e50 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:55:35 -0500 Subject: [PATCH 28/45] code block --- modules/manage/pages/topic-iceberg-integration.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index f6d4f072b8..3239abf8cb 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -277,10 +277,12 @@ You'll be able to use to the catalog to load, query, or refresh the Iceberg data For example, if you have Redpanda cluster configuration properties set to connect to a REST catalog named `demo`: +``` iceberg_rest_catalog_type: rest iceberg_rest_catalog_endpoint: http://catalog-service:8181 iceberg_rest_catalog_client_id: iceberg_rest_catalog_client_secret: +``` And you have a Spark configured to use the `demo` catalog: From d19a441a61967da78272875443c3fc399385d2a9 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:02:02 -0500 Subject: [PATCH 29/45] Specify REST example section --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 3239abf8cb..9994b3becd 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -271,7 +271,7 @@ For an Iceberg REST catalog, set the following additional cluster configuration // update xref when PR for extracted properties is ready See xref:reference:properties/cluster-properties.adoc[Cluster Configuration Properties] for the full list of cluster properties to configure for a catalog integration. -=== Example catalog configuration +=== Example REST catalog configuration You'll be able to use to the catalog to load, query, or refresh the Iceberg data as you produce to the Redpanda topic. Refer to the official documentation of your query engine or Iceberg-compatible tool for guidance on integrating the REST-based catalog. From 524e98fe0f2e6d4bb58e2996f0220ef29becdd35 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:48:57 -0500 Subject: [PATCH 30/45] Edits per SME reviews --- .../pages/topic-iceberg-integration.adoc | 150 ++++++++++-------- 1 file changed, 83 insertions(+), 67 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 9994b3becd..eba84004d0 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -29,11 +29,12 @@ rpk cluster license info * It is not possible to append data from Redpanda topics to an existing Iceberg table. * If you enable the Iceberg integration on an existing Redpanda topic, Redpanda does not backfill the generated Iceberg table with topic data. -* JSON schemas are not currently supported. For Avro schemas, records cannot contain fields greater than 128 KB. +* JSON schemas are not currently supported. If the topic data is in JSON, use the `key_value` mode to store the JSON in Iceberg, which then can be parsed by most query engines. +* If using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. See <> for more details. * xref:manage:remote-read-replicas.adoc[Remote read replicas] and xref:manage:topic-recovery.adoc[topic recovery] are not supported for Iceberg-enabled topics. -== Architecture +== Iceberg concepts Apache Iceberg is an open source format specification for defining structured tables in a data lake. The table format lets you quickly and easily manage, query, and process huge amounts of structured and unstructured data. This is similar to the way in which you would manage and run SQL queries against relational data in a database or data warehouse. The open format lets you use many different languages, tools, and applications to process the same data in a consistent way, so you can avoid vendor lock-in. This data management system is referred to as a _data lakehouse_. @@ -41,7 +42,7 @@ In the Iceberg specification, tables consist of the following layers: * Data layer ** Data files: Store the data. The Iceberg integration currently supports the Parquet file format. Parquet files are column-based and suitable for analytical workloads at scale. They come with compression capabilities that optimize files for object storage. -* Metadata layer +* Metadata layer: Stores table metadata separately from data files. The metadata layer allows multiple writers to stage metadata changes and apply updates atomically. It also supports database snapshots, and time travel queries that query the database at a previous point in time. + -- ** Manifest files: Track data files and contain metadata about these files, such as record count, partition membership, and file paths. @@ -54,7 +55,7 @@ In the Redpanda Iceberg integration, the manifest files are in JSON format. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage. The brokers also write events to segment files stored on disk. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible https://iceberg.apache.org/vendors/[data lakehouse, data engineering, and business intelligence tools]. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to the log segments uploaded via Tiered Storage. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible https://iceberg.apache.org/vendors/[data lakehouse, data engineering, and business intelligence tools]. == Enable Iceberg integration @@ -88,8 +89,8 @@ new-topic-name OK . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: + -- -* `key_value`: Creates an Iceberg table with a `Key` column and a `Value` column. Redpanda stores the raw topic content in the `Value` column. We also refer to this as the "schemaless" mode. -* `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Redpanda parses the topic values per field and stores them in the corresponding table columns. +* `key_value`: Creates an Iceberg table using a simple schema, consisting two columns, one for the record metadata including the key, and another binary column for the record's value. +* `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Requires that you register a schema in the Schema Registry (see next step), and that producers write to the topic using the Schema Registry wire format. Redpanda parses the schema used by the record based on the schema ID encoded in the payload header, and stores the topic values in the corresponding table columns. * `disabled` (default): Disables writing to an Iceberg table for this topic. -- + @@ -104,7 +105,7 @@ TOPIC STATUS new-topic-name OK ---- -. Register a schema for the topic (optional). You must also set the `redpanda.iceberg.mode` topic property to `value_schema_id_prefix`. +. Register a schema for the topic. This step is required for the `value_schema_id_prefix` mode, but is optional otherwise. + [,bash] ---- @@ -116,24 +117,23 @@ rpk registry schema create --schema --type +iceberg_rest_catalog_client_id: iceberg_rest_catalog_client_secret: -``` +---- -And you have a Spark configured to use the `demo` catalog: +And you use Spark as a processing engine, configured to use the `streaming` catalog: ``` -spark.sql.catalog.demo = org.apache.iceberg.spark.SparkCatalog -spark.sql.catalog.demo.type = rest -spark.sql.catalog.demo.uri = http://catalog-service:8181 -spark.sql.catalog.demo.warehouse = s3://redpanda/ +spark.sql.catalog.streaming = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.streaming.type = rest +spark.sql.catalog.streaming.uri = http://catalog-service:8181 +spark.sql.catalog.streaming.warehouse = s3://redpanda/ ``` Using Spark SQL, you can query the Iceberg table directly by specifying the catalog name: -``` -SELECT * FROM demo.redpanda.ClickEvent; -``` +[,sql] +---- +SELECT * FROM streaming.redpanda.ClickEvent; +---- + +Using a REST catalog enables Spark to automatically discover the topic's Iceberg table. If using the object storage catalog type, you may have to explicitly create a new table and load the Iceberg data in your processing engine. == Access data in Iceberg tables @@ -310,11 +321,11 @@ For example, suppose you produce the same stream of events to a topic `ClickEven {"user_id": 2324, "event_type": "BUTTON_CLICK", "ts": "2024-11-25T20:23:59.380Z"} ---- -When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on whether you've registered a schema on which to derive the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. +When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on the topic Iceberg mode and whether you've registered a schema on which to derive the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. -=== Structured approach with schema +=== Query topic with schema (`value_schema_id_prefix` mode) -Register the following schema for `ClickEvent` under the `ClickEvent-value` subject: +The following is an example Avro schema for `ClickEvent`: [,avro] ---- @@ -330,18 +341,20 @@ Register the following schema for `ClickEvent` under the `ClickEvent-value` subj } ---- +With `redpanda.iceberg.mode` set to `value_schema_id_prefix`, you can register the schema under the `ClickEvent-value` subject: + [,bash] ---- rpk registry schema create ClickEvent-value --schema path/to/schema.avsc --type avro ---- -Query engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, the query returns values from columns in the `ClickEvent` table, with the column names matching the schema fields: +Processing engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, this Spark SQL query returns values from columns in the `ClickEvent` table, with the column names matching the schema fields: [,sql] ---- SELECT user_id, - event_type, - ts + event_type, + ts FROM ClickEvent; ---- @@ -354,26 +367,29 @@ FROM ClickEvent; +---------+--------------+--------------------------+ ---- -=== Schemaless approach +=== Query topic in key-value mode You can also forgo using a schema, which means using semi-structured data in Iceberg. -This example queries the semi-structured data in the `ClickEvent_schemaless` table, which consists of a column `redpanda` containing the record key, value, and metadata: +This example queries the semi-structured data in the `ClickEvent_schemaless` table, which also consists of another column `redpanda` containing the record key and other metadata: [,sql] ---- SELECT - redpanda.data + value FROM ClickEvent_schemaless; ---- [,bash,role=no-copy] ---- +------------------------------------------------------------------------------+ -| redpanda.data | +| redpanda.value | +------------------------------------------------------------------------------+ | {"user_id":2324,"event_type":"BUTTON_CLICK","ts":"2024-11-25T20:23:59.380Z"} | +------------------------------------------------------------------------------+ ---- == Suggested reading + +* xref:manage:schema-reg/schema-id-validation.adoc[] +* https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] From 51b8756be6428d0116b2c9e3cb492fadec294eca Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:08:13 -0500 Subject: [PATCH 31/45] Clean up types translation table --- .../pages/topic-iceberg-integration.adoc | 97 +++++++++++-------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index eba84004d0..00addba6e9 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -1,4 +1,4 @@ -= Topics as Iceberg Tables += Iceberg Topics :description: Learn how to integrate Redpanda topics with Apache Iceberg. :page-context-links: [{"name": "Linux", "to": "manage:topic-iceberg-integration.adoc" } ] :page-categories: Management, High Availability, Data Replication, Integration @@ -11,7 +11,7 @@ The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. Wh == Prerequisites -* Install `rpk`. +* Install xref:get-started:rpk-install.adoc[`rpk`]. * {empty} include::shared:partial$enterprise-license.adoc[] @@ -128,7 +128,7 @@ The JSON Schema format is not supported in this beta release. If your topic data === Iceberg modes and table schemas -For both `key_value` and `value_schema_id_prefix` modes, Redpanda writes to a `redpanda` table column that stores a single struct per record, containing nested columns of the metadata from each record, including the record key, headers, timestamp, the partition it belongs to, and its offset. +For both `key_value` and `value_schema_id_prefix` modes, Redpanda writes to a `redpanda` table column that stores a single Iceberg struct per record, containing nested columns of the metadata from each record, including the record key, headers, timestamp, the partition it belongs to, and its offset. For example, if you produce to a topic according to the following Avro schema: @@ -194,60 +194,71 @@ With schema integration, Redpanda uses the schema ID prefix embedded in each rec === Schema types translation +Redpanda supports direct translations of the following types to Iceberg value domains: + [tabs] ====== Avro:: + -- -* Redpanda supports direct translations of the following Avro types to Iceberg value domains: -** boolean -** int -** long -** float -** double -** bytes -** string -** record -** array -** maps -** fixed -** decimal -** uuid -** date -** time -** timestamp -* Different flavors of timestamp and time types are all represented by the same Iceberg type, conversion will be done. -* Avro unions are flattened to structs with optional fields, out of which only one is present, or none in case there is a field with null type: +|=== +| Avro type | Iceberg type + +| boolean | boolean +| int | int +| long | long +| float | float +| double | double +| bytes | binary +| string | string +| record | struct +| array | list +| maps | list +| fixed | +| decimal | decimal +| uuid | uuid +| date | date +| time | time +| timestamp | timestamp +|=== + +* Different flavors of time (such as time-millis) and timestamp (such as timestamp-millis) types are translated to the same Iceberg `time` and `timestamp` types respectively. +* Avro unions are flattened to Iceberg structs with optional fields: ** For example, the union `["int", "long", "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 LONG NULLABLE, 2 FLOAT NULLABLE)`. ** The union `["int", null, "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 FLOAT NULLABLE)`. * All fields are required by default (Avro always sets a default in binary representation). -* Duration logical type is ignored. -* Null type is ignored and not represented in Iceberg schema. +* The Avro duration logical type is ignored. +* The Avro null type is ignored and not represented in the Iceberg schema. * Recursive types are not supported. -- Protobuf:: + -- -* Redpanda supports direct translations of the following Avro types to Iceberg value domains: -** BOOL -** DOUBLE -** FLOAT -** INT32 -** SINT32 -** INT64 -** SINT64 -** SFIXED32 -** SFIXED64 -** STRING -** BYTES -** MAP -* Repeated values are translated into Iceberg ARRAY types. -* Enums are translated into Iceberg INT types based on the integer value of the enumerated type. -* UINT32 and FIXED32 are translated into Iceberg LONG types as that is the existing semantic for unsigned 32bit values in Iceberg. -* UINT64 and FIXED64 values are translated into their base 10 string representation. -* Timestamp well known protos are translated into TIMESTAMP Iceberg types. -* Messages are converted into STRUCT types. + +|=== +| Protobuf type | Iceberg type + +| bool | boolean +| double | double +| float | float +| int32 | int +| sint32 | int +| int64 | long +| sint64 | long +| sfixed32 | int +| sfixed64 | int +| string | string +| bytes | binary +| map | list +|=== + +* Repeated values are translated into Iceberg `array` types. +* Enums are translated into Iceberg `int` types based on the integer value of the enumerated type. +* uint32 and fixed32 are translated into Iceberg `long` types as that is the existing semantic for unsigned 32-bit values in Iceberg. +* uint64 and fixed64 values are translated into their Base-10 string representation. +* The timestamp well-known type in Protobuf is translated into `timestamp` in Iceberg. +* Messages are converted into Iceberg structs. * Recursive types are not supported. -- ====== From c478d1b7d7f1e9df9884bbd0bcc41e1dc5b50862 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:08:01 -0500 Subject: [PATCH 32/45] Apply suggestions from review --- .../pages/topic-iceberg-integration.adoc | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 00addba6e9..7c50f6f508 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -55,7 +55,7 @@ In the Redpanda Iceberg integration, the manifest files are in JSON format. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to the log segments uploaded via Tiered Storage. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible https://iceberg.apache.org/vendors/[data lakehouse, data engineering, and business intelligence tools]. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to the log segments uploaded via Tiered Storage. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible data lakehouse, data engineering, and business intelligence https://iceberg.apache.org/vendors/[tools^]. == Enable Iceberg integration @@ -122,7 +122,7 @@ The Iceberg table is inside a namespace called `redpanda`, and has the same name == Schema support and mapping -The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the schemaless mode where Redpanda stores the record values as-is in the table. +The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the `key_value` mode where Redpanda stores the record values as-is in the table. The JSON Schema format is not supported in this beta release. If your topic data is in JSON, it is recommended to use the `key_value` mode. @@ -170,7 +170,7 @@ CREATE TABLE ClickEvent ( ) ---- -Consider this schemaless approach if the topic data is in JSON, or if you can use the Iceberg data in its semi-structured format. +Consider this approach if the topic data is in JSON, or if you can use the Iceberg data in its semi-structured format. The `value_schema_id_prefix` mode translates to the following table format: @@ -214,7 +214,7 @@ Avro:: | record | struct | array | list | maps | list -| fixed | +| fixed | fixed | decimal | decimal | uuid | uuid | date | date @@ -224,8 +224,8 @@ Avro:: * Different flavors of time (such as time-millis) and timestamp (such as timestamp-millis) types are translated to the same Iceberg `time` and `timestamp` types respectively. * Avro unions are flattened to Iceberg structs with optional fields: -** For example, the union `["int", "long", "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 LONG NULLABLE, 2 FLOAT NULLABLE)`. -** The union `["int", null, "float"]` is represented as an Iceberg struct `OBJECT(0 INT NULLABLE, 1 FLOAT NULLABLE)`. +** For example, the union `["int", "long", "float"]` is represented as an Iceberg struct `struct<0 INT NULLABLE, 1 LONG NULLABLE, 2 FLOAT NULLABLE>`. +** The union `["int", null, "float"]` is represented as an Iceberg struct `struct<0 INT NULLABLE, 1 FLOAT NULLABLE>`. * All fields are required by default (Avro always sets a default in binary representation). * The Avro duration logical type is ignored. * The Avro null type is ignored and not represented in the Iceberg schema. @@ -235,7 +235,6 @@ Avro:: Protobuf:: + -- - |=== | Protobuf type | Iceberg type @@ -250,7 +249,7 @@ Protobuf:: | sfixed64 | int | string | string | bytes | binary -| map | list +| map | map |=== * Repeated values are translated into Iceberg `array` types. @@ -272,7 +271,7 @@ Set the cluster configuration property `iceberg_catalog_type` with one of the fo * `rest`: Connect to and update an Iceberg catalog via REST API. * `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. + -This type is not recommended for production use cases. +This option is not recommended for production use cases. For an Iceberg REST catalog, set the following additional cluster configuration properties: @@ -319,25 +318,28 @@ Using Spark SQL, you can query the Iceberg table directly by specifying the cata SELECT * FROM streaming.redpanda.ClickEvent; ---- -Using a REST catalog enables Spark to automatically discover the topic's Iceberg table. If using the object storage catalog type, you may have to explicitly create a new table and load the Iceberg data in your processing engine. +Using a REST catalog enables Spark to automatically discover the topic's Iceberg table. If using the object storage catalog type, you may have to explicitly create a new table and load the Iceberg data, depending on the processing engine you're using. == Access data in Iceberg tables You can use the same analytical tools to access table data in a data lake as you would for a relational database. -For example, suppose you produce the same stream of events to a topic `ClickEvent`, and another topic `ClickEvent_schemaless`. The topics have Tiered Storage configured to an AWS S3 bucket. A sample record contains the following data: +For example, suppose you produce the same stream of events to a topic `ClickEvent`, which uses a schema, and another topic `ClickEvent_key_value`, which uses the key-value mode. The topics have Tiered Storage configured to an AWS S3 bucket. A sample record contains the following data: [,bash,role=no-copy] ---- {"user_id": 2324, "event_type": "BUTTON_CLICK", "ts": "2024-11-25T20:23:59.380Z"} ---- -When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on the topic Iceberg mode and whether you've registered a schema on which to derive the table format. In either approach, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. +When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on the topic Iceberg mode and whether you've registered a schema on which to derive the table format. Depending on the processing engine, you may need to first create a new table that gets added to your Iceberg catalog implementation. + +In either mode, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. === Query topic with schema (`value_schema_id_prefix` mode) -The following is an example Avro schema for `ClickEvent`: +In this example, it is assumed you have created the `ClickEvent` topic and set `redpanda.iceberg.mode` to `value_schema_id_prefix`. The following is an Avro schema for `ClickEvent`: +.`schema.avsc` [,avro] ---- { @@ -352,21 +354,28 @@ The following is an example Avro schema for `ClickEvent`: } ---- -With `redpanda.iceberg.mode` set to `value_schema_id_prefix`, you can register the schema under the `ClickEvent-value` subject: +You can register the schema under the `ClickEvent-value` subject: [,bash] ---- rpk registry schema create ClickEvent-value --schema path/to/schema.avsc --type avro ---- -Processing engines such as Spark SQL provide Iceberg integrations to allow easy access to existing Iceberg tables in object storage. The table structure is derived from the schema. In this example, this Spark SQL query returns values from columns in the `ClickEvent` table, with the column names matching the schema fields: +If you produce to the `ClickEvent` topic in the following format: + +[,bash] +---- +echo '"key1" {"user_id":2324,"event_type":"BUTTON_CLICK","ts":"2024-11-25T20:23:59.380Z"}' | rpk topic produce ClickEvent --format='%k %v\n' --schema-id=topic +---- + +The following SQL query returns values from columns in the `ClickEvent` table, with the table structure derived from the schema, and column names matching the schema fields. If you've integrated a catalog, query engines such as Spark SQL provide Iceberg integrations that allow easy discovery and access to existing Iceberg tables in object storage. [,sql] ---- SELECT user_id, event_type, ts -FROM ClickEvent; +FROM .ClickEvent; ---- [,bash,role=no-copy] @@ -382,19 +391,26 @@ FROM ClickEvent; You can also forgo using a schema, which means using semi-structured data in Iceberg. -This example queries the semi-structured data in the `ClickEvent_schemaless` table, which also consists of another column `redpanda` containing the record key and other metadata: +If you produce to the `ClickEvent_key_value` topic in the following format: + +[,bash] +---- +echo 'key1 {"user_id":2324,"event_type":"BUTTON_CLICK","ts":"2024-11-25T20:23:59.380Z"}' | rpk topic produce ClickEvent_key_value --format='%k %v\n' +---- + +This example queries the semi-structured data in the `ClickEvent_key_value` table, which also consists of another column `redpanda` containing the record key and other metadata: [,sql] ---- SELECT value -FROM ClickEvent_schemaless; +FROM .ClickEvent_key_value; ---- [,bash,role=no-copy] ---- +------------------------------------------------------------------------------+ -| redpanda.value | +| value | +------------------------------------------------------------------------------+ | {"user_id":2324,"event_type":"BUTTON_CLICK","ts":"2024-11-25T20:23:59.380Z"} | +------------------------------------------------------------------------------+ From 5b7572a848c53104730499bc9fdf311760563144 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:22:50 -0500 Subject: [PATCH 33/45] Flesh out catalog section more --- .../pages/topic-iceberg-integration.adoc | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 7c50f6f508..9138bd5e2c 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -51,7 +51,7 @@ In the Iceberg specification, tables consist of the following layers: -- + In the Redpanda Iceberg integration, the manifest files are in JSON format. -* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. The Iceberg integration supports two <> types. You can configure Redpanda to catalog files stored in the same object storage bucket or container where the Iceberg data files are located, or you can configure Redpanda to use an https://iceberg.apache.org/concepts/catalog/#catalog-implementations[Iceberg REST catalog^] endpoint to update an externally-managed catalog when there are changes to the Iceberg data and metadata. +* Catalog: Contains the current metadata pointer for the table. Clients reading and writing data to the table see the same version of the current state of the table. The Iceberg integration supports two <> types. You can configure Redpanda to catalog files stored in the same object storage bucket or container where the Iceberg data files are located, or you can configure Redpanda to use an https://iceberg.apache.org/concepts/catalog/#decoupling-using-the-rest-catalog[Iceberg REST catalog^] endpoint to update an externally-managed catalog when there are changes to the Iceberg data and metadata. image::shared:iceberg-integration.png[] @@ -268,10 +268,10 @@ You can configure the Iceberg integration to either create a file in the same ob Set the cluster configuration property `iceberg_catalog_type` with one of the following values: -* `rest`: Connect to and update an Iceberg catalog via REST API. +* `rest`: Connect to and update an Iceberg catalog via REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. * `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. + -This option is not recommended for production use cases. +This option is not recommended for production use cases. Many catalog services such as https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^] provide Iceberg REST endpoints to simplify your data lakehouse management. For an Iceberg REST catalog, set the following additional cluster configuration properties: @@ -281,6 +281,7 @@ For an Iceberg REST catalog, set the following additional cluster configuration + -- For REST catalogs that use self-signed certificates, also configure these properties: + * `iceberg_rest_catalog_trust_file`: The path to a file containing a certificate chain to trust for the REST catalog. * `iceberg_rest_catalog_crl_file`: The path to the certificate revocation list for the specified trust file. -- @@ -288,9 +289,11 @@ For REST catalogs that use self-signed certificates, also configure these proper // update xref when PR for extracted properties is ready See xref:reference:properties/cluster-properties.adoc[Cluster Configuration Properties] for the full list of cluster properties to configure for a catalog integration. -=== Example REST catalog integration +=== Example catalog integration + +You'll be able to use to the catalog to load, query, or refresh the Iceberg data as you produce to the Redpanda topic. Refer to the official documentation of your query engine or Iceberg-compatible tool for guidance on integrating your Iceberg catalog. -You'll be able to use to the catalog to load, query, or refresh the Iceberg data as you produce to the Redpanda topic. Refer to the official documentation of your query engine or Iceberg-compatible tool for guidance on integrating the REST-based catalog. +==== REST catalog For example, if you have Redpanda cluster configuration properties set to connect to a REST catalog named `streaming`: @@ -308,7 +311,6 @@ And you use Spark as a processing engine, configured to use the `streaming` cata spark.sql.catalog.streaming = org.apache.iceberg.spark.SparkCatalog spark.sql.catalog.streaming.type = rest spark.sql.catalog.streaming.uri = http://catalog-service:8181 -spark.sql.catalog.streaming.warehouse = s3://redpanda/ ``` Using Spark SQL, you can query the Iceberg table directly by specifying the catalog name: @@ -318,7 +320,18 @@ Using Spark SQL, you can query the Iceberg table directly by specifying the cata SELECT * FROM streaming.redpanda.ClickEvent; ---- -Using a REST catalog enables Spark to automatically discover the topic's Iceberg table. If using the object storage catalog type, you may have to explicitly create a new table and load the Iceberg data, depending on the processing engine you're using. +Spark can use the REST catalog to automatically discover the topic's Iceberg table. + +==== Filesystem-based catalog (`object_storage`) + +If using the `object_storage` catalog type, you must set up the catalog integration in your processing engine accordingly. For example, you can configure Spark to use a filesystem-based catalog with at least the following properties: + +``` +spark.sql.catalog.streaming.type = hadoop +spark.sql.catalog.hadoop_prod.warehouse = s3a:///path/to/redpanda-iceberg-table +``` + +Depending on your processing engine, you may also need to create a new table for the Iceberg data. == Access data in Iceberg tables @@ -333,7 +346,7 @@ For example, suppose you produce the same stream of events to a topic `ClickEven When you point your Iceberg-compatible tool or framework to the object storage location of the Iceberg tables, how you consume the data depends on the topic Iceberg mode and whether you've registered a schema on which to derive the table format. Depending on the processing engine, you may need to first create a new table that gets added to your Iceberg catalog implementation. -In either mode, you do not need to rely on complex ETL jobs or pipelines to consume real-time data from Redpanda. +In either mode, you do not need to rely on complex ETL jobs or pipelines to access real-time data from Redpanda in your data lakehouse. === Query topic with schema (`value_schema_id_prefix` mode) From 2ad812b85e6c09d8cc34e5553904c3ac78158a35 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:27:01 -0500 Subject: [PATCH 34/45] Match nav with page title --- modules/ROOT/nav.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index bd9b956a3d..67463c3ab8 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -162,7 +162,7 @@ *** xref:manage:security/iam-roles.adoc[] ** xref:manage:tiered-storage-linux/index.adoc[Tiered Storage] *** xref:manage:tiered-storage.adoc[] -*** xref:manage:topic-iceberg-integration.adoc[Apache Iceberg Integration] +*** xref:manage:topic-iceberg-integration.adoc[Iceberg topics] *** xref:manage:fast-commission-decommission.adoc[] *** xref:manage:mountable-topics.adoc[] *** xref:manage:remote-read-replicas.adoc[Remote Read Replicas] From 889ba9e80ddd35a23bfa460a9dad9ba380777493 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:28:54 -0500 Subject: [PATCH 35/45] Missed an edit per SME review --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 9138bd5e2c..3b9c5b1f3a 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -269,7 +269,7 @@ You can configure the Iceberg integration to either create a file in the same ob Set the cluster configuration property `iceberg_catalog_type` with one of the following values: * `rest`: Connect to and update an Iceberg catalog via REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. -* `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL to access the catalog for your Redpanda Iceberg tables. +* `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL with an Iceberg client to access the catalog and data files for your Redpanda Iceberg tables. + This option is not recommended for production use cases. Many catalog services such as https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^] provide Iceberg REST endpoints to simplify your data lakehouse management. From f3be0bf7b17df25b7105758f9ff9af840a0770a6 Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:29:41 -0500 Subject: [PATCH 36/45] Apply suggestions from code review Co-authored-by: Tyler Rockwood --- .../pages/topic-iceberg-integration.adoc | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 3b9c5b1f3a..aa5560df70 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -158,15 +158,15 @@ The `key_value` mode writes to the following table format: [,sql] ---- -CREATE TABLE ClickEvent ( - redpanda OBJECT( - partition INT, - timestamp TIMESTAMP, - offset LONG, - headers ARRAY(OBJECT(KEY BINARY, VALUE BINARY NULLABLE)), - key BINARY NULLABLE - ), - value BINARY NULLABLE +CREATE TABLE ClickEvent ( + redpanda struct< + partition: integer NOT NULL, + timestamp: timestamp NOT NULL, + offset: long NOT NULL, + headers: array>, + key: binary + >, + value binary ) ---- @@ -177,16 +177,16 @@ The `value_schema_id_prefix` mode translates to the following table format: [,sql] ---- CREATE TABLE ClickEvent ( - redpanda OBJECT( - partition INT, - timestamp TIMESTAMP, - offset LONG, - headers ARRAY(OBJECT(KEY BINARY, VALUE BINARY NULLABLE)), - key BINARY NULLABLE - ), - user_id INT, - event_type STRING NULLABLE, - ts STRING NULLABLE + redpanda struct< + partition: integer NOT NULL, + timestamp: timestamp NOT NULL, + offset: long NOT NULL, + headers: array>, + key: binary + >, + user_id integer NOT NULL, + event_type string, + ts string ) ---- From a7db75ef038eb4c1f1ec3aa5dedcd1600a6a862b Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:26:45 -0500 Subject: [PATCH 37/45] Edits per review --- modules/manage/pages/topic-iceberg-integration.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index aa5560df70..958a7d007b 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -90,7 +90,7 @@ new-topic-name OK + -- * `key_value`: Creates an Iceberg table using a simple schema, consisting two columns, one for the record metadata including the key, and another binary column for the record's value. -* `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. Requires that you register a schema in the Schema Registry (see next step), and that producers write to the topic using the Schema Registry wire format. Redpanda parses the schema used by the record based on the schema ID encoded in the payload header, and stores the topic values in the corresponding table columns. +* `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. You must register a schema in the Schema Registry (see next step), and producers must write to the topic using the Schema Registry wire format. Redpanda parses the schema used by the record based on the schema ID encoded in the payload header, and stores the topic values in the corresponding table columns. * `disabled` (default): Disables writing to an Iceberg table for this topic. -- + @@ -128,7 +128,7 @@ The JSON Schema format is not supported in this beta release. If your topic data === Iceberg modes and table schemas -For both `key_value` and `value_schema_id_prefix` modes, Redpanda writes to a `redpanda` table column that stores a single Iceberg struct per record, containing nested columns of the metadata from each record, including the record key, headers, timestamp, the partition it belongs to, and its offset. +For both `key_value` and `value_schema_id_prefix` modes, Redpanda writes to a `redpanda` table column that stores a single Iceberg https://iceberg.apache.org/spec/#nested-types[struct^] per record, containing nested columns of the metadata from each record, including the record key, headers, timestamp, the partition it belongs to, and its offset. For example, if you produce to a topic according to the following Avro schema: @@ -268,7 +268,7 @@ You can configure the Iceberg integration to either create a file in the same ob Set the cluster configuration property `iceberg_catalog_type` with one of the following values: -* `rest`: Connect to and update an Iceberg catalog via REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. +* `rest`: Connect to and update an Iceberg catalog using a REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. * `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL with an Iceberg client to access the catalog and data files for your Redpanda Iceberg tables. + This option is not recommended for production use cases. Many catalog services such as https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^] provide Iceberg REST endpoints to simplify your data lakehouse management. From 066db4c2194f380cb1c2f5cc014c561c4a2f828f Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:28:46 -0500 Subject: [PATCH 38/45] Apply suggestions from code review Co-authored-by: Angela Simms <102690377+asimms41@users.noreply.github.com> --- .../manage/pages/topic-iceberg-integration.adoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 958a7d007b..a051368f17 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -55,7 +55,7 @@ In the Redpanda Iceberg integration, the manifest files are in JSON format. image::shared:iceberg-integration.png[] -When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to the log segments uploaded via Tiered Storage. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible data lakehouse, data engineering, and business intelligence https://iceberg.apache.org/vendors/[tools^]. +When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers store streaming data in the Iceberg-compatible format in Parquet files in object storage, in addition to the log segments uploaded using Tiered Storage. Storing the streaming data in Iceberg tables in the cloud allows you to derive real-time insights through many compatible data lakehouse, data engineering, and business intelligence https://iceberg.apache.org/vendors/[tools^]. == Enable Iceberg integration @@ -89,7 +89,7 @@ new-topic-name OK . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: + -- -* `key_value`: Creates an Iceberg table using a simple schema, consisting two columns, one for the record metadata including the key, and another binary column for the record's value. +* `key_value`: Creates an Iceberg table using a simple schema, consisting of two columns, one for the record metadata including the key, and another binary column for the record's value. * `value_schema_id_prefix`: Creates an Iceberg table whose structure matches the Redpanda schema for this topic, with columns corresponding to each field. You must register a schema in the Schema Registry (see next step), and producers must write to the topic using the Schema Registry wire format. Redpanda parses the schema used by the record based on the schema ID encoded in the payload header, and stores the topic values in the corresponding table columns. * `disabled` (default): Disables writing to an Iceberg table for this topic. -- @@ -222,7 +222,7 @@ Avro:: | timestamp | timestamp |=== -* Different flavors of time (such as time-millis) and timestamp (such as timestamp-millis) types are translated to the same Iceberg `time` and `timestamp` types respectively. +* Different flavors of time (such as `time-millis`) and timestamp (such as `timestamp-millis`) types are translated to the same Iceberg `time` and `timestamp` types respectively. * Avro unions are flattened to Iceberg structs with optional fields: ** For example, the union `["int", "long", "float"]` is represented as an Iceberg struct `struct<0 INT NULLABLE, 1 LONG NULLABLE, 2 FLOAT NULLABLE>`. ** The union `["int", null, "float"]` is represented as an Iceberg struct `struct<0 INT NULLABLE, 1 FLOAT NULLABLE>`. @@ -322,9 +322,9 @@ SELECT * FROM streaming.redpanda.ClickEvent; Spark can use the REST catalog to automatically discover the topic's Iceberg table. -==== Filesystem-based catalog (`object_storage`) +==== File system-based catalog (`object_storage`) -If using the `object_storage` catalog type, you must set up the catalog integration in your processing engine accordingly. For example, you can configure Spark to use a filesystem-based catalog with at least the following properties: +If you are using the `object_storage` catalog type, you must set up the catalog integration in your processing engine accordingly. For example, you can configure Spark to use a file system-based catalog with at least the following properties: ``` spark.sql.catalog.streaming.type = hadoop @@ -374,7 +374,7 @@ You can register the schema under the `ClickEvent-value` subject: rpk registry schema create ClickEvent-value --schema path/to/schema.avsc --type avro ---- -If you produce to the `ClickEvent` topic in the following format: +You can then produce to the `ClickEvent` topic using the following format: [,bash] ---- @@ -404,7 +404,7 @@ FROM .ClickEvent; You can also forgo using a schema, which means using semi-structured data in Iceberg. -If you produce to the `ClickEvent_key_value` topic in the following format: +You can produce to the `ClickEvent_key_value` topic using the following format: [,bash] ---- From 4a777b5cce6b74878813d4826c3feb602cac3717 Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:55:48 -0500 Subject: [PATCH 39/45] Update modules/manage/pages/topic-iceberg-integration.adoc Co-authored-by: Angela Simms <102690377+asimms41@users.noreply.github.com> --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index a051368f17..111ea9b370 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -30,7 +30,7 @@ rpk cluster license info * It is not possible to append data from Redpanda topics to an existing Iceberg table. * If you enable the Iceberg integration on an existing Redpanda topic, Redpanda does not backfill the generated Iceberg table with topic data. * JSON schemas are not currently supported. If the topic data is in JSON, use the `key_value` mode to store the JSON in Iceberg, which then can be parsed by most query engines. -* If using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post. +* If you are using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. See <> for more details. * xref:manage:remote-read-replicas.adoc[Remote read replicas] and xref:manage:topic-recovery.adoc[topic recovery] are not supported for Iceberg-enabled topics. From 4745b22dc17b068982d378e86cce8395f0a00d26 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:00:49 -0500 Subject: [PATCH 40/45] Edits per PR review --- .../pages/topic-iceberg-integration.adoc | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 111ea9b370..8f8bc20d7e 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -5,7 +5,7 @@ :page-beta: true -The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in the Iceberg open table format. This makes your streaming data immediately available for analytical systems, such as data warehouses like RedShift, Snowflake, and Clickhouse, and big data processing platforms, such as Apache Spark and Flink, without setting up and maintaining additional ETL pipelines. +The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in the Iceberg open table format. This makes your streaming data immediately available in downstream analytical systems, including data warehouses like Snowflake, Databricks, Clickhouse, and Redshift, without setting up and maintaining additional ETL pipelines. You can also integrate your data directly into commonly-used big data processing frameworks, such as Apache Spark and Flink, standardizing and simplifying the consumption of streams as tables in a wide variety of data analytics pipelines. The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. When a cluster or topic has Tiered Storage enabled, Redpanda stores the Iceberg files in the configured Tiered Storage bucket or container. @@ -27,7 +27,7 @@ rpk cluster license info == Limitations -* It is not possible to append data from Redpanda topics to an existing Iceberg table. +* It is not possible to append topic data to an existing Iceberg table that is not created by Redpanda. * If you enable the Iceberg integration on an existing Redpanda topic, Redpanda does not backfill the generated Iceberg table with topic data. * JSON schemas are not currently supported. If the topic data is in JSON, use the `key_value` mode to store the JSON in Iceberg, which then can be parsed by most query engines. * If you are using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post. @@ -61,7 +61,7 @@ When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers s To create an Iceberg table for a Redpanda topic, you must set the cluster configuration property `iceberg_enabled` to `true`, and also configure the topic property `redpanda.iceberg.mode`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. -. Set the `iceberg_enabled` configuration option on your cluster to `true`. +. Set the `iceberg_enabled` configuration option on your cluster to `true`. You must restart your cluster if you change this configuration for a running cluster. + [,bash] ---- @@ -264,14 +264,16 @@ Protobuf:: == Set up catalog integration -You can configure the Iceberg integration to either create a file in the same object storage bucket or container to serve as the catalog, or connect to a REST-based catalog. +You can configure the Iceberg integration to either store the metadata in https://iceberg.apache.org/javadoc/1.5.0/org/apache/iceberg/hadoop/HadoopCatalog.html[HadoopCatalog^] format in the same object storage bucket or container, or connect to a REST-based catalog. Set the cluster configuration property `iceberg_catalog_type` with one of the following values: * `rest`: Connect to and update an Iceberg catalog using a REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. * `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL with an Iceberg client to access the catalog and data files for your Redpanda Iceberg tables. -+ -This option is not recommended for production use cases. Many catalog services such as https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^] provide Iceberg REST endpoints to simplify your data lakehouse management. + +Switching catalog types is not supported. + +For production use cases, Redpanda recommends the `rest` option with REST-enabled Iceberg catalog services such as https://docs.tabular.io/[Tabular^], https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^]. For an Iceberg REST catalog, set the following additional cluster configuration properties: @@ -328,10 +330,10 @@ If you are using the `object_storage` catalog type, you must set up the catalog ``` spark.sql.catalog.streaming.type = hadoop -spark.sql.catalog.hadoop_prod.warehouse = s3a:///path/to/redpanda-iceberg-table +spark.sql.catalog.streaming.warehouse = s3a:///path/to/redpanda-iceberg-table ``` -Depending on your processing engine, you may also need to create a new table for the Iceberg data. +Depending on your processing engine, you may also need to create a new table in your data warehouse or lakehouse for the Iceberg data. == Access data in Iceberg tables @@ -350,7 +352,7 @@ In either mode, you do not need to rely on complex ETL jobs or pipelines to acce === Query topic with schema (`value_schema_id_prefix` mode) -In this example, it is assumed you have created the `ClickEvent` topic and set `redpanda.iceberg.mode` to `value_schema_id_prefix`. The following is an Avro schema for `ClickEvent`: +In this example, it is assumed you have created the `ClickEvent` topic, set `redpanda.iceberg.mode` to `value_schema_id_prefix`, and are connecting to a REST-based Iceberg catalog. The following is an Avro schema for `ClickEvent`: .`schema.avsc` [,avro] @@ -404,6 +406,8 @@ FROM .ClickEvent; You can also forgo using a schema, which means using semi-structured data in Iceberg. +In this example, it is assumed you have created the `ClickEvent_key_value` topic, set `redpanda.iceberg.mode` to `key_value`, and are also connecting to a REST-based Iceberg catalog. + You can produce to the `ClickEvent_key_value` topic using the following format: [,bash] From 1710e310ce2bca6b81bcacace50fb24b2866b0a7 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:12:47 -0500 Subject: [PATCH 41/45] Add Iceberg to What's new --- modules/get-started/pages/whats-new.adoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/get-started/pages/whats-new.adoc b/modules/get-started/pages/whats-new.adoc index bf0fc55e83..2ffdc4175c 100644 --- a/modules/get-started/pages/whats-new.adoc +++ b/modules/get-started/pages/whats-new.adoc @@ -7,6 +7,10 @@ This topic includes new content added in version {page-component-version}. For a * xref:redpanda-cloud:get-started:whats-new-cloud.adoc[] * xref:redpanda-cloud:get-started:cloud-overview.adoc#redpanda-cloud-vs-self-managed-feature-compatibility[Redpanda Cloud vs Self-Managed feature compatibility] +== Iceberg topics + +The xref:manage:topic-iceberg-integration.adoc[Iceberg integration for Redpanda] allows you to store topic data in the cloud in the Iceberg open table format. This makes your streaming data immediately available in downstream analytical systems without setting up and maintaining additional ETL pipelines. You can also integrate your data directly into commonly-used big data processing frameworks, standardizing and simplifying the consumption of streams as tables in a wide variety of data analytics pipelines. + == Leader pinning For a Redpanda cluster deployed across multiple availability zones (AZs), xref:develop:produce-data/leader-pinning.adoc[leader pinning] ensures that a topic's partition leaders are geographically closer to clients. Leader pinning can lower networking costs and help guarantee lower latency by routing produce and consume requests to brokers located in certain AZs. @@ -115,3 +119,15 @@ The following cluster properties are new in this version: * xref:reference:properties/cluster-properties.adoc#default_leaders_preference[`default_leaders_preference`] * xref:reference:properties/cluster-properties.adoc#rpk_path[`rpk_path`] * xref:reference:properties/cluster-properties.adoc#tombstone_retention_ms[`tombstone_retention_ms`] +* xref:reference:properties/cluster-properties.adoc#iceberg_catalog_base_location[`iceberg_catalog_base_location`] +* xref:reference:properties/cluster-properties.adoc#iceberg_catalog_commit_interval_ms[`iceberg_catalog_commit_interval_ms`] +* xref:reference:properties/cluster-properties.adoc#iceberg_catalog_type[`iceberg_catalog_type`] +* xref:reference:properties/cluster-properties.adoc#iceberg_delete[`iceberg_delete`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_client_id[`iceberg_rest_catalog_client_id`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_client_secret[`iceberg_rest_catalog_client_secret`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_crl_file[`iceberg_rest_catalog_crl_file`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_endpoint[`iceberg_rest_catalog_endpoint`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_prefix[`iceberg_rest_catalog_prefix`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_request_timeout_ms[`iceberg_rest_catalog_request_timeout_ms`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_token[`iceberg_rest_catalog_token`] +* xref:reference:properties/cluster-properties.adoc#iceberg_rest_catalog_trust_file[`iceberg_rest_catalog_trust_file`] From 14b6ee2b4141e8dc28d6ac98a74aacdf6e96f400 Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:02:59 -0500 Subject: [PATCH 42/45] Edits per review --- .../pages/topic-iceberg-integration.adoc | 23 +++++++++---------- .../pages/properties/cluster-properties.adoc | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 8f8bc20d7e..67c32b424b 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -4,6 +4,10 @@ :page-categories: Management, High Availability, Data Replication, Integration :page-beta: true +[NOTE] +==== +include::shared:partial$enterprise-license.adoc[] +==== The Apache Iceberg integration for Redpanda allows you to store topic data in the cloud in the Iceberg open table format. This makes your streaming data immediately available in downstream analytical systems, including data warehouses like Snowflake, Databricks, Clickhouse, and Redshift, without setting up and maintaining additional ETL pipelines. You can also integrate your data directly into commonly-used big data processing frameworks, such as Apache Spark and Flink, standardizing and simplifying the consumption of streams as tables in a wide variety of data analytics pipelines. @@ -12,17 +16,12 @@ The Iceberg integration uses xref:manage:tiered-storage.adoc[Tiered Storage]. Wh == Prerequisites * Install xref:get-started:rpk-install.adoc[`rpk`]. - -* {empty} -include::shared:partial$enterprise-license.adoc[] -+ -To check if you already have a license key applied to your cluster: +* To check if you already have a license key applied to your cluster: + [,bash] ---- rpk cluster license info ---- - * Enable xref:manage:tiered-storage.adoc#set-up-tiered-storage[Tiered Storage] for the topics for which you want to generate Iceberg tables. == Limitations @@ -30,7 +29,7 @@ rpk cluster license info * It is not possible to append topic data to an existing Iceberg table that is not created by Redpanda. * If you enable the Iceberg integration on an existing Redpanda topic, Redpanda does not backfill the generated Iceberg table with topic data. * JSON schemas are not currently supported. If the topic data is in JSON, use the `key_value` mode to store the JSON in Iceberg, which then can be parsed by most query engines. -* If you are using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post. +* If you are using Avro or Protobuf data, you must use the Schema Registry wire format, where producers include the magic byte and schema ID in the message payload header. See also: xref:manage:schema-reg/schema-id-validation.adoc[] and the https://www.redpanda.com/blog/schema-registry-kafka-streaming#how-does-serialization-work-with-schema-registry-in-kafka[Understanding Apache Kafka Schema Registry^] blog post to learn more about the wire format. * You can only use one schema per topic. Schema versioning as well as upcasting (where a value is cast into its more generic data type) are not supported. See <> for more details. * xref:manage:remote-read-replicas.adoc[Remote read replicas] and xref:manage:topic-recovery.adoc[topic recovery] are not supported for Iceberg-enabled topics. @@ -59,7 +58,7 @@ When you enable the Iceberg integration for a Redpanda topic, Redpanda brokers s == Enable Iceberg integration -To create an Iceberg table for a Redpanda topic, you must set the cluster configuration property `iceberg_enabled` to `true`, and also configure the topic property `redpanda.iceberg.mode`. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. +To create an Iceberg table for a Redpanda topic, you must set the cluster configuration property xref:reference:properties/cluster-properties.adoc#iceberg_enabled[`iceberg_delete`] to `true`, and also configure the topic property xref:reference:properties/topic-properties.adoc#redpandaicebergmode[`redpanda.iceberg.mode`]. You can choose to provide a schema if you need the Iceberg table to be structured with defined columns. . Set the `iceberg_enabled` configuration option on your cluster to `true`. You must restart your cluster if you change this configuration for a running cluster. + @@ -77,13 +76,13 @@ Successfully updated configuration. New configuration version is 2. + [,bash,] ---- -rpk topic create --partitions 1 --replicas 1 +rpk topic create ---- + [,bash,role=no-copy] ---- TOPIC STATUS -new-topic-name OK + OK ---- . Enable the integration for the topic by configuring `redpanda.iceberg.mode`. You can choose one of the following modes: @@ -102,7 +101,7 @@ rpk topic alter-config --set redpanda.iceberg.mode= OK ---- . Register a schema for the topic. This step is required for the `value_schema_id_prefix` mode, but is optional otherwise. @@ -115,7 +114,7 @@ rpk registry schema create --schema --type 1 1 PROTOBUF ---- The Iceberg table is inside a namespace called `redpanda`, and has the same name as the Redpanda topic name. As you produce records to the topic, the data also becomes available in object storage for consumption by Iceberg-compatible clients. diff --git a/modules/reference/pages/properties/cluster-properties.adoc b/modules/reference/pages/properties/cluster-properties.adoc index 6747c89743..843dc88601 100644 --- a/modules/reference/pages/properties/cluster-properties.adoc +++ b/modules/reference/pages/properties/cluster-properties.adoc @@ -1672,7 +1672,7 @@ URL of Iceberg REST catalog endpoint. === iceberg_rest_catalog_prefix -Prefix part of the Iceberg REST catalog URL. Prefix is appended to the catalog path, for example `/v1/{prefix}/namespaces`. +Prefix part of the Iceberg REST catalog URL. Prefix is appended to the catalog path, for example `/v1/\{prefix}/namespaces`. *Requires restart:* Yes From 866e43939503b3d4749d352c875e7f94cbbed53b Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:02:59 -0500 Subject: [PATCH 43/45] Edits per review --- modules/manage/pages/topic-iceberg-integration.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 67c32b424b..760d3c28d9 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -325,7 +325,7 @@ Spark can use the REST catalog to automatically discover the topic's Iceberg tab ==== File system-based catalog (`object_storage`) -If you are using the `object_storage` catalog type, you must set up the catalog integration in your processing engine accordingly. For example, you can configure Spark to use a file system-based catalog with at least the following properties: +If you are using the `object_storage` catalog type, you must set up the catalog integration in your processing engine accordingly. For example, you can configure Spark to use a file system-based catalog with at least the following properties, is using AWS S3 for object storage: ``` spark.sql.catalog.streaming.type = hadoop @@ -403,7 +403,7 @@ FROM .ClickEvent; === Query topic in key-value mode -You can also forgo using a schema, which means using semi-structured data in Iceberg. +In `key_value` mode, you do not associate the topic with a schema in the Schema Registry, which means using semi-structured data in Iceberg. In this example, it is assumed you have created the `ClickEvent_key_value` topic, set `redpanda.iceberg.mode` to `key_value`, and are also connecting to a REST-based Iceberg catalog. From 776a3b4064a45178d44b44d09f805d391e7cf5be Mon Sep 17 00:00:00 2001 From: kbatuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:02:59 -0500 Subject: [PATCH 44/45] Edits per review --- modules/manage/pages/topic-iceberg-integration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 760d3c28d9..8bcacdee0d 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -270,7 +270,7 @@ Set the cluster configuration property `iceberg_catalog_type` with one of the fo * `rest`: Connect to and update an Iceberg catalog using a REST API. See the https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml[Iceberg REST Catalog API specification]. * `object_storage`: Write catalog files to the same object storage bucket as the data files. Use the object storage URL with an Iceberg client to access the catalog and data files for your Redpanda Iceberg tables. -Switching catalog types is not supported. +Once you have enabled the Iceberg integration for a topic and selected a catalog type, you cannot switch to another catalog type. For production use cases, Redpanda recommends the `rest` option with REST-enabled Iceberg catalog services such as https://docs.tabular.io/[Tabular^], https://docs.databricks.com/en/data-governance/unity-catalog/index.html[Databricks Unity^] and https://github.com/apache/polaris[Apache Polaris^]. From 903785ca6a38321459909bf2476d4629a956afc5 Mon Sep 17 00:00:00 2001 From: Kat Batuigas <36839689+kbatuigas@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:13:00 -0500 Subject: [PATCH 45/45] Apply suggestions from code review Co-authored-by: Paulo Borges --- modules/manage/pages/topic-iceberg-integration.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/manage/pages/topic-iceberg-integration.adoc b/modules/manage/pages/topic-iceberg-integration.adoc index 8bcacdee0d..4728bbef42 100644 --- a/modules/manage/pages/topic-iceberg-integration.adoc +++ b/modules/manage/pages/topic-iceberg-integration.adoc @@ -123,7 +123,7 @@ The Iceberg table is inside a namespace called `redpanda`, and has the same name The `redpanda.iceberg.mode` property determines how Redpanda maps the topic data to the Iceberg table structure. You can either have the generated Iceberg table match the stucture of a Avro or Protobuf schema in the Schema Registry, or use the `key_value` mode where Redpanda stores the record values as-is in the table. -The JSON Schema format is not supported in this beta release. If your topic data is in JSON, it is recommended to use the `key_value` mode. +The JSON Schema format is not supported. If your topic data is in JSON, it is recommended to use the `key_value` mode. === Iceberg modes and table schemas @@ -253,9 +253,9 @@ Protobuf:: * Repeated values are translated into Iceberg `array` types. * Enums are translated into Iceberg `int` types based on the integer value of the enumerated type. -* uint32 and fixed32 are translated into Iceberg `long` types as that is the existing semantic for unsigned 32-bit values in Iceberg. -* uint64 and fixed64 values are translated into their Base-10 string representation. -* The timestamp well-known type in Protobuf is translated into `timestamp` in Iceberg. +* `uint32` and `fixed32` are translated into Iceberg `long` types as that is the existing semantic for unsigned 32-bit values in Iceberg. +* `uint64` and `fixed64` values are translated into their Base-10 string representation. +* The `timestamp` type in Protobuf is translated into `timestamp` in Iceberg. * Messages are converted into Iceberg structs. * Recursive types are not supported. --