Round-Trip an Apache Iceberg Table

This example reads and writes an Apache Iceberg™ table on Confluent Manager for Apache Flink (CMF) from end to end. You create an object storage, configure CMF and build the Iceberg connector, then create a table, write three rows, and read them back.

Important

Reading an Iceberg table back through Flink SQL requires CMF 2.4.2 or later. Writing requires a cp-flink-sql image that carries the connector classloader fix (1.19-cp10 or later). Use both, or the SELECT step fails to load the Iceberg catalog.

Prerequisites

Complete the examples setup first. It installs the MinIO object store and enables artifact management, which this example builds on. Iceberg needs extra CMF configuration and a dedicated compute pool, both covered below. The credentials (console / console123) and namespace (flink) match the setup. Substitute your own.

Why Iceberg needs configuration on both sides

Iceberg’s Hadoop catalog runs in two places, so both need to reach the object store. It runs inside the Flink cluster (the INSERT and SELECT jobs), and it runs inside CMF itself when CMF compiles a statement (CREATE TABLE writes the table’s first metadata file, and every statement loads the catalog to plan the job). You give the Flink side its credentials through the compute pool, and the CMF side through the configuration in the next section.

Configure CMF for Iceberg

CMF needs S3 access on its compile path, a larger artifact upload limit, and a wider class allowlist. First create a core-site.xml ConfigMap in CMF’s namespace so CMF’s Iceberg catalog can reach the object store (CMF is not a Flink process, so it reads Hadoop settings from HADOOP_CONF_DIR rather than from a pool’s flink.hadoop.* keys):

apiVersion: v1
kind: ConfigMap
metadata:
  name: cmf-hadoop-conf
  namespace: flink
data:
  core-site.xml: |
    <?xml version="1.0"?>
    <configuration>
      <property><name>fs.s3a.endpoint</name><value>http://minio.flink.svc.cluster.local:9000</value></property>
      <property><name>fs.s3a.path.style.access</name><value>true</value></property>
      <property><name>fs.s3a.connection.ssl.enabled</name><value>false</value></property>
      <property><name>fs.s3a.access.key</name><value>console</value></property>
      <property><name>fs.s3a.secret.key</name><value>console123</value></property>
      <property><name>fs.s3a.aws.credentials.provider</name><value>org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider</value></property>
      <property><name>fs.s3a.impl</name><value>org.apache.hadoop.fs.s3a.S3AFileSystem</value></property>
    </configuration>

Apply it, then upgrade CMF with the following iceberg-values.yaml. These values extend the examples setup values with the Iceberg-specific settings, so this single upgrade is all the Iceberg example needs: it raises maxUploadSize above the ~112 MB connector JAR, adds the JDK packages the Iceberg connector loads at compile time to the class allowlist, and mounts the ConfigMap with the AWS environment the S3A credential chain reads.

cmf:
  artifacts:
    enabled: true
    basePath: s3://flink/cmf-artifacts
    maxUploadSize: 300MB
    configuration:
      s3.endpoint: http://minio.flink.svc.cluster.local:9000
      s3.path.style.access: "true"
      s3.connection.ssl.enabled: "false"
      s3.access-key: console
      s3.secret-key: console123
  sql:
    environmentCatalog:
      enabled: true
    # The default allowlist plus the JDK packages a Hadoop/Iceberg connector
    # loads at compile time (org.w3c.dom, org.xml.sax, org.ietf.jgss, sun.misc).
    # Helm replaces this list, so it must repeat the defaults.
    classAllowlist:
      - 'java\..*'
      - 'javax\..*'
      - 'jdk\.internal\..*'
      - 'org\.apache\.flink\..*'
      - 'io\.confluent\.flink\..*'
      - 'org\.slf4j\..*'
      - 'org\.apache\.kafka\..*'
      - 'org\.w3c\.dom\..*'
      - 'org\.xml\.sax\..*'
      - 'org\.ietf\.jgss\..*'
      - 'sun\.misc\..*'
extraEnv:
  - name: HADOOP_CONF_DIR
    value: /opt/hadoop-conf
  - name: AWS_ACCESS_KEY_ID
    value: console
  - name: AWS_SECRET_ACCESS_KEY
    value: console123
  - name: AWS_REGION
    value: us-east-1
mountedVolumes:
  volumes:
    - name: hadoop-conf
      configMap:
        name: cmf-hadoop-conf
  volumeMounts:
    - name: hadoop-conf
      mountPath: /opt/hadoop-conf
kubectl apply -f cmf-hadoop-conf.yaml

helm upgrade --install cmf confluentinc/confluent-manager-for-apache-flink \
  --namespace flink \
  --values iceberg-values.yaml

Build the connector JAR

The connector is a fat JAR that bundles the Iceberg Flink runtime, an S3A filesystem, and a slim AWS SDK. Iceberg’s published iceberg-flink-runtime JAR carries neither hadoop-aws nor an AWS SDK, so an S3 warehouse needs those shaded in. The bundle also relocates Flink’s HadoopUtils class so the connector resolves Hadoop from its own JAR rather than CMF’s Hadoop-free host classpath.

The versions are load-bearing: iceberg-flink-runtime-1.19 1.10.2 is the last Iceberg release that supports Flink 1.19, and the AWS SDK version must match the one hadoop-aws was built against. All dependencies are on Maven Central. pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>io.confluent.examples.flink</groupId>
  <artifactId>iceberg-flink-1.19-s3-bundle</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>11</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 1.10.2 is the last Iceberg release that supports Flink 1.19. -->
    <iceberg.version>1.10.2</iceberg.version>
    <hadoop.version>3.4.1</hadoop.version>
    <flink.version>1.19.1</flink.version>
    <!-- The AWS SDK v2 version hadoop-aws 3.4.1 was built against; pin it exactly. -->
    <awssdk.version>2.24.6</awssdk.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>bom</artifactId>
        <version>${awssdk.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <!-- The 'iceberg' factory (FlinkDynamicTableFactory) and the HadoopCatalog. -->
    <dependency>
      <groupId>org.apache.iceberg</groupId>
      <artifactId>iceberg-flink-runtime-1.19</artifactId>
      <version>${iceberg.version}</version>
    </dependency>

    <!-- Hadoop client (Configuration, FileSystem); needed at compile and runtime. -->
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client-api</artifactId>
      <version>${hadoop.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client-runtime</artifactId>
      <version>${hadoop.version}</version>
    </dependency>

    <!-- S3AFileSystem (s3a://). Exclude hadoop-common/-client (provided by
         hadoop-client-runtime) and the large AWS SDK bundle; add slim modules below. -->
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-aws</artifactId>
      <version>${hadoop.version}</version>
      <exclusions>
        <exclusion>
          <groupId>org.apache.hadoop</groupId>
          <artifactId>hadoop-common</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.apache.hadoop</groupId>
          <artifactId>hadoop-client</artifactId>
        </exclusion>
        <exclusion>
          <groupId>software.amazon.awssdk</groupId>
          <artifactId>bundle</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

    <!-- Slim AWS SDK v2 modules S3A needs (vs. the full bundle); versions from the BOM. -->
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>s3</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>s3-transfer-manager</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>apache-client</artifactId>
    </dependency>
    <dependency>
      <groupId>software.amazon.awssdk</groupId>
      <artifactId>sts</artifactId>
    </dependency>

    <!-- Source of org.apache.flink.runtime.util.HadoopUtils; only that class is
         kept and it is relocated (see below). -->
    <dependency>
      <groupId>org.apache.flink</groupId>
      <artifactId>flink-hadoop-fs</artifactId>
      <version>${flink.version}</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>iceberg-flink-1.19-s3-bundle-${project.version}</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.5.3</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
              <filters>
                <!-- Keep only HadoopUtils from flink-hadoop-fs. -->
                <filter>
                  <artifact>org.apache.flink:flink-hadoop-fs</artifact>
                  <includes>
                    <include>org/apache/flink/runtime/util/HadoopUtils*.class</include>
                  </includes>
                </filter>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                    <exclude>module-info.class</exclude>
                    <exclude>META-INF/versions/*/module-info.class</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <!-- Merge the Flink Factory SPI (iceberg) and the Hadoop FileSystem SPI (s3a). -->
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
              </transformers>
              <!-- Relocate HadoopUtils so it no longer matches org.apache.flink.* and
                   loads from this JAR instead of CMF's Hadoop-free host classpath. -->
              <relocations>
                <relocation>
                  <pattern>org.apache.flink.runtime.util.HadoopUtils</pattern>
                  <shadedPattern>shaded.flink.runtime.util.HadoopUtils</shadedPattern>
                </relocation>
              </relocations>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Build it:

mvn clean package

The build writes target/iceberg-flink-1.19-s3-bundle-1.0.0.jar (about 112 MB).

Create the compute pool

Create a compute pool on the cp-flink-sql:1.19-cp11 image whose configuration carries the Hadoop S3A credentials the Iceberg catalog uses on the Flink side, not only the Flink s3.* keys. Save this as iceberg-pool.json:

{
  "apiVersion": "cmf.confluent.io/v1",
  "kind": "ComputePool",
  "metadata": { "name": "iceberg-pool" },
  "spec": {
    "type": "DEDICATED",
    "clusterSpec": {
      "flinkVersion": "v1_19",
      "image": "confluentinc/cp-flink-sql:1.19-cp11",
      "flinkConfiguration": {
        "taskmanager.numberOfTaskSlots": "4",
        "execution.checkpointing.interval": "2000",
        "state.checkpoints.dir": "s3://flink/iceberg-pool/checkpoints",
        "state.savepoints.dir": "s3://flink/iceberg-pool/savepoints",
        "high-availability.storageDir": "s3://flink/iceberg-pool/recovery",
        "s3.endpoint": "http://minio.flink.svc.cluster.local:9000",
        "s3.path.style.access": "true",
        "s3.connection.ssl.enabled": "false",
        "s3.access-key": "console",
        "s3.secret-key": "console123",
        "flink.hadoop.fs.s3a.endpoint": "http://minio.flink.svc.cluster.local:9000",
        "flink.hadoop.fs.s3a.path.style.access": "true",
        "flink.hadoop.fs.s3a.connection.ssl.enabled": "false",
        "flink.hadoop.fs.s3a.access.key": "console",
        "flink.hadoop.fs.s3a.secret.key": "console123",
        "flink.hadoop.fs.s3a.aws.credentials.provider": "org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider"
      },
      "taskManager": { "resource": { "cpu": 0.5, "memory": "1024m" } },
      "jobManager": { "resource": { "cpu": "500m", "memory": "1536m" } }
    }
  }
}
confluent flink compute-pool create iceberg-pool.json \
  --environment test \
  --url http://cmf:8080

Upload the connector artifact

Label the JAR as a connector with the iceberg identifier, in artifact.json:

{
  "apiVersion": "cmf.confluent.io/v1",
  "kind": "Artifact",
  "metadata": {
    "name": "iceberg-connector.jar",
    "labels": {
      "cmf.confluent.io/sql-artifact-type": "connector",
      "cmf.confluent.io/sql-artifact-identifier": "iceberg"
    }
  },
  "spec": {}
}

Upload it. The JAR is large, so allow for the upload time:

curl -X POST http://cmf:8080/cmf/api/v1/environments/test/artifacts \
  -F 'artifact=@artifact.json;type=application/json' \
  -F 'file=@target/iceberg-flink-1.19-s3-bundle-1.0.0.jar'

The response reaches status.phase of READY.

Create the table

Create an Iceberg table in the environment catalog with a Hadoop catalog on the object store. CREATE TABLE stores the options only; the connector is not resolved yet.

confluent flink statement create ice-create \
  --url http://cmf:8080 \
  --environment test \
  --compute-pool iceberg-pool \
  --catalog _env_test \
  --database default \
  --sql "CREATE TABLE \`ice_orders\` (\`id\` BIGINT, \`data\` STRING) WITH ('connector' = 'iceberg', 'catalog-type' = 'hadoop', 'catalog-name' = 'hadoop_s3', 'warehouse' = 's3a://flink/iceberg-warehouse');"

The statement reaches COMPLETED with sqlKind of CREATE_TABLE.

Write rows

Insert three rows. This is the first statement to use the table, so CMF resolves the iceberg artifact and ships it to the Flink cluster. The bounded INSERT reaches COMPLETED when the rows are written.

confluent flink statement create ice-insert \
  --url http://cmf:8080 \
  --environment test \
  --compute-pool iceberg-pool \
  --catalog _env_test \
  --database default \
  --sql "INSERT INTO \`ice_orders\` VALUES (1, 'a'), (2, 'b'), (3, 'c');"

Confirm the data landed in object storage. The Hadoop catalog lays the table out under <database>/<table>/:

kubectl -n flink port-forward svc/minio 9000:9000 &
mc alias set minio http://localhost:9000 console console123
mc ls --recursive minio/flink/iceberg-warehouse/
default/ice_orders/data/00000-0-...-00001.parquet
default/ice_orders/metadata/snap-...-1-....avro
default/ice_orders/metadata/....-m0.avro
default/ice_orders/metadata/v2.metadata.json
default/ice_orders/metadata/version-hint.text

Read rows back

Query the table. CMF resolves and ships the same artifact, and the job reads the current snapshot:

confluent flink statement create ice-select \
  --url http://cmf:8080 \
  --environment test \
  --compute-pool iceberg-pool \
  --catalog _env_test \
  --database default \
  --sql "SELECT * FROM \`ice_orders\`;"

The query returns the three rows:

id  data
1   a
2   b
3   c

Clean up

Drop the table, then remove the pool and artifact:

confluent flink statement create ice-drop \
  --url http://cmf:8080 --environment test --compute-pool iceberg-pool \
  --catalog _env_test --database default \
  --sql "DROP TABLE IF EXISTS \`ice_orders\`;"

curl -X DELETE http://cmf:8080/cmf/api/v1/environments/test/compute-pools/iceberg-pool
curl -X DELETE http://cmf:8080/cmf/api/v1/environments/test/artifacts/iceberg-connector.jar

Dropping the table removes it from the environment catalog. Remove the Iceberg warehouse files from object storage to reclaim the space:

mc rm --recursive --force minio/flink/iceberg-warehouse/