Snowflake World Tour hits your city

See how leading teams deploy agents at scale. Find a stop near you. Register free.

Snowflake for Developers/Guides/Getting Started with Snowflake and BigQuery via Iceberg
Quickstart

Getting Started with Snowflake and BigQuery via Iceberg

Apache Iceberg
Yoav Ostrinsky

Overview

Apache Iceberg has become the de-facto open table format for the data lakehouse because it lets multiple engines read and write the same physical tables without copying data. This guide shows you how to make Snowflake and Google BigQuery interoperate on one shared set of Iceberg tables using the modern, catalog-based approach: the Google BigLake Iceberg REST catalog (part of Google's Lakehouse for Apache Iceberg), Snowflake catalog-linked databases, and workload identity federation for keyless authentication.

Use Case

There is often no one-size-fits-all engine for every workload. Teams land data with one platform and serve it with another, or inherit multiple platforms through mergers and acquisitions. Sharing a single set of Iceberg tables through a common catalog lets you:

  • Modernize a data lake into an open lakehouse
  • Enable data interoperability and a joint data-mesh architecture
  • Build batch or streaming ingestion, transformation, and CDC pipelines
  • Serve analytics-ready data to teams on the engine they prefer — without duplicating storage

The Two Interoperability Patterns You Will Build

The BigLake Iceberg REST catalog exposes one endpoint (https://biglake.googleapis.com/iceberg/v1/restcatalog) but supports two warehouse flavours. Which flavour a catalog uses determines who owns the tables and who can write. This guide walks through both, because real deployments usually need one specific direction:

Pattern A — Snowflake writes, BigQuery readsPattern B — BigQuery writes, Snowflake reads
Catalog warehousegs://<bucket> (GCS flavour)bq://projects/<project> (BigQuery federation)
Who owns the tablesThe BigLake catalog (files in GCS)BigQuery (Apache Iceberg managed tables)
Primary writerSnowflake (via catalog-linked database)BigQuery (DML / streaming / CDC)
ReaderBigQuery, live, via project.catalog.namespace.tableSnowflake, via catalog integration + external volume
Credential vendingSupported (no external volume needed)Not supported (external volume required)
Data freshnessLive for readersReader refreshes after writer publishes metadata
Snowflake and BigQuery interoperating over one BigLake Iceberg REST catalog: two patterns

Prerequisites

  • Familiarity with Snowflake and a Snowflake account (with ACCOUNTADMIN or a role that can create catalog integrations, external volumes, and databases)
  • Familiarity with Google Cloud and a Google Cloud project where you can enable APIs and grant IAM roles
  • The gcloud CLI installed and authenticated (gcloud auth login)

What You'll Learn

  • The BigLake Iceberg REST catalog model and its two warehouse flavours
  • How to authenticate Snowflake to Google with workload identity federation (no keys)
  • Pattern A: create an Iceberg table in Snowflake through a catalog-linked database and read it live in BigQuery
  • Pattern B: create a BigQuery-managed Iceberg table and read it in Snowflake via a catalog integration + external volume
  • How data freshness works in each direction and how to keep readers in sync

What You'll Build

  • A shared GCS bucket, a BigLake Iceberg REST catalog, and a workload identity federation pool/provider
  • A Snowflake catalog-linked database that writes an Iceberg table BigQuery reads (Pattern A)
  • A BigQuery Apache Iceberg managed table that Snowflake reads (Pattern B)

Regions must line up. Keep your GCS bucket, BigLake catalog, BigQuery datasets/connections, and Snowflake account in compatible regions to avoid cross-region latency and egress. This guide uses us-west1 on the Google side; substitute your own region consistently everywhere it appears.

Understanding the Architecture

Before touching a keyboard, it helps to have the mental model straight — this is what makes the two patterns click.

One catalog, one REST endpoint. Google's Lakehouse runtime catalog (the service historically called the BigLake metastore) speaks the open Apache Iceberg REST catalog protocol at https://biglake.googleapis.com/iceberg/v1/restcatalog. Any Iceberg-aware engine — Snowflake, BigQuery, Apache Spark, Trino — can talk to it.

Two warehouse flavours decide who owns the tables. When you create a catalog (or point an engine at one), the warehouse you choose changes the behavior fundamentally:

  • GCS flavour (warehouse = gs://<bucket>): the catalog itself owns Iceberg tables whose files live in your bucket. These are Google-managed Iceberg REST catalog tables. External engines that can write Iceberg (Snowflake via a catalog-linked database, or Spark) create and mutate these tables, and the catalog can vend short-lived, scoped storage credentials to readers/writers so they never need direct bucket IAM. BigQuery reads these tables live and natively using the four-part name project.catalog.namespace.table — but today BigQuery cannot run DML against them. → This is Pattern A.

  • BigQuery federation flavour (warehouse = bq://projects/<project>/locations/<location>): the catalog proxies BigQuery's own catalog. The tables here are Apache Iceberg managed tables — BigQuery-managed Iceberg tables (formerly "BigLake tables for Apache Iceberg in BigQuery"). BigQuery is the writer (full DML, streaming, CDC). This flavour does not vend credentials, so external readers like Snowflake attach their own storage access through an external volume. → This is Pattern B.

One BigLake REST endpoint, two warehouse flavours

Keyless by design. In both patterns Snowflake authenticates to Google using workload identity federation with an OAuth TOKEN_EXCHANGE grant. Snowflake presents a signed identity token; Google exchanges it for a short-lived access token scoped to the roles you grant a federated subject. There are no service-account keys to create, store, or rotate.

A note on names. In April 2026 Google rebranded BigLake to Lakehouse for Apache Iceberg and the BigLake metastore to the Lakehouse runtime catalog. The APIs, CLI (gcloud biglake), IAM roles, and endpoint hostname are unchanged and still say biglake, and Snowflake's documentation still refers to "BigLake." This guide uses BigLake to match the tooling.

Shared Setup: Project, Bucket, and Keyless Auth

Both patterns share the same Google project, storage bucket, and workload identity federation (WIF) trust between Snowflake and Google. Do this section once.

Set your variables

Run these in a local terminal. Substitute your own values; the rest of the guide reuses these names.

export PROJECT_ID="<your-gcp-project>"
export REGION="us-west1"
export BUCKET="<your-unique-bucket-name>"
export POOL_ID="snowflake-pool"
export PROVIDER_ID="snowflake-oidc"

gcloud config set project "$PROJECT_ID"
export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
echo "Project number: $PROJECT_NUMBER"

Enable the required APIs

gcloud services enable \
  biglake.googleapis.com \
  bigquery.googleapis.com \
  storage.googleapis.com \
  iam.googleapis.com \
  iamcredentials.googleapis.com \
  sts.googleapis.com \
  cloudresourcemanager.googleapis.com

Create the shared GCS bucket

gcloud storage buckets create "gs://$BUCKET" \
  --location="$REGION" \
  --uniform-bucket-level-access

Get Snowflake's identity issuer URL

Snowflake publishes an OIDC issuer that Google will trust. In a Snowflake workspace:

USE ROLE ACCOUNTADMIN;
SELECT SYSTEM$GET_WORKLOAD_IDENTITY_ISSUER_URL();

Copy the returned URL (looks like https://identity.snowflake.com/oauth2/.../.../...). Save it locally:

export SNOWFLAKE_ISSUER_URL="<paste-the-issuer-url-here>"

Create the workload identity pool and OIDC provider

gcloud iam workload-identity-pools create "$POOL_ID" \
  --location=global \
  --display-name="Snowflake pool"

gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
  --location=global \
  --workload-identity-pool="$POOL_ID" \
  --issuer-uri="$SNOWFLAKE_ISSUER_URL" \
  --attribute-mapping="google.subject=assertion.sub"

The audience that Snowflake's catalog integration will use is the provider resource name:

export OAUTH_AUDIENCE="//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID"
echo "$OAUTH_AUDIENCE"

Creating a WIF pool requires roles/iam.workloadIdentityPoolAdmin. If you grant it to yourself, allow ~60–90 seconds for the grant to propagate before the create commands succeed. If gcloud prompts hang in your shell, prefix commands with CLOUDSDK_CORE_DISABLE_PROMPTS=1.

You now have the shared foundation. Pick your direction below.

Pattern A: Snowflake Writes, BigQuery Reads

In this pattern the BigLake catalog owns the Iceberg tables (files in your bucket). Snowflake writes through a catalog-linked database and BigQuery reads the same tables live — no metadata copying.

Pattern A: Snowflake writes to a GCS-flavour BigLake catalog, BigQuery reads live

Create the BigLake catalog (GCS flavour) and a namespace

The catalog id is the bucket name. gcs-bucket selects the GCS warehouse flavour.

gcloud biglake iceberg catalogs create "$BUCKET" \
  --catalog-type=gcs-bucket

gcloud biglake iceberg namespaces create "analytics" \
  --catalog="$BUCKET"

Turn on credential vending

Vended credentials let the catalog hand short-lived storage tokens to Snowflake, so Snowflake never needs direct bucket IAM.

gcloud biglake iceberg catalogs update "$BUCKET" \
  --credential-mode=vended-credentials

This prints the catalog's service account (looks like blirc-<PROJECT_NUMBER>-xxxx@gcp-sa-biglakerestcatalog.iam.gserviceaccount.com). Grant it storage access on the bucket so it can vend:

export BIGLAKE_SA="<paste-the-blirc-...-service-account>"
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
  --member="serviceAccount:$BIGLAKE_SA" \
  --role="roles/storage.objectUser"

Create the Snowflake catalog integration

Back in Snowflake. The header key must be double-quoted.

USE ROLE ACCOUNTADMIN;

CREATE OR REPLACE CATALOG INTEGRATION biglake_int
  CATALOG_SOURCE = ICEBERG_REST
  TABLE_FORMAT = ICEBERG
  REST_CONFIG = (
    CATALOG_URI = 'https://biglake.googleapis.com/iceberg/v1/restcatalog'
    CATALOG_NAME = 'gs://<your-unique-bucket-name>'
    ACCESS_DELEGATION_MODE = VENDED_CREDENTIALS
    ADDITIONAL_HEADERS = ( "x-goog-user-project" = '<your-gcp-project>' )
  )
  REST_AUTHENTICATION = (
    TYPE = OAUTH
    OAUTH_GRANT_TYPE = TOKEN_EXCHANGE
    OAUTH_TOKEN_URI = 'https://sts.googleapis.com/v1/token'
    OAUTH_AUDIENCE = '<paste-your-OAUTH_AUDIENCE>'
    OAUTH_ALLOWED_SCOPES = ('https://www.googleapis.com/auth/bigquery')
  )
  ENABLED = TRUE;

Grant the federated subject its Google roles

Get the subject Snowflake will present:

DESC CATALOG INTEGRATION biglake_int;
-- copy the WORKLOAD_IDENTITY_FEDERATION_SUBJECT value

The federated subject changes every time you run CREATE OR REPLACE on the integration. If you recreate it, re-run DESC and re-apply the grants below.

Grant it (in your terminal). serviceUsageConsumer is required because of the x-goog-user-project billing header; biglake.viewer is enough for read in vended mode.

export SUBJECT="<paste-WORKLOAD_IDENTITY_FEDERATION_SUBJECT>"
export MEMBER="principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/subject/$SUBJECT"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="$MEMBER" --role="roles/serviceusage.serviceUsageConsumer" --condition=None
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="$MEMBER" --role="roles/biglake.viewer" --condition=None

Verify the integration

Allow ~60–90 seconds for IAM to propagate (a 403 right after granting is normal — wait and retry).

SELECT SYSTEM$VERIFY_CATALOG_INTEGRATION('biglake_int');

Create the catalog-linked database and write a table

A catalog-linked database (CLD) mirrors the BigLake catalog into Snowflake. Because vended credentials are on, no external volume is needed.

CREATE OR REPLACE DATABASE biglake_db
  LINKED_CATALOG = ( CATALOG = 'biglake_int' );

USE DATABASE biglake_db;
USE SCHEMA analytics;

CREATE OR REPLACE ICEBERG TABLE customers (id INT, name STRING);
INSERT INTO customers VALUES (1, 'Ada'), (2, 'Grace'), (3, 'Alan');
SELECT * FROM customers ORDER BY id;

Expected result:

IDNAME
1Ada
2Grace
3Alan

Read the same table live in BigQuery

No metadata copying, no external table definition. BigQuery reads the catalog directly with the four-part name project.catalog.namespace.table. In a BigQuery query window:

SELECT * FROM `<your-unique-bucket-name>.analytics.customers` ORDER BY id;

Expected result — the same three rows Snowflake just wrote:

idname
1Ada
2Grace
3Alan

Freshness in Pattern A is automatic. BigQuery reads the shared catalog on every query, so a new Snowflake INSERT is visible immediately on the next BigQuery SELECT. There is nothing to refresh.

BigQuery is read-only here today. Running DML (e.g. INSERT) against a GCS-flavour catalog table from BigQuery returns "DML statements are only supported over tables that have data stored in BigQuery." Bidirectional write to these tables is a Google preview feature (see What's Next). For BigQuery-side writes today, use Pattern B.

Pattern B: BigQuery Writes, Snowflake Reads

Here BigQuery owns the tables as Apache Iceberg managed tables, and Snowflake reads them through a catalog integration that federates BigQuery's catalog (bq://). Because this flavour does not vend credentials, Snowflake attaches its own storage access via an external volume.

Pattern B: BigQuery writes a managed Iceberg table, Snowflake reads via federation + external volume

Create a BigQuery connection and grant it storage

bq mk --connection --location="$REGION" --connection_type=CLOUD_RESOURCE biglake_conn

# get the connection's service account
bq show --connection --location="$REGION" --format=json "$PROJECT_ID.$REGION.biglake_conn"
export CONN_SA="<paste the serviceAccountId from the output>"

gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
  --member="serviceAccount:$CONN_SA" \
  --role="roles/storage.objectUser"

Create a dataset and a BigQuery-managed Iceberg table

In a BigQuery query window:

CREATE SCHEMA IF NOT EXISTS bq_val OPTIONS (location = 'us-west1');

CREATE OR REPLACE TABLE `bq_val.drivers`
(
  driver_id   INT64,
  driver_name STRING
)
WITH CONNECTION `us-west1.biglake_conn`
OPTIONS (
  file_format = 'PARQUET',
  table_format = 'ICEBERG',
  storage_uri = 'gs://<your-unique-bucket-name>/blmt/drivers'
);

INSERT INTO `bq_val.drivers` VALUES (10, 'BQ-Alice'), (20, 'BQ-Bob');

Publish the latest metadata for external readers

BigQuery-managed Iceberg tables publish an Iceberg metadata pointer that external engines read. After a write, publish it:

EXPORT TABLE METADATA FROM `bq_val.drivers`;

Create the Snowflake catalog integration (BigQuery federation)

Point CATALOG_NAME at bq:// and use EXTERNAL_VOLUME_CREDENTIALS — this flavour does not vend.

USE ROLE ACCOUNTADMIN;

CREATE OR REPLACE CATALOG INTEGRATION bq_fed_int
  CATALOG_SOURCE = ICEBERG_REST
  TABLE_FORMAT = ICEBERG
  REST_CONFIG = (
    CATALOG_URI = 'https://biglake.googleapis.com/iceberg/v1/restcatalog'
    CATALOG_NAME = 'bq://projects/<your-gcp-project>'
    ACCESS_DELEGATION_MODE = EXTERNAL_VOLUME_CREDENTIALS
    ADDITIONAL_HEADERS = ( "x-goog-user-project" = '<your-gcp-project>' )
  )
  REST_AUTHENTICATION = (
    TYPE = OAUTH
    OAUTH_GRANT_TYPE = TOKEN_EXCHANGE
    OAUTH_TOKEN_URI = 'https://sts.googleapis.com/v1/token'
    OAUTH_AUDIENCE = '<paste-your-OAUTH_AUDIENCE>'
    OAUTH_ALLOWED_SCOPES = ('https://www.googleapis.com/auth/bigquery')
  )
  ENABLED = TRUE;

Create an external volume over the bucket

The bq:// federation flavour does not support credential vending, so Snowflake cannot rely on the catalog to hand it storage tokens the way Pattern A does. Instead you attach your own read path to the data files with an external volume. Read-only is enough here.

CREATE OR REPLACE EXTERNAL VOLUME bq_extvol
  STORAGE_LOCATIONS = (
    (
      NAME = 'gcs-drivers'
      STORAGE_PROVIDER = 'GCS'
      STORAGE_BASE_URL = 'gcs://<your-unique-bucket-name>/'
    )
  )
  ALLOW_WRITES = FALSE;

DESC EXTERNAL VOLUME bq_extvol;
-- copy STORAGE_GCP_SERVICE_ACCOUNT

Grant Google IAM to BOTH Snowflake principals

Pattern B needs storage read for two distinct identities:

  1. The external-volume service account (the STORAGE_GCP_SERVICE_ACCOUNT from DESC) reads the data files. It also needs buckets.get to activate the volume — grant legacyBucketReader in addition to object read.
  2. The federated subject (from DESC CATALOG INTEGRATION bq_fed_int) reads metadata.json through the REST endpoint using its own identity — it needs storage.objectViewer too.
# 1) external-volume SA
export EXTVOL_SA="<paste STORAGE_GCP_SERVICE_ACCOUNT>"
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
  --member="serviceAccount:$EXTVOL_SA" --role="roles/storage.legacyBucketReader" --condition=None
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
  --member="serviceAccount:$EXTVOL_SA" --role="roles/storage.objectViewer" --condition=None

# 2) federated subject (re-DESC after any CREATE OR REPLACE)
export SUBJECT_B="<paste WORKLOAD_IDENTITY_FEDERATION_SUBJECT for bq_fed_int>"
export MEMBER_B="principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/subject/$SUBJECT_B"
for ROLE in serviceusage.serviceUsageConsumer biglake.viewer bigquery.dataViewer storage.objectViewer; do
  gcloud projects add-iam-policy-binding "$PROJECT_ID" \
    --member="$MEMBER_B" --role="roles/$ROLE" --condition=None
done

Verify, then create the Snowflake Iceberg table and read

SELECT SYSTEM$VERIFY_CATALOG_INTEGRATION('bq_fed_int');
SELECT SYSTEM$VERIFY_EXTERNAL_VOLUME('bq_extvol');

CREATE OR REPLACE DATABASE bq_demo;
USE SCHEMA bq_demo.public;

CREATE OR REPLACE ICEBERG TABLE drivers
  CATALOG = 'bq_fed_int'
  EXTERNAL_VOLUME = 'bq_extvol'
  CATALOG_NAMESPACE = 'bq_val'
  CATALOG_TABLE_NAME = 'drivers';

SELECT * FROM drivers ORDER BY driver_id;

Expected result — the rows BigQuery wrote:

DRIVER_IDDRIVER_NAME
10BQ-Alice
20BQ-Bob

First activation can be slow. The very first bind of a brand-new cross-cloud external volume can loop on "Query needs to be retried to setup external volume" for a few minutes. This almost always means the external-volume SA is missing buckets.get — confirm the legacyBucketReader grant above — then wait and retry.

Prefer a whole database? You do not have to register tables one at a time. Both patterns can also be consumed as a catalog-linked database, which auto-mirrors every namespace/table from the catalog. Pattern B works the same as Pattern A here, except it needs the external volume (there is no vending on bq://):

CREATE OR REPLACE DATABASE bq_linked
  LINKED_CATALOG = ( CATALOG = 'bq_fed_int' )
  EXTERNAL_VOLUME = 'bq_extvol';
SELECT * FROM bq_linked.bq_val.drivers ORDER BY driver_id;

Keeping Readers in Sync

Freshness behaves differently in each direction, and it is worth stating plainly.

Pattern A (BigQuery reading Snowflake's tables): live. BigQuery queries the shared catalog on every read, so new Snowflake writes appear immediately. Nothing to do.

Pattern B (Snowflake reading BigQuery's tables): reader refreshes after the writer publishes. BigQuery must publish updated Iceberg metadata, and Snowflake must pick it up. To see it in action, write another row in BigQuery:

-- BigQuery
INSERT INTO `bq_val.drivers` VALUES (30, 'BQ-Carol');
EXPORT TABLE METADATA FROM `bq_val.drivers`;

A plain re-query in Snowflake will still show the old rows. Refresh, then re-query:

-- Snowflake
ALTER ICEBERG TABLE bq_demo.public.drivers REFRESH;
SELECT * FROM bq_demo.public.drivers ORDER BY driver_id;

Expected result after refresh:

DRIVER_IDDRIVER_NAME
10BQ-Alice
20BQ-Bob
30BQ-Carol

To avoid manual refreshes, enable automatic refresh so Snowflake polls the catalog for newly published metadata:

ALTER ICEBERG TABLE bq_demo.public.drivers SET AUTO_REFRESH = TRUE;

With AUTO_REFRESH = TRUE, Snowflake keeps the table current as BigQuery publishes new metadata. You can also have BigQuery publish metadata automatically on mutation for managed Iceberg tables; contact Google to enable that on your project if you want zero manual EXPORT TABLE METADATA calls.

Cleanup

When you're done, remove everything the guide created so you don't leave billable storage or standing IAM behind. Run the subset for the pattern(s) you built — or all of it if you did both. Tear down Snowflake first (it references the catalogs), then Google Cloud.

Snowflake

USE ROLE ACCOUNTADMIN;

-- Pattern A
DROP DATABASE IF EXISTS biglake_db;              -- catalog-linked database
DROP CATALOG INTEGRATION IF EXISTS biglake_int;

-- Pattern B
DROP TABLE IF EXISTS bq_demo.public.drivers;
DROP DATABASE IF EXISTS bq_demo;
DROP DATABASE IF EXISTS bq_linked;               -- only if you built the CLD variant
DROP EXTERNAL VOLUME IF EXISTS bq_extvol;
DROP CATALOG INTEGRATION IF EXISTS bq_fed_int;

Google Cloud

Run these in the terminal where your $PROJECT_ID, $REGION, $BUCKET, $POOL_ID, and $PROVIDER_ID variables are still set.

# --- Pattern B: BigQuery table, dataset, and connection ---
bq rm -f -t "$PROJECT_ID:bq_val.drivers"
bq rm -f -d "$PROJECT_ID:bq_val"
bq rm --connection --location="$REGION" "$PROJECT_ID.$REGION.biglake_conn"

# --- Pattern A: BigLake table, namespace, and catalog ---
gcloud biglake iceberg tables delete customers \
  --namespace=analytics --catalog="$BUCKET" --quiet
gcloud biglake iceberg namespaces delete analytics \
  --catalog="$BUCKET" --quiet
gcloud biglake iceberg catalogs delete "$BUCKET" --quiet

# --- Shared: workload identity provider + pool ---
gcloud iam workload-identity-pools providers delete "$PROVIDER_ID" \
  --location=global --workload-identity-pool="$POOL_ID" --quiet
gcloud iam workload-identity-pools delete "$POOL_ID" --location=global --quiet

# --- Shared: the GCS bucket (removes all objects it holds) ---
gcloud storage rm --recursive "gs://$BUCKET"

On the IAM grants. The project-level role bindings you added to the federated subject (principal://.../subject/...) become inert the moment the workload identity pool is deleted, so there is nothing dangling to clean up. If you prefer to remove them explicitly, run gcloud projects remove-iam-policy-binding "$PROJECT_ID" --member=... --role=... for each role before deleting the pool. Deleting the BigQuery connection and BigLake catalog removes their auto-provisioned service accounts with them.

What's Next

  • Bidirectional writes to a single table (preview). Google has announced read and write interoperability for GCS-flavour Iceberg REST catalog tables — meaning BigQuery will be able to write the very same tables Snowflake writes in Pattern A. When that reaches your project you can collapse Patterns A and B into one truly shared, multi-writer table. Until then, choose the pattern that matches your primary writer.
  • Add a third engine. The same catalog is open to Apache Spark (Google's Managed Service for Apache Spark, or self-managed Spark) using the Iceberg REST catalog config with GoogleAuthManager and, for the GCS flavour, the X-Iceberg-Access-Delegation: vended-credentials header. Nothing about Snowflake or BigQuery changes.
  • Automate the plumbing. Move the gcloud/SQL steps into your IaC of choice and schedule EXPORT TABLE METADATA (Pattern B) so readers stay fresh without manual steps.

Conclusion and Resources

You built genuine, catalog-based interoperability between Snowflake and BigQuery on shared Apache Iceberg tables — no metadata files copied by hand and no service-account keys. You saw both directions and, importantly, why each one works the way it does: one BigLake Iceberg REST catalog with two warehouse flavours that decide table ownership, write access, credential vending, and freshness.

What You Learned

  • The BigLake Iceberg REST catalog model and its GCS vs. BigQuery-federation warehouse flavours
  • Keyless Snowflake-to-Google authentication with workload identity federation
  • Pattern A: Snowflake writes via a catalog-linked database; BigQuery reads live
  • Pattern B: BigQuery writes a managed Iceberg table; Snowflake reads via a federated catalog integration + external volume
  • How freshness differs by direction and how to keep readers in sync with REFRESH / AUTO_REFRESH

Resources

Updated 2026-07-08

This content is provided as is, and is not maintained on an ongoing basis. It may be out of date with current Snowflake instances