Apache Iceberg

Writes records as Parquet files to an Iceberg table in S3, or a store that speaks its API, and commits them to the table through its REST catalog. Each commit is a snapshot of the table.

Example

Signs in to a Polaris catalog with OAuth client credentials, the client's secret in the worker-host variable CATALOG_CLIENT_SECRET, and writes the table analytics.web.access_log, partitioned by the day of ts and 16 buckets of user.id when it creates it. Storage is signed with what the catalog vends.

{
  "id": "access-log",
  "type": "iceberg",
  "catalog": "https://polaris.example.com/api/catalog",
  "warehouse": "analytics",
  "namespace": "analytics.web",
  "table": "access_log",
  "partitionBy": [
    "day(ts)",
    "bucket(16, user.id)"
  ],
  "clientId": "smeltr",
  "clientSecret": "CATALOG_CLIENT_SECRET",
  "scope": "PRINCIPAL_ROLE:ALL"
}
The Apache Iceberg form on the Destinations page: catalog https://polaris.example.com/api/catalog, namespace analytics.web, table access_log, partition by day(ts) and bucket(16, user.id), catalog sign-in OAuth client credentials, client id smeltr. Open complete screenshot ↗The Apache Iceberg form on the Destinations page: catalog https://polaris.example.com/api/catalog, namespace analytics.web, table access_log, partition by day(ts) and bucket(16, user.id), catalog sign-in OAuth client credentials, client id smeltr. Open complete screenshot ↗
An Iceberg destination on the Destinations page. Choose the catalog sign-in, and its fields follow. Warehouse and scope sit under Catalog and storage.

Signs in to AWS Glue's Iceberg endpoint with AWS signatures, as the worker host's AWS credentials, in the region the address names. Set the warehouse to the account's id, and the namespace to a Glue database.

{
  "id": "access-log",
  "type": "iceberg",
  "catalog": "https://glue.us-east-2.amazonaws.com/iceberg",
  "warehouse": "111122223333",
  "namespace": "web",
  "table": "access_log",
  "signingName": "glue"
}

Before you start

  • Have an Iceberg REST catalog the worker hosts can reach, such as Apache Polaris, AWS Glue's Iceberg endpoint, or S3 Tables. The table's files are in S3 or a store that speaks its API.
  • Create the namespace in the catalog. The destination creates a missing table in it, but not the namespace.
  • A table that exists is of format version 2 and not encrypted. A table the destination creates is of version 2.
  • Give the principal the destination signs in to the catalog as permission to load and update the table, and to create it when it is missing. Signing in lists the ways.
  • Give whatever signs for storage permission to write and delete objects under the table's data location, and to read and write objects under its metadata location: the credentials the catalog vends, the access key or role the destination names, or the worker host's own. The destination deletes no metadata.
  • Keep the table's location, its write.data.path and its write.metadata.path in one bucket. The destination refuses to start on a table whose paths are in another bucket than its location.
  • Send this destination records of one type: the routes writing to it write the same record, which is the table's schema. See Columns.

Settings

It also takes the settings every destination takes.

catalog string required

The REST catalog's base address, before /v1.

Examples
https://polaris.example.com/api/cataloghttps://catalog.example.com:8181

namespace string required

The table's namespace, its levels joined by dots.

Examples
logsanalytics.web

table string required

The table it writes. A missing table is created from the route's record.

Examples
firewallaccess_log

partitionBy list of strings default none, an unpartitioned table

How a table it creates is partitioned: a column, or year, month, day or hour of one, or bucket(n, column) or truncate(width, column). A table that exists keeps its own.

Examples
day(ts)hostbucket(16, user.id)truncate(4, name)hour(ts)

token credential default none

The credential holding a bearer token for the catalog. Not with clientId.

Examples
CATALOG_TOKEN{"secret": "catalog-token","version": 1}

clientId string default none

The OAuth client it signs in to the catalog as, with clientSecret.

Examples
smeltr0oa1b2c3d4

clientSecret credential default none

The credential holding the OAuth client's secret. Only with clientId.

Examples
CATALOG_CLIENT_SECRET{"secret": "catalog-client","version": 1}

signingName string default no AWS signature

Signs every catalog call with AWS Signature Version 4 for this service, with the AWS credentials storage names, else the host's, in the region the catalog's AWS address names, else its region: glue for Glue's Iceberg endpoint, s3tables for S3 Tables. Needs region when the address names none. Not with token or clientId.

Examples
glues3tables

identity identity default none

The connection identity it connects to the catalog and the store under, for a private CA or a client certificate.

Examples
catalog-clientlake-ca

Advanced: Catalog and storage

warehouse string default the catalog's default

The warehouse the catalog serves the table from.

Examples
analyticss3://lake/warehouse

tokenUrl string default the catalog's own /v1/oauth/tokens

Where the client's token is obtained. Only with clientId.

Example
https://auth.example.com/oauth2/token

scope string default none

The scope the client's token is asked for. Use the scope your catalog takes. Only with clientId.

Examples
PRINCIPAL_ROLE:ALLcatalog

accessKeyId credential default what the catalog vends, else the host's credentials

The credential holding the access key's id. Set both accessKeyId and secretAccessKey, or neither.

Examples
AWS_ACCESS_KEY_ID{"secret": "aws-access-key-id","version": 1}

secretAccessKey credential default what the catalog vends, else the host's credentials

The credential holding the access key's secret.

Examples
AWS_SECRET_ACCESS_KEY{"secret": "aws-secret-access-key","version": 1}

region string default the region the catalog gives the table, else the one the catalog's AWS address names, else the host's, else us-east-1

The bucket's region.

Examples
us-east-2eu-west-1ap-southeast-2

endpoint string default the one the catalog gives the table, else the host's, else AWS

An S3-compatible service's address. Without an access key, the worker host's AWS settings, such as AWS_ENDPOINT_URL, come after the catalog's.

Examples
https://minio.example.com:9000https://10.0.8.20:9000

roleArn string default none

A role to assume, for a bucket in another account.

Examples
arn:aws:iam::111122223333:role/smeltr-writerarn:aws:iam::111122223333:role/service-role/smeltr-writer

externalId string default none

The external id the role requires. Only with roleArn.

Examples
smeltracme-7f3c2a

authEndpoint string default the region's own

Where the role in roleArn is assumed. Only with roleArn.

Examples
https://sts.us-east-2.amazonaws.comhttps://sts-fips.us-east-1.amazonaws.com

Advanced: Delivery

compression string default zstd

How each Parquet file compresses its columns.

none
Writes columns uncompressed.
gzip
Compresses columns with gzip.
snappy
Compresses columns with Snappy.
lz4
Compresses columns with LZ4.
zstd
Compresses columns with Zstandard.

inFlight integer default 4

How many parts of one file upload at once. 1 to 64.

Examples
812

maxObjectBytes size default 64 MiB

The largest data file it writes, compressed. 64 KiB to 64 GiB.

Examples
256 MiB512 MiB

bufferBytes size default 256 MiB

Memory it may use on each worker for records not yet committed. Files are committed early when it fills. 4 MiB to 32 TiB.

Examples
1 GiB4 GiB

stallAfterSecs integer default 30

Seconds one call to the store or the catalog, or one commit, may take before it counts as stalled. 1 to 120.

Examples
60120

fileIntervalSecs integer default 60

The longest records wait, in seconds, before they are committed to the table. Each commit is a snapshot the table keeps until it is expired; records are acknowledged only once committed, so a source is held open up to this long. 5 to 240.

Examples
30240

Signing in

The destination signs in to the catalog one way, and to the table's storage another. The GUI form's catalog sign-in chooses the first.

Catalog sign-inSettingsWhat it sends
nonenone of theseCalls with no sign-in, until the catalog gives a token of the table's own when it loads it.
A bearer tokentokenThe token from the credential on every call, until the catalog gives a token of the table's own when it loads it.
OAuth client credentialsclientId, clientSecret, and tokenUrl and scope when the catalog needs themA token it obtains from the token URL with the client's id and secret, replaced before it expires, and again when the catalog refuses it.
AWS signaturessigningNameEvery call signed with AWS Signature Version 4 for the service it names: glue for Glue's Iceberg endpoint, s3tables for S3 Tables. It signs with the access key or role the destination names for storage, else the worker host's AWS credentials, for the region the catalog's AWS address names, else region. When the address names no region, region is required.

Storage is signed with the first of these the destination has:

  1. The access key in accessKeyId and secretAccessKey, or the role in roleArn.
  2. The credentials the catalog vends for the table. The destination asks for them when it loads the table, and asks again five minutes before they expire.
  3. The worker host's own AWS credentials, such as an instance role.

The table's bucket is in region, else the region the catalog gives the table, else the one the catalog's AWS address names, else the worker host's, else us-east-1. It is reached at endpoint, else the endpoint the catalog gives the table, else, with no access key named, the one the worker host's AWS settings name, else AWS's own address for the region.

Columns

The table's schema is the record the routes write here, its fields the table's columns, nested fields as structs and arrays as lists. A missing table is created with that schema and with partitionBy as its partition spec. A table that exists keeps its columns and its spec: a field it lacks, at any depth, is added as an optional column before any file is written, and a column the record lacks reads as null in the rows written. A field that can hold no value, null or a list or closed struct of nothing else, has no column. Each field is written as the Iceberg type of its record type:

Record typeIceberg type
boolboolean
i64long
durationlong, its count of microseconds
f64double
strstring
variantstring, the value as JSON
datetimetimestamptz
bytesbinary
array<T>list
{name: T, ...}struct; an open object's other keys go in a string column named rest

Every column the destination writes is optional. It refuses to start when the table holds a field as another type, or has a required column the record may leave null or does not write.

How it works

When it starts, the destination loads the table from the catalog, or creates it, and adds the record's new columns. It then writes each batch's records into Parquet files under the table's data location, write.data.path when the table sets it, partitioned by the table's own spec: identity, year, month, day, hour, bucket and truncate. Each file carries the table's column ids. A file stays open across batches, one per partition, at most 64 at once; a 65th closes the one written least recently. A file also closes at maxObjectBytes.

The destination commits when fileIntervalSecs passes, when the records it holds near bufferBytes, and whenever the worker waits for its destinations to acknowledge everything they hold. A commit closes every open file, writes a manifest of the new files and a manifest list under the table's metadata location, merging the parent's small manifests as the table's commit.manifest.* properties say, and asks the catalog to append them as the table's next snapshot on top of the one it read. A batch is acknowledged once the commit holding its records lands.

A commit another writer beat to the table is made again at once on top of the new snapshot, up to 8 times. A failed call is tried again with backoff until stallAfterSecs. When the catalog's answer to a commit is lost, the destination looks for that commit in the table before it tries again, so it does not make it twice. Past stallAfterSecs the destination aborts its open uploads and writes the batches again. It deletes the files it finished unless a lost answer may have committed them; then they stay, and their rows may be in the table twice. A file storage keeps after a failure is an orphan for the table's own cleanup.

Each commit is a snapshot the table keeps until it is expired. Expire snapshots and compact small files with the table's own maintenance.

A file larger than 8 MiB goes up as a multipart upload, which the destination completes when the file closes and aborts when it fails. Two things leave one unfinished: the worker stops without shutting down (it is killed, it crashes, or its host is lost) while the file is open, or storage does not answer the abort within stallAfterSecs. A file stays open at most fileIntervalSecs, so give the bucket a lifecycle rule that aborts incomplete multipart uploads after a day: longer than any file stays open.

Verify

Send a record with a unique marker through the source. After fileIntervalSecs, look for a new snapshot in the table's history, and query the table for the marker with any engine that reads Iceberg.

Troubleshooting

the catalog refused …
The catalog answered with the status the message names. On 401 or 403, check the sign-in: the token, the client's id, secret and scope, or the AWS credentials and the signing name. On 404, check catalog, warehouse and that the namespace exists.
the routes writing here write different records, and a table has one schema
Two routes feeding the destination write records of different types. Make them write the same record, or give each table a destination of its own.
… is … in the table, and the record writes … there as …
The table holds a column as another type than the record's field. Rename the field in the pipeline or the column in the table.
the table's column … is required …
Every column the destination writes is optional. Make the column optional in the table, or send the records to a table of their own.
it is of format version …, and only version 2 is written
Write to a table of format version 2.
it is encrypted, and an encrypted table is not written
Write to a table that is not encrypted.
the catalog vended no single credential for both the table's data location …
The catalog's credentials cover one of the table's locations and not the other. Name an access key or a role for storage, or have the catalog vend one credential for both.
the catalog places the table's storage at …, plain HTTP off this host
The catalog gives the table an http:// endpoint on another host. Name an https:// endpoint.
Access denied writing to storage
Check that whatever signs for storage may write and delete objects under the table's data location, and read and write objects under its metadata location.

See also